13.2 Downcalls and Function Descriptors
Master creating downcall handles for calling native functions with proper type mapping and calling conventions.
Function Descriptor Basics
Understanding Function Descriptors:
import java.lang.foreign.*;
public class FunctionDescriptorBasics {
// No parameters, returns int
// C: int getpid(void);
private static final FunctionDescriptor GETPID_DESC =
FunctionDescriptor.of(ValueLayout.JAVA_INT);
// One parameter, returns long
// C: size_t strlen(const char *s);
private static final FunctionDescriptor STRLEN_DESC =
FunctionDescriptor.of(
ValueLayout.JAVA_LONG, // Return type
ValueLayout.ADDRESS // Parameter: const char*
);
// Multiple parameters, returns int
// C: int strcmp(const char *s1, const char *s2);
private static final FunctionDescriptor STRCMP_DESC =
FunctionDescriptor.of(
ValueLayout.JAVA_INT, // Return type
ValueLayout.ADDRESS, // Parameter 1
ValueLayout.ADDRESS // Parameter 2
);
// Void return
// C: void *memset(void *s, int c, size_t n);
private static final FunctionDescriptor MEMSET_DESC =
FunctionDescriptor.of(
ValueLayout.ADDRESS, // Return: void* (pointer)
ValueLayout.ADDRESS, // Parameter: void *s
ValueLayout.JAVA_INT, // Parameter: int c
ValueLayout.JAVA_LONG // Parameter: size_t n
);
// No return value
// C: void exit(int status);
private static final FunctionDescriptor EXIT_DESC =
FunctionDescriptor.ofVoid(
ValueLayout.JAVA_INT // Parameter: int status
);
}
Creating Downcall Handles:
import java.lang.invoke.MethodHandle;
public class DowncallCreation {
private final Linker linker = Linker.nativeLinker();
private final SymbolLookup lookup = linker.defaultLookup();
public void createBasicDowncalls() throws Throwable {
// Simple function: int getpid(void)
MemorySegment getpidSym = lookup.find("getpid").orElseThrow();
MethodHandle getpid = linker.downcallHandle(
getpidSym,
FunctionDescriptor.of(ValueLayout.JAVA_INT)
);
int pid = (int) getpid.invoke();
System.out.println("Process ID: " + pid);
// Function with parameters: size_t strlen(const char *s)
MemorySegment strlenSym = lookup.find("strlen").orElseThrow();
MethodHandle strlen = linker.downcallHandle(
strlenSym,
FunctionDescriptor.of(
ValueLayout.JAVA_LONG,
ValueLayout.ADDRESS
)
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment str = arena.allocateUtf8String("Hello");
long len = (long) strlen.invoke(str);
System.out.println("String length: " + len);
}
}
}
Type Mapping Guidelines
Primitive Type Mapping:
public class TypeMapping {
// C type -> Java layout
// char -> JAVA_BYTE
// short -> JAVA_SHORT
// int -> JAVA_INT
// long (LP64) -> JAVA_LONG
// long long -> JAVA_LONG
// float -> JAVA_FLOAT
// double -> JAVA_DOUBLE
// void* -> ADDRESS
// size_t -> JAVA_LONG (on LP64)
// bool (C++) -> JAVA_BOOLEAN
// Platform-specific considerations
private static final ValueLayout SIZE_T_LAYOUT =
ValueLayout.JAVA_LONG; // LP64 systems
private static final ValueLayout POINTER_LAYOUT =
ValueLayout.ADDRESS;
// Example: malloc(size_t size) -> void*
private static final FunctionDescriptor MALLOC_DESC =
FunctionDescriptor.of(
POINTER_LAYOUT, // void* return
SIZE_T_LAYOUT // size_t parameter
);
}
Struct Parameter Mapping:
public class StructParameters {
// C struct passed by value
// struct Point { int x; int y; };
// void process_point(struct Point p);
private static final MemoryLayout POINT_LAYOUT =
MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")
);
private static final FunctionDescriptor PROCESS_POINT_DESC =
FunctionDescriptor.ofVoid(POINT_LAYOUT); // Pass by value
public void callWithStruct() throws Throwable {
try (Arena arena = Arena.ofConfined()) {
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = SymbolLookup.loaderLookup();
MethodHandle processPoint = linker.downcallHandle(
lookup.find("process_point").orElseThrow(),
PROCESS_POINT_DESC
);
// Allocate and populate struct
MemorySegment point = arena.allocate(POINT_LAYOUT);
point.set(ValueLayout.JAVA_INT, 0, 10); // x
point.set(ValueLayout.JAVA_INT, 4, 20); // y
// Call native function
processPoint.invoke(point);
}
}
}
Pointer Parameter Mapping:
public class PointerParameters {
// C: int read_file(const char *path, char *buffer, int size);
private static final FunctionDescriptor READ_FILE_DESC =
FunctionDescriptor.of(
ValueLayout.JAVA_INT, // Return: int
ValueLayout.ADDRESS, // const char *path
ValueLayout.ADDRESS, // char *buffer (output)
ValueLayout.JAVA_INT // int size
);
public byte[] readFile(String path, int bufferSize) throws Throwable {
try (Arena arena = Arena.ofConfined()) {
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = SymbolLookup.loaderLookup();
MethodHandle readFile = linker.downcallHandle(
lookup.find("read_file").orElseThrow(),
READ_FILE_DESC
);
// Allocate parameters
MemorySegment pathStr = arena.allocateUtf8String(path);
MemorySegment buffer = arena.allocate(bufferSize);
// Call function
int bytesRead = (int) readFile.invoke(pathStr, buffer, bufferSize);
if (bytesRead < 0) {
throw new RuntimeException("Read failed");
}
// Extract data
byte[] result = new byte[bytesRead];
MemorySegment.copy(
buffer, ValueLayout.JAVA_BYTE, 0,
result, 0,
bytesRead
);
return result;
}
}
}
Variadic Functions
Handling Variadic Arguments:
public class VariadicFunctions {
// C: int printf(const char *format, ...);
// Note: Variadic functions require special handling
private final Linker linker = Linker.nativeLinker();
private final SymbolLookup lookup = linker.defaultLookup();
/**
* Call printf with specific arguments
*/
public void printfExample() throws Throwable {
try (Arena arena = Arena.ofConfined()) {
MemorySegment printfSym = lookup.find("printf").orElseThrow();
// For specific call: printf("%d %s\n", 42, "hello")
FunctionDescriptor printfDesc = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // Return: int
ValueLayout.ADDRESS, // format string
ValueLayout.JAVA_INT, // arg 1: int
ValueLayout.ADDRESS // arg 2: const char*
);
MethodHandle printf = linker.downcallHandle(printfSym, printfDesc);
MemorySegment format = arena.allocateUtf8String("%d %s\n");
MemorySegment str = arena.allocateUtf8String("hello");
int result = (int) printf.invoke(format, 42, str);
System.out.println("printf returned: " + result);
}
}
/**
* Type-safe wrapper for common printf patterns
*/
public void printfString(String format, String arg) throws Throwable {
try (Arena arena = Arena.ofConfined()) {
MemorySegment printfSym = lookup.find("printf").orElseThrow();
FunctionDescriptor desc = FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
ValueLayout.ADDRESS
);
MethodHandle printf = linker.downcallHandle(printfSym, desc);
MemorySegment fmt = arena.allocateUtf8String(format);
MemorySegment str = arena.allocateUtf8String(arg);
printf.invoke(fmt, str);
}
}
public void printfInt(String format, int value) throws Throwable {
try (Arena arena = Arena.ofConfined()) {
MemorySegment printfSym = lookup.find("printf").orElseThrow();
FunctionDescriptor desc = FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
ValueLayout.JAVA_INT
);
MethodHandle printf = linker.downcallHandle(printfSym, desc);
MemorySegment fmt = arena.allocateUtf8String(format);
printf.invoke(fmt, value);
}
}
}
Return Value Handling
Different Return Types:
public class ReturnValueHandling {
private final Linker linker = Linker.nativeLinker();
private final SymbolLookup lookup = linker.defaultLookup();
/**
* Primitive return: int abs(int x)
*/
public int absoluteValue(int x) throws Throwable {
MethodHandle abs = linker.downcallHandle(
lookup.find("abs").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.JAVA_INT
)
);
return (int) abs.invoke(x);
}
/**
* Pointer return: char *getenv(const char *name)
*/
public String getEnvironmentVariable(String name) throws Throwable {
try (Arena arena = Arena.ofConfined()) {
MethodHandle getenv = linker.downcallHandle(
lookup.find("getenv").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.ADDRESS,
ValueLayout.ADDRESS
)
);
MemorySegment nameStr = arena.allocateUtf8String(name);
MemorySegment result = (MemorySegment) getenv.invoke(nameStr);
if (result.address() == 0) {
return null; // Not found
}
return result.reinterpret(Long.MAX_VALUE).getUtf8String(0);
}
}
/**
* Struct return by value
*/
public record Point(int x, int y) {}
private static final MemoryLayout POINT_LAYOUT =
MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")
);
public Point getOrigin() throws Throwable {
try (Arena arena = Arena.ofConfined()) {
MethodHandle getOrigin = linker.downcallHandle(
lookup.find("get_origin").orElseThrow(),
FunctionDescriptor.of(POINT_LAYOUT)
);
// Returns MemorySegment containing struct
MemorySegment result = (MemorySegment) getOrigin.invoke(arena);
int x = result.get(ValueLayout.JAVA_INT, 0);
int y = result.get(ValueLayout.JAVA_INT, 4);
return new Point(x, y);
}
}
}
Error Handling in Downcalls
Checking Error Codes:
public class ErrorHandlingDowncalls {
private final Linker linker = Linker.nativeLinker();
private final SymbolLookup lookup = linker.defaultLookup();
/**
* Handle errno-style errors
*/
public void handleErrno() throws Throwable {
try (Arena arena = Arena.ofConfined()) {
// C: int some_function(const char *path)
MethodHandle someFunc = linker.downcallHandle(
lookup.find("some_function").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS
)
);
MemorySegment path = arena.allocateUtf8String("/path/to/file");
int result = (int) someFunc.invoke(path);
if (result < 0) {
// Get errno
MemorySegment errnoAddr = lookup.find("errno").orElseThrow();
int errno = errnoAddr.get(ValueLayout.JAVA_INT, 0);
throw new RuntimeException(
"Function failed with errno: " + errno
);
}
}
}
/**
* Handle NULL pointer returns
*/
public MemorySegment allocateMemory(long size) throws Throwable {
// C: void *malloc(size_t size)
MethodHandle malloc = linker.downcallHandle(
lookup.find("malloc").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.ADDRESS,
ValueLayout.JAVA_LONG
)
);
MemorySegment ptr = (MemorySegment) malloc.invoke(size);
if (ptr.address() == 0) {
throw new OutOfMemoryError("malloc failed");
}
return ptr;
}
}
Real-World Example: Math Library Wrapper
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class MathLibrary {
private final Linker linker = Linker.nativeLinker();
private final SymbolLookup lookup;
private final Map<String, MethodHandle> handles = new ConcurrentHashMap<>();
public MathLibrary() {
// Load math library
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
// Math functions in msvcrt on Windows
System.loadLibrary("msvcrt");
} else {
// libm on Unix-like systems
System.loadLibrary("m");
}
this.lookup = SymbolLookup.loaderLookup();
// Initialize common functions
initializeFunctions();
}
private void initializeFunctions() {
try {
// double sin(double x)
createHandle("sin",
FunctionDescriptor.of(
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE
));
// double cos(double x)
createHandle("cos",
FunctionDescriptor.of(
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE
));
// double sqrt(double x)
createHandle("sqrt",
FunctionDescriptor.of(
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE
));
// double pow(double x, double y)
createHandle("pow",
FunctionDescriptor.of(
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE
));
// double exp(double x)
createHandle("exp",
FunctionDescriptor.of(
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE
));
// double log(double x)
createHandle("log",
FunctionDescriptor.of(
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE
));
// double floor(double x)
createHandle("floor",
FunctionDescriptor.of(
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE
));
// double ceil(double x)
createHandle("ceil",
FunctionDescriptor.of(
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE
));
} catch (Exception e) {
throw new RuntimeException("Failed to initialize math functions", e);
}
}
private void createHandle(String name, FunctionDescriptor descriptor) {
MemorySegment symbol = lookup.find(name)
.orElseThrow(() -> new UnsatisfiedLinkError(
"Math function not found: " + name
));
MethodHandle handle = linker.downcallHandle(symbol, descriptor);
handles.put(name, handle);
}
/**
* Sine function
*/
public double sin(double x) {
try {
return (double) handles.get("sin").invoke(x);
} catch (Throwable t) {
throw new RuntimeException("sin failed", t);
}
}
/**
* Cosine function
*/
public double cos(double x) {
try {
return (double) handles.get("cos").invoke(x);
} catch (Throwable t) {
throw new RuntimeException("cos failed", t);
}
}
/**
* Square root
*/
public double sqrt(double x) {
try {
return (double) handles.get("sqrt").invoke(x);
} catch (Throwable t) {
throw new RuntimeException("sqrt failed", t);
}
}
/**
* Power function
*/
public double pow(double x, double y) {
try {
return (double) handles.get("pow").invoke(x, y);
} catch (Throwable t) {
throw new RuntimeException("pow failed", t);
}
}
/**
* Exponential function
*/
public double exp(double x) {
try {
return (double) handles.get("exp").invoke(x);
} catch (Throwable t) {
throw new RuntimeException("exp failed", t);
}
}
/**
* Natural logarithm
*/
public double log(double x) {
try {
return (double) handles.get("log").invoke(x);
} catch (Throwable t) {
throw new RuntimeException("log failed", t);
}
}
/**
* Floor function
*/
public double floor(double x) {
try {
return (double) handles.get("floor").invoke(x);
} catch (Throwable t) {
throw new RuntimeException("floor failed", t);
}
}
/**
* Ceiling function
*/
public double ceil(double x) {
try {
return (double) handles.get("ceil").invoke(x);
} catch (Throwable t) {
throw new RuntimeException("ceil failed", t);
}
}
// Example usage
public static void main(String[] args) {
MathLibrary math = new MathLibrary();
System.out.println("sin(π/2) = " + math.sin(Math.PI / 2));
System.out.println("cos(π) = " + math.cos(Math.PI));
System.out.println("sqrt(16) = " + math.sqrt(16));
System.out.println("pow(2, 10) = " + math.pow(2, 10));
System.out.println("exp(1) = " + math.exp(1));
System.out.println("log(e) = " + math.log(Math.E));
System.out.println("floor(3.7) = " + math.floor(3.7));
System.out.println("ceil(3.2) = " + math.ceil(3.2));
}
}
Best Practices
1. Create Descriptors Once:
// Good - static final descriptor
private static final FunctionDescriptor STRLEN_DESC =
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);
// Bad - create every time
public void bad() {
FunctionDescriptor desc = FunctionDescriptor.of(
ValueLayout.JAVA_LONG, ValueLayout.ADDRESS
);
}
2. Cache Method Handles:
// Good - cache handles
private static final MethodHandle STRLEN;
static {
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = linker.defaultLookup();
STRLEN = linker.downcallHandle(
lookup.find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
}
// Bad - create every call
public void bad() throws Throwable {
MethodHandle strlen = linker.downcallHandle(...);
}
3. Validate Return Values:
// Good - check for errors
int result = (int) func.invoke();
if (result < 0) {
throw new RuntimeException("Function failed: " + result);
}
// Bad - ignore errors
int result = (int) func.invoke();
// Assume success
4. Match C Signatures Exactly:
// C: int strcmp(const char *s1, const char *s2);
FunctionDescriptor.of(
ValueLayout.JAVA_INT, // return int
ValueLayout.ADDRESS, // const char *s1
ValueLayout.ADDRESS // const char *s2
);
5. Handle Exceptions Properly:
// Good - wrap checked exceptions
public int callNative(String arg) {
try {
return (int) handle.invoke(allocateString(arg));
} catch (Throwable t) {
throw new RuntimeException("Native call failed", t);
}
}
These patterns enable safe, efficient downcall handling for native function integration.