11.1 Foreign Function & Memory API Fundamentals

The Foreign Function & Memory (FFM) API provides safe, efficient interoperability with native code and off-heap memory without JNI complexity.

Why FFM Over JNI?

JNI Limitations:

// JNI requires writing C glue code
// my_native.c
#include <jni.h>

JNIEXPORT jlong JNICALL Java_MyClass_nativeMethod(
    JNIEnv *env, jobject obj, jstring input) {

    const char *str = (*env)->GetStringUTFChars(env, input, 0);
    long result = strlen(str);
    (*env)->ReleaseStringUTFChars(env, input, str);
    return result;
}

// Then compile native library, manage shared library loading
// Complex error-prone manual memory management

FFM Advantages:

// Pure Java - no C glue code needed
// Direct calls to native functions
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class FFMExample {
    public static long strlen(String s) throws Throwable {
        try (Arena arena = Arena.ofConfined()) {
            Linker linker = Linker.nativeLinker();
            SymbolLookup stdlib = linker.defaultLookup();

            MemorySegment symbol = stdlib.find("strlen")
                .orElseThrow(() -> new UnsatisfiedLinkError("strlen"));

            MethodHandle strlen = linker.downcallHandle(
                symbol,
                FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
            );

            MemorySegment cstr = arena.allocateUtf8String(s);
            return (long) strlen.invoke(cstr);
        }
    }
}

// Safer - compiler-checked types
// Cleaner - no manual memory management
// Faster - JIT optimized

Core Concepts

1. Arena - Memory Lifecycle Management:

// Arena manages memory segment lifetimes
// All allocations freed when arena closes

// Confined arena - single thread access
try (Arena arena = Arena.ofConfined()) {
    MemorySegment segment = arena.allocate(1024);
    // Use segment...
} // Segment automatically freed

// Shared arena - multi-thread access
try (Arena arena = Arena.ofShared()) {
    MemorySegment segment = arena.allocate(1024);
    // Multiple threads can access
} // Freed when arena closes

// Global arena - never freed (use sparingly)
Arena global = Arena.global();
MemorySegment permanent = global.allocate(1024);
// Lives until JVM shutdown

2. MemorySegment - Typed Memory View:

// MemorySegment provides safe access to memory regions
try (Arena arena = Arena.ofConfined()) {
    // Allocate 100 bytes
    MemorySegment segment = arena.allocate(100);

    // Write values at specific offsets
    segment.set(ValueLayout.JAVA_INT, 0, 42);
    segment.set(ValueLayout.JAVA_INT, 4, 100);

    // Read values
    int first = segment.get(ValueLayout.JAVA_INT, 0);
    int second = segment.get(ValueLayout.JAVA_INT, 4);

    System.out.println("First: " + first);   // 42
    System.out.println("Second: " + second); // 100
}

3. ValueLayout - Memory Type Descriptors:

// ValueLayout describes memory types
public class LayoutExamples {
    // Primitive types
    ValueLayout.JAVA_BYTE     // 1 byte
    ValueLayout.JAVA_SHORT    // 2 bytes
    ValueLayout.JAVA_INT      // 4 bytes
    ValueLayout.JAVA_LONG     // 8 bytes
    ValueLayout.JAVA_FLOAT    // 4 bytes
    ValueLayout.JAVA_DOUBLE   // 8 bytes
    ValueLayout.ADDRESS       // Platform pointer size

    // Platform-specific sizes
    public static void showSizes() {
        System.out.println("INT size: " + ValueLayout.JAVA_INT.byteSize());
        System.out.println("LONG size: " + ValueLayout.JAVA_LONG.byteSize());
        System.out.println("ADDRESS size: " + ValueLayout.ADDRESS.byteSize());
    }
}

Memory Allocation Patterns

Allocating Primitives:

try (Arena arena = Arena.ofConfined()) {
    // Single int
    MemorySegment intSeg = arena.allocate(ValueLayout.JAVA_INT);
    intSeg.set(ValueLayout.JAVA_INT, 0, 42);

    // Array of ints
    MemorySegment intArray = arena.allocateArray(ValueLayout.JAVA_INT, 10);
    for (int i = 0; i < 10; i++) {
        intArray.setAtIndex(ValueLayout.JAVA_INT, i, i * 10);
    }

    // Read back
    for (int i = 0; i < 10; i++) {
        int value = intArray.getAtIndex(ValueLayout.JAVA_INT, i);
        System.out.println("intArray[" + i + "] = " + value);
    }
}

Allocating Strings:

try (Arena arena = Arena.ofConfined()) {
    // UTF-8 encoded C string (null-terminated)
    MemorySegment cString = arena.allocateUtf8String("Hello, World!");

    // Get string back
    String javaString = cString.getUtf8String(0);
    System.out.println(javaString);

    // Array of C strings
    String[] words = {"apple", "banana", "cherry"};
    MemorySegment[] cStrings = new MemorySegment[words.length];

    for (int i = 0; i < words.length; i++) {
        cStrings[i] = arena.allocateUtf8String(words[i]);
    }
}

Allocating Structures:

// Struct equivalent:
// struct Point {
//     int x;
//     int y;
// };

MemoryLayout POINT_LAYOUT = MemoryLayout.structLayout(
    ValueLayout.JAVA_INT.withName("x"),
    ValueLayout.JAVA_INT.withName("y")
);

try (Arena arena = Arena.ofConfined()) {
    MemorySegment point = arena.allocate(POINT_LAYOUT);

    // Access fields by path
    VarHandle xHandle = POINT_LAYOUT.varHandle(
        MemoryLayout.PathElement.groupElement("x")
    );
    VarHandle yHandle = POINT_LAYOUT.varHandle(
        MemoryLayout.PathElement.groupElement("y")
    );

    xHandle.set(point, 0L, 10);
    yHandle.set(point, 0L, 20);

    int x = (int) xHandle.get(point, 0L);
    int y = (int) yHandle.get(point, 0L);

    System.out.println("Point(" + x + ", " + y + ")");
}

Accessing Native Memory

Direct Memory Access:

try (Arena arena = Arena.ofConfined()) {
    MemorySegment buffer = arena.allocate(1024);

    // Write byte pattern
    for (long i = 0; i < 1024; i++) {
        buffer.set(ValueLayout.JAVA_BYTE, i, (byte) (i % 256));
    }

    // Read as different types
    int intValue = buffer.get(ValueLayout.JAVA_INT, 0);
    long longValue = buffer.get(ValueLayout.JAVA_LONG, 0);

    System.out.printf("As int: 0x%x%n", intValue);
    System.out.printf("As long: 0x%x%n", longValue);
}

Copying Between Segments:

try (Arena arena = Arena.ofConfined()) {
    MemorySegment src = arena.allocateArray(ValueLayout.JAVA_INT, 10);
    MemorySegment dst = arena.allocateArray(ValueLayout.JAVA_INT, 10);

    // Fill source
    for (int i = 0; i < 10; i++) {
        src.setAtIndex(ValueLayout.JAVA_INT, i, i * 100);
    }

    // Copy using MemorySegment.copy
    MemorySegment.copy(
        src, ValueLayout.JAVA_INT, 0,
        dst, ValueLayout.JAVA_INT, 0,
        10
    );

    // Verify
    for (int i = 0; i < 10; i++) {
        System.out.println(dst.getAtIndex(ValueLayout.JAVA_INT, i));
    }
}

Slicing Segments:

try (Arena arena = Arena.ofConfined()) {
    MemorySegment large = arena.allocate(1000);

    // Create slice of first 100 bytes
    MemorySegment slice = large.asSlice(0, 100);

    // Modifications to slice affect original
    slice.set(ValueLayout.JAVA_INT, 0, 42);

    int value = large.get(ValueLayout.JAVA_INT, 0);
    System.out.println(value);  // 42
}

Safety and Lifecycle

Scope Safety:

// BAD - segment escapes arena scope
public MemorySegment dangerousMethod() {
    try (Arena arena = Arena.ofConfined()) {
        return arena.allocate(100);  // ILLEGAL!
    } // Segment freed here, but returned
}

// GOOD - segment stays within scope
public void safeMethod() {
    try (Arena arena = Arena.ofConfined()) {
        MemorySegment segment = arena.allocate(100);
        processSegment(segment);
    } // Safe - segment not leaked
}

Thread Safety:

// Confined arena - single thread only
try (Arena arena = Arena.ofConfined()) {
    MemorySegment segment = arena.allocate(100);

    // This will throw if called from another thread
    new Thread(() -> {
        try {
            segment.get(ValueLayout.JAVA_INT, 0);  // IllegalStateException!
        } catch (Exception e) {
            System.err.println("Cannot access from different thread");
        }
    }).start();
}

// Shared arena - multi-thread safe
try (Arena arena = Arena.ofShared()) {
    MemorySegment segment = arena.allocate(100);

    // Safe from multiple threads
    Thread t1 = new Thread(() -> {
        segment.set(ValueLayout.JAVA_INT, 0, 42);
    });
    Thread t2 = new Thread(() -> {
        int value = segment.get(ValueLayout.JAVA_INT, 0);
        System.out.println(value);
    });

    t1.start();
    t2.start();
    t1.join();
    t2.join();
}

Bounds Checking:

try (Arena arena = Arena.ofConfined()) {
    MemorySegment segment = arena.allocate(10);

    try {
        // Out of bounds - throws IndexOutOfBoundsException
        segment.get(ValueLayout.JAVA_INT, 20);
    } catch (IndexOutOfBoundsException e) {
        System.err.println("Bounds check prevented illegal access");
    }
}

Real-World Example: Reading Configuration File

import java.lang.foreign.*;
import java.nio.file.Path;

public class ConfigReader {
    // Config file structure:
    // struct Config {
    //     int version;
    //     int flags;
    //     char name[64];
    //     long timestamp;
    // };

    private static final MemoryLayout CONFIG_LAYOUT = MemoryLayout.structLayout(
        ValueLayout.JAVA_INT.withName("version"),
        ValueLayout.JAVA_INT.withName("flags"),
        MemoryLayout.sequenceLayout(64, ValueLayout.JAVA_BYTE).withName("name"),
        ValueLayout.JAVA_LONG.withName("timestamp")
    );

    public record Config(int version, int flags, String name, long timestamp) {}

    public static Config readConfig(Path filePath) throws Exception {
        try (Arena arena = Arena.ofConfined()) {
            // Memory-map the file
            MemorySegment mapped = MemorySegment.mapFile(
                filePath,
                0,
                CONFIG_LAYOUT.byteSize(),
                FileChannel.MapMode.READ_ONLY,
                arena.scope()
            );

            // Extract fields
            VarHandle versionHandle = CONFIG_LAYOUT.varHandle(
                MemoryLayout.PathElement.groupElement("version")
            );
            VarHandle flagsHandle = CONFIG_LAYOUT.varHandle(
                MemoryLayout.PathElement.groupElement("flags")
            );
            VarHandle timestampHandle = CONFIG_LAYOUT.varHandle(
                MemoryLayout.PathElement.groupElement("timestamp")
            );

            int version = (int) versionHandle.get(mapped, 0L);
            int flags = (int) flagsHandle.get(mapped, 0L);
            long timestamp = (long) timestampHandle.get(mapped, 0L);

            // Extract name (null-terminated string)
            MemorySegment nameSegment = mapped.asSlice(8, 64);
            String name = extractNullTerminatedString(nameSegment);

            return new Config(version, flags, name, timestamp);
        }
    }

    private static String extractNullTerminatedString(MemorySegment segment) {
        int length = 0;
        while (length < segment.byteSize() && 
               segment.get(ValueLayout.JAVA_BYTE, length) != 0) {
            length++;
        }

        byte[] bytes = new byte[length];
        MemorySegment.copy(segment, ValueLayout.JAVA_BYTE, 0,
                          bytes, 0, length);

        return new String(bytes, StandardCharsets.UTF_8);
    }
}

Best Practices

1. Use Try-With-Resources:

// Good - automatic cleanup
try (Arena arena = Arena.ofConfined()) {
    MemorySegment segment = arena.allocate(1024);
    // Use segment
} // Automatically freed

// Avoid - manual management
Arena arena = Arena.ofConfined();
MemorySegment segment = arena.allocate(1024);
// Easy to forget to close
arena.close();

2. Choose Right Arena Type:

// Single thread - use confined
try (Arena arena = Arena.ofConfined()) { }

// Multiple threads - use shared
try (Arena arena = Arena.ofShared()) { }

// Permanent data - use global (sparingly)
Arena global = Arena.global();

3. Validate Layouts:

// Verify layout matches native structure
MemoryLayout layout = MemoryLayout.structLayout(
    ValueLayout.JAVA_INT.withName("field1"),
    MemoryLayout.paddingLayout(4),  // Explicit padding
    ValueLayout.JAVA_LONG.withName("field2")
);

System.out.println("Total size: " + layout.byteSize());
System.out.println("Alignment: " + layout.byteAlignment());

4. Use Named Layouts:

// Good - named fields
MemoryLayout POINT = MemoryLayout.structLayout(
    ValueLayout.JAVA_INT.withName("x"),
    ValueLayout.JAVA_INT.withName("y")
);

// Better for maintenance and clarity

5. Handle Platform Differences:

// Use platform-neutral layouts
ValueLayout.ADDRESS  // Correct size on all platforms

// Check platform-specific values
if (ValueLayout.ADDRESS.byteSize() == 8) {
    // 64-bit platform
} else {
    // 32-bit platform
}