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
}