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.