Foreign Function & Memory API (JEP 442): Calling Native Code Without JNI
Preview Feature in Java 21 — Finalized in Java 22 (JEP 454). The API shown here is the Java 21 preview version; it is nearly identical to the final API.
Why Replace JNI?
Java Native Interface (JNI) has been the standard way to call native code since Java 1.1. It works but is notoriously painful:
- Requires writing C wrapper code for every native call
- Native method signatures must exactly match Java declarations (or you get silent crashes)
- Memory management is manual — leak native memory once and you have a slow memory leak
- JNI calls disable JIT optimizations around the call site
- Debugging native crashes through JNI is extremely difficult
The Foreign Function & Memory (FFM) API replaces all of this: call native functions directly from Java, manage native memory safely with automatic lifetime management, and do it without writing a single line of C.
Core Concepts
flowchart TD
subgraph FFM["Foreign Function & Memory API"]
Linker["Linker\nBridge between Java and native ABI"]
SL["SymbolLookup\nFind native functions by name"]
FD["FunctionDescriptor\nDescribe C function signature in Java types"]
MH["MethodHandle\nInvoke the native function"]
Arena["Arena\nAllocate and manage native memory lifetime"]
MS["MemorySegment\nSafe reference to native memory region"]
end
Linker --> SL & FD & MH
Arena --> MS
Setting Up
No additional dependencies — FFM is part of the JDK. Enable preview in Java 21:
<!-- pom.xml -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>21</release>
<compilerArgs><arg>--enable-preview</arg></compilerArgs>
</configuration>
</plugin>
Also add --enable-native-access=ALL-UNNAMED to JVM args to suppress the native access warning:
java --enable-preview --enable-native-access=ALL-UNNAMED MyApp
Calling a C Standard Library Function
Example 1: strlen
C signature: size_t strlen(const char *s);
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
public class StrlenExample {
public static void main(String[] args) throws Throwable {
// 1. Get the native linker for this platform
Linker linker = Linker.nativeLinker();
// 2. Look up the symbol in libc
SymbolLookup stdlib = linker.defaultLookup();
MemorySegment strlenAddr = stdlib.find("strlen")
.orElseThrow(() -> new RuntimeException("strlen not found"));
// 3. Describe the C function signature
// size_t strlen(const char *s)
// → returns JAVA_LONG, takes ADDRESS
FunctionDescriptor descriptor = FunctionDescriptor.of(
ValueLayout.JAVA_LONG, // return type: size_t (64-bit)
ValueLayout.ADDRESS // parameter: const char*
);
// 4. Create a MethodHandle — the callable bridge
MethodHandle strlen = linker.downcallHandle(strlenAddr, descriptor);
// 5. Allocate native memory and call the function
try (Arena arena = Arena.ofConfined()) {
MemorySegment cString = arena.allocateUtf8String("Hello, FFM!");
long length = (long) strlen.invoke(cString);
System.out.println("strlen = " + length); // 11
}
// Arena closed — native memory freed automatically
}
}
Example 2: printf
C signature: int printf(const char *format, ...); (variadic)
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MemorySegment printfAddr = stdlib.find("printf").orElseThrow();
// Describe concrete call (non-variadic portion)
FunctionDescriptor descriptor = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // int return
ValueLayout.ADDRESS, // const char* format
ValueLayout.JAVA_INT // int argument
);
MethodHandle printf = linker.downcallHandle(printfAddr, descriptor);
try (Arena arena = Arena.ofConfined()) {
MemorySegment format = arena.allocateUtf8String("Value: %d\n");
printf.invoke(format, 42); // prints: Value: 42
}
Memory Management with Arena
Arena controls the lifetime of native memory allocations. When the arena closes, all memory allocated through it is freed — no leaks, even if an exception is thrown.
Arena Types
// Confined arena — thread-restricted, deterministic close
try (Arena arena = Arena.ofConfined()) {
MemorySegment seg = arena.allocate(1024);
// Only the creating thread can access seg
// Freed when try block exits
}
// Shared arena — accessible from multiple threads
try (Arena arena = Arena.ofShared()) {
MemorySegment seg = arena.allocate(1024);
// Any thread can read/write seg
// Freed when try block exits
}
// Auto arena — GC-managed lifetime (no explicit close)
Arena arena = Arena.ofAuto();
MemorySegment seg = arena.allocate(1024);
// Freed when arena object is GC'd (not deterministic)
// Global arena — lives for the JVM lifetime
MemorySegment seg = Arena.global().allocate(1024);
// Never freed — use for truly permanent native structures
MemorySegment — Working with Native Memory
Reading and Writing Primitives
try (Arena arena = Arena.ofConfined()) {
// Allocate 16 bytes
MemorySegment segment = arena.allocate(16);
// Write values at specific byte offsets
segment.set(ValueLayout.JAVA_INT, 0, 42); // write int at offset 0
segment.set(ValueLayout.JAVA_LONG, 4, 1000L); // write long at offset 4
segment.set(ValueLayout.JAVA_DOUBLE, 12, 3.14); // write double at offset 12
// Read them back
int i = segment.get(ValueLayout.JAVA_INT, 0);
long l = segment.get(ValueLayout.JAVA_LONG, 4);
double d = segment.get(ValueLayout.JAVA_DOUBLE, 12);
System.out.printf("int=%d, long=%d, double=%.2f%n", i, l, d);
}
Working with Arrays
try (Arena arena = Arena.ofConfined()) {
// Allocate a native int array of 10 elements
MemorySegment intArray = arena.allocateArray(ValueLayout.JAVA_INT, 10);
// Write elements
for (int i = 0; i < 10; i++) {
intArray.setAtIndex(ValueLayout.JAVA_INT, i, i * i);
}
// Read elements
for (int i = 0; i < 10; i++) {
System.out.print(intArray.getAtIndex(ValueLayout.JAVA_INT, i) + " ");
}
// 0 1 4 9 16 25 36 49 64 81
}
Copying Between Java and Native Memory
try (Arena arena = Arena.ofConfined()) {
byte[] javaBytes = "Hello, native!".getBytes(StandardCharsets.UTF_8);
// Copy Java array to native memory
MemorySegment nativeBuffer = arena.allocate(javaBytes.length);
MemorySegment.copy(javaBytes, 0, nativeBuffer, ValueLayout.JAVA_BYTE, 0, javaBytes.length);
// Copy native memory back to Java array
byte[] result = new byte[javaBytes.length];
MemorySegment.copy(nativeBuffer, ValueLayout.JAVA_BYTE, 0, result, 0, result.length);
System.out.println(new String(result)); // Hello, native!
}
Mapping C Structs
Use MemoryLayout to describe C struct layouts:
// C struct: struct Point { int x; int y; };
StructLayout pointLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")
);
// VarHandles for field access
VarHandle xHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("x"));
VarHandle yHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("y"));
try (Arena arena = Arena.ofConfined()) {
MemorySegment point = arena.allocate(pointLayout);
xHandle.set(point, 0L, 10); // set x = 10
yHandle.set(point, 0L, 20); // set y = 20
int x = (int) xHandle.get(point, 0L);
int y = (int) yHandle.get(point, 0L);
System.out.printf("Point(%d, %d)%n", x, y); // Point(10, 20)
}
Loading a Native Library
// Load a custom shared library (e.g., libmylib.so on Linux, mylib.dll on Windows)
SymbolLookup myLib = SymbolLookup.libraryLookup("mylib", Arena.ofAuto());
// Or load by full path
SymbolLookup myLib = SymbolLookup.libraryLookup(Path.of("/usr/local/lib/libmylib.so"), Arena.ofAuto());
// Find and call a function from the library
MemorySegment computeAddr = myLib.find("compute")
.orElseThrow(() -> new RuntimeException("compute not found in mylib"));
FunctionDescriptor desc = FunctionDescriptor.of(
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_INT
);
MethodHandle compute = Linker.nativeLinker().downcallHandle(computeAddr, desc);
try (Arena arena = Arena.ofConfined()) {
double result = (double) compute.invoke(3.14, 2);
System.out.println("Result: " + result);
}
Upcalls — Calling Java from Native Code
FFM supports upcalls: pass a Java method as a C function pointer to a native function:
// Define a Java method that matches the C callback signature
// C: typedef int (*comparator)(const void* a, const void* b);
MethodHandle javaComparator = MethodHandles.lookup().findStatic(
MyClass.class, "compareInts",
MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class)
);
// Wrap as a native function pointer
try (Arena arena = Arena.ofConfined()) {
FunctionDescriptor callbackDesc = FunctionDescriptor.of(
ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS
);
MemorySegment callbackPointer = Linker.nativeLinker()
.upcallStub(javaComparator, callbackDesc, arena);
// Pass callbackPointer to qsort or any native function that takes a callback
}
// The Java callback method
static int compareInts(MemorySegment a, MemorySegment b) {
int ia = a.get(ValueLayout.JAVA_INT, 0);
int ib = b.get(ValueLayout.JAVA_INT, 0);
return Integer.compare(ia, ib);
}
FFM vs JNI Comparison
| Aspect | JNI | FFM API |
|---|---|---|
| C wrapper code | Required | Not required |
| Type safety | Runtime crashes on mismatch | Descriptor checked at call time |
| Memory lifetime | Manual free() | Arena-scoped, automatic |
| Performance | JIT-inhibited around calls | Full JIT optimization |
| Debugging | Crash dumps, hard to trace | Normal Java stack traces |
| Platform native lib | Required | Not required |
| Learning curve | Very high (C + JNI conventions) | High (Java only) |
Key Takeaways
Linker.nativeLinker()provides the bridge to the platform’s C ABISymbolLookupfinds native functions by name in the standard library or a loaded libraryFunctionDescriptordescribes the C function signature using JavaValueLayouttypesLinker.downcallHandle()creates aMethodHandlethat invokes the native functionArenamanages native memory lifetime — always use try-with-resources for confined/shared arenasMemorySegmentis a safe, bounds-checked reference to a native memory region- Upcalls let you pass Java methods as native function pointers (callbacks)
- Finalized in Java 22 — Java 21 preview is nearly identical to the final API
Next: Vector API (JEP 448) — express data-parallel SIMD computations in Java that the JIT compiles to hardware vector instructions.