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");
}