11.3 Native Function Calls and Symbol Lookup
Call native functions directly from Java using the Linker API for efficient foreign function invocation.
Symbol Lookup
Finding Native Functions:
import java.lang.foreign.*;
public class SymbolLookupExamples {
public static void demonstrateLookup() {
Linker linker = Linker.nativeLinker();
// Default lookup - system libraries
SymbolLookup stdlib = linker.defaultLookup();
// Find specific symbols
MemorySegment strlen = stdlib.find("strlen")
.orElseThrow(() -> new UnsatisfiedLinkError("strlen not found"));
MemorySegment printf = stdlib.find("printf")
.orElseThrow(() -> new UnsatisfiedLinkError("printf not found"));
System.out.println("Found strlen at: " + strlen.address());
System.out.println("Found printf at: " + printf.address());
}
// Load specific library
public static SymbolLookup loadLibrary(String libName) {
System.loadLibrary(libName);
return SymbolLookup.loaderLookup();
}
}
Library Loading:
// Load system library (platform-specific naming)
public class LibraryLoader {
public static SymbolLookup loadMath() {
// Linux: libm.so, macOS: libm.dylib, Windows: msvcrt.dll
String libName = switch (System.getProperty("os.name").toLowerCase()) {
case String s when s.contains("linux") -> "m";
case String s when s.contains("mac") -> "m";
case String s when s.contains("win") -> "msvcrt";
default -> throw new UnsupportedOperationException("Unknown platform");
};
System.loadLibrary(libName);
return SymbolLookup.loaderLookup();
}
// Load custom library
public static SymbolLookup loadCustomLibrary(Path libraryPath) {
System.load(libraryPath.toAbsolutePath().toString());
return SymbolLookup.loaderLookup();
}
}
Function Descriptors
Describing Native Function Signatures:
// C: size_t strlen(const char *s);
FunctionDescriptor STRLEN_DESC = FunctionDescriptor.of(
ValueLayout.JAVA_LONG, // Return: size_t
ValueLayout.ADDRESS // Param: const char*
);
// C: int printf(const char *format, ...);
// Note: variadic functions need special handling
FunctionDescriptor PRINTF_DESC = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // Return: int
ValueLayout.ADDRESS // Param: const char*
// Additional args handled separately
);
// C: void* malloc(size_t size);
FunctionDescriptor MALLOC_DESC = FunctionDescriptor.of(
ValueLayout.ADDRESS, // Return: void*
ValueLayout.JAVA_LONG // Param: size_t
);
// C: void free(void *ptr);
FunctionDescriptor FREE_DESC = FunctionDescriptor.ofVoid(
ValueLayout.ADDRESS // Param: void*
);
// C: int strcmp(const char *s1, const char *s2);
FunctionDescriptor STRCMP_DESC = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // Return: int
ValueLayout.ADDRESS, // Param: const char*
ValueLayout.ADDRESS // Param: const char*
);
Downcall Handles
Creating and Using Downcalls:
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
public class DowncallExamples {
private static final Linker LINKER = Linker.nativeLinker();
private static final SymbolLookup STDLIB = LINKER.defaultLookup();
// strlen example
public static long strlen(String s) throws Throwable {
try (Arena arena = Arena.ofConfined()) {
// Find symbol
MemorySegment strlenAddr = STDLIB.find("strlen")
.orElseThrow(() -> new UnsatisfiedLinkError("strlen"));
// Create downcall handle
MethodHandle strlen = LINKER.downcallHandle(
strlenAddr,
FunctionDescriptor.of(
ValueLayout.JAVA_LONG,
ValueLayout.ADDRESS
)
);
// Allocate C string
MemorySegment cString = arena.allocateUtf8String(s);
// Invoke
return (long) strlen.invoke(cString);
}
}
// getpid example
public static int getpid() throws Throwable {
MemorySegment getpidAddr = STDLIB.find("getpid")
.orElseThrow(() -> new UnsatisfiedLinkError("getpid"));
MethodHandle getpid = LINKER.downcallHandle(
getpidAddr,
FunctionDescriptor.of(ValueLayout.JAVA_INT) // No parameters
);
return (int) getpid.invoke();
}
// abs example
public static int abs(int value) throws Throwable {
MemorySegment absAddr = STDLIB.find("abs")
.orElseThrow(() -> new UnsatisfiedLinkError("abs"));
MethodHandle abs = LINKER.downcallHandle(
absAddr,
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.JAVA_INT
)
);
return (int) abs.invoke(value);
}
}
Reusable Downcall Pattern:
public class NativeLibrary {
private final Linker linker = Linker.nativeLinker();
private final SymbolLookup lookup;
public NativeLibrary(String libraryName) {
System.loadLibrary(libraryName);
this.lookup = SymbolLookup.loaderLookup();
}
public MethodHandle findFunction(String name, FunctionDescriptor descriptor) {
MemorySegment symbol = lookup.find(name)
.orElseThrow(() -> new UnsatisfiedLinkError(
"Function not found: " + name
));
return linker.downcallHandle(symbol, descriptor);
}
// Cache commonly used functions
private MethodHandle strlen;
public long strlen(Arena arena, String s) throws Throwable {
if (strlen == null) {
strlen = findFunction("strlen", FunctionDescriptor.of(
ValueLayout.JAVA_LONG,
ValueLayout.ADDRESS
));
}
MemorySegment cString = arena.allocateUtf8String(s);
return (long) strlen.invoke(cString);
}
}
Passing Complex Arguments
Structures as Arguments:
// C function: void process_point(struct Point p);
// struct Point { int x; int y; };
public class StructArguments {
private static final MemoryLayout POINT_LAYOUT = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")
);
public static void processPoint(int x, int y) throws Throwable {
try (Arena arena = Arena.ofConfined()) {
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = SymbolLookup.loaderLookup();
MemorySegment funcAddr = lookup.find("process_point")
.orElseThrow();
MethodHandle processPoint = linker.downcallHandle(
funcAddr,
FunctionDescriptor.ofVoid(POINT_LAYOUT) // Pass by value
);
// Allocate and populate struct
MemorySegment point = arena.allocate(POINT_LAYOUT);
VarHandle xHandle = POINT_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("x")
);
VarHandle yHandle = POINT_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("y")
);
xHandle.set(point, 0L, x);
yHandle.set(point, 0L, y);
// Call function
processPoint.invoke(point);
}
}
}
Pointer Arguments:
// C function: int read_config(struct Config *config);
public class PointerArguments {
private static final MemoryLayout CONFIG_LAYOUT = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("version"),
ValueLayout.JAVA_INT.withName("flags")
);
public record Config(int version, int flags) {}
public static Config readConfig() throws Throwable {
try (Arena arena = Arena.ofConfined()) {
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = SymbolLookup.loaderLookup();
MemorySegment funcAddr = lookup.find("read_config")
.orElseThrow();
MethodHandle readConfig = linker.downcallHandle(
funcAddr,
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS // Pointer to struct
)
);
// Allocate struct
MemorySegment config = arena.allocate(CONFIG_LAYOUT);
// Call function (fills config)
int result = (int) readConfig.invoke(config);
if (result != 0) {
throw new RuntimeException("read_config failed: " + result);
}
// Extract values
VarHandle versionHandle = CONFIG_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("version")
);
VarHandle flagsHandle = CONFIG_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("flags")
);
int version = (int) versionHandle.get(config, 0L);
int flags = (int) flagsHandle.get(config, 0L);
return new Config(version, flags);
}
}
}
Array Arguments:
// C function: int sum_array(int *arr, int count);
public class ArrayArguments {
public static int sumArray(int[] values) throws Throwable {
try (Arena arena = Arena.ofConfined()) {
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = SymbolLookup.loaderLookup();
MemorySegment funcAddr = lookup.find("sum_array")
.orElseThrow();
MethodHandle sumArray = linker.downcallHandle(
funcAddr,
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
ValueLayout.JAVA_INT
)
);
// Copy Java array to native memory
MemorySegment nativeArray = arena.allocateArray(
ValueLayout.JAVA_INT,
values
);
// Call function
return (int) sumArray.invoke(nativeArray, values.length);
}
}
}
Handling Return Values
Returning Structures:
// C function: struct Point get_origin(void);
public class StructReturns {
private static final MemoryLayout POINT_LAYOUT = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")
);
public record Point(int x, int y) {}
public static Point getOrigin() throws Throwable {
try (Arena arena = Arena.ofConfined()) {
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = SymbolLookup.loaderLookup();
MemorySegment funcAddr = lookup.find("get_origin")
.orElseThrow();
MethodHandle getOrigin = linker.downcallHandle(
funcAddr,
FunctionDescriptor.of(POINT_LAYOUT) // Return struct by value
);
// Call function - returns MemorySegment
MemorySegment result = (MemorySegment) getOrigin.invoke(arena);
// Extract fields
VarHandle xHandle = POINT_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("x")
);
VarHandle yHandle = POINT_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("y")
);
int x = (int) xHandle.get(result, 0L);
int y = (int) yHandle.get(result, 0L);
return new Point(x, y);
}
}
}
Returning Pointers:
// C function: char* get_version(void);
public class PointerReturns {
public static String getVersion() throws Throwable {
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = SymbolLookup.loaderLookup();
MemorySegment funcAddr = lookup.find("get_version")
.orElseThrow();
MethodHandle getVersion = linker.downcallHandle(
funcAddr,
FunctionDescriptor.of(ValueLayout.ADDRESS) // Return pointer
);
// Call function
MemorySegment strPtr = (MemorySegment) getVersion.invoke();
// Read null-terminated string
// Note: String lifetime managed by native code
return strPtr.reinterpret(Long.MAX_VALUE).getUtf8String(0);
}
}
Real-World Example: SQLite Integration
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
public class SQLiteWrapper {
private final Linker linker = Linker.nativeLinker();
private final SymbolLookup sqlite;
// Function handles
private final MethodHandle sqlite3_open;
private final MethodHandle sqlite3_close;
private final MethodHandle sqlite3_exec;
private final MethodHandle sqlite3_errmsg;
public SQLiteWrapper() throws Throwable {
// Load SQLite library
System.loadLibrary("sqlite3");
sqlite = SymbolLookup.loaderLookup();
// Bind functions
// int sqlite3_open(const char *filename, sqlite3 **ppDb);
sqlite3_open = linker.downcallHandle(
sqlite.find("sqlite3_open").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
ValueLayout.ADDRESS
)
);
// int sqlite3_close(sqlite3 *db);
sqlite3_close = linker.downcallHandle(
sqlite.find("sqlite3_close").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS
)
);
// int sqlite3_exec(sqlite3 *db, const char *sql,
// callback, void *arg, char **errmsg);
sqlite3_exec = linker.downcallHandle(
sqlite.find("sqlite3_exec").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
ValueLayout.ADDRESS,
ValueLayout.ADDRESS,
ValueLayout.ADDRESS,
ValueLayout.ADDRESS
)
);
// const char *sqlite3_errmsg(sqlite3 *db);
sqlite3_errmsg = linker.downcallHandle(
sqlite.find("sqlite3_errmsg").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.ADDRESS,
ValueLayout.ADDRESS
)
);
}
public void example() throws Throwable {
try (Arena arena = Arena.ofConfined()) {
// Open database
MemorySegment dbPtr = arena.allocate(ValueLayout.ADDRESS);
MemorySegment filename = arena.allocateUtf8String(":memory:");
int rc = (int) sqlite3_open.invoke(filename, dbPtr);
if (rc != 0) {
throw new RuntimeException("Failed to open database");
}
MemorySegment db = dbPtr.get(ValueLayout.ADDRESS, 0);
try {
// Execute SQL
MemorySegment sql = arena.allocateUtf8String(
"CREATE TABLE test (id INTEGER, name TEXT)"
);
rc = (int) sqlite3_exec.invoke(
db,
sql,
MemorySegment.NULL, // No callback
MemorySegment.NULL, // No callback arg
MemorySegment.NULL // No error message
);
if (rc != 0) {
MemorySegment errMsg = (MemorySegment) sqlite3_errmsg.invoke(db);
String error = errMsg.reinterpret(Long.MAX_VALUE).getUtf8String(0);
throw new RuntimeException("SQL error: " + error);
}
System.out.println("Table created successfully");
} finally {
// Close database
sqlite3_close.invoke(db);
}
}
}
}
Error Handling
Checking Return Codes:
public class ErrorHandling {
public static void safeCall() throws Throwable {
try (Arena arena = Arena.ofConfined()) {
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = SymbolLookup.loaderLookup();
MemorySegment funcAddr = lookup.find("some_function")
.orElseThrow();
MethodHandle func = linker.downcallHandle(
funcAddr,
FunctionDescriptor.of(ValueLayout.JAVA_INT)
);
int result = (int) func.invoke();
if (result < 0) {
// Get error message
MemorySegment errnoAddr = lookup.find("errno")
.orElseThrow();
int errno = errnoAddr.get(ValueLayout.JAVA_INT, 0);
throw new RuntimeException("Function failed with errno: " + errno);
}
}
}
}
Best Practices
1. Cache MethodHandles:
public class OptimizedWrapper {
// Create once, reuse many times
private static final MethodHandle STRLEN;
static {
try {
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MemorySegment addr = stdlib.find("strlen").orElseThrow();
STRLEN = linker.downcallHandle(
addr,
FunctionDescriptor.of(
ValueLayout.JAVA_LONG,
ValueLayout.ADDRESS
)
);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
public static long strlen(Arena arena, String s) throws Throwable {
return (long) STRLEN.invoke(arena.allocateUtf8String(s));
}
}
2. Validate Function Descriptors:
// Document expected C signature
// C: int process(const char *input, int flags, void *output);
FunctionDescriptor PROCESS_DESC = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // return int
ValueLayout.ADDRESS, // const char *input
ValueLayout.JAVA_INT, // int flags
ValueLayout.ADDRESS // void *output
);
3. Handle Platform Differences:
public class PlatformSpecific {
private static final boolean IS_WINDOWS =
System.getProperty("os.name").toLowerCase().contains("win");
public static SymbolLookup loadMathLibrary() {
String libName = IS_WINDOWS ? "msvcrt" : "m";
System.loadLibrary(libName);
return SymbolLookup.loaderLookup();
}
}
4. Use Try-With-Resources for Arenas:
// Good - automatic cleanup
public void goodExample() throws Throwable {
try (Arena arena = Arena.ofConfined()) {
MemorySegment str = arena.allocateUtf8String("test");
// Use str
} // Automatically freed
}
5. Check Symbol Existence:
// Graceful handling of missing symbols
Optional<MemorySegment> symbol = lookup.find("optional_function");
if (symbol.isPresent()) {
// Function available
MethodHandle func = linker.downcallHandle(symbol.get(), descriptor);
} else {
// Use fallback
System.out.println("Function not available, using fallback");
}