Java 26 Foreign Function & Memory API — distilled
Sources:
Oracle Java SE 26 core guide + java.lang.foreign API docs.
What FFM is for
FFM (java.lang.foreign) lets Java code:
- call native/foreign functions without JNI glue;
- pass Java code as native function pointers;
- allocate/access off-heap memory safely with bounds/lifetime checks;
- model native structs/unions/arrays/pointers via layouts.
FFM is stable since Java 22. Java 26 docs describe same core model with JDK 26 API pages.
Main types
| Type | Use |
|---|---|
Arena |
Owns lifetime of native segments. Allocate via allocate, allocateFrom; close to free. |
MemorySegment |
Bounds-checked view over heap/native/mapped memory. Has address, byte size, scope. |
SegmentAllocator |
Allocation interface implemented by Arena; helpers copy Java data to native memory. |
MemoryLayout |
Describes size/alignment/field structure; creates var handles. |
ValueLayout |
Primitive layouts (JAVA_INT, JAVA_LONG, ADDRESS, unaligned variants, byte order). |
AddressLayout |
Pointer layout. Optional target layout enables dereference sizing. |
SequenceLayout |
Array layout. |
StructLayout / UnionLayout |
C struct / union layout. Add explicit padding for ABI. |
FunctionDescriptor |
Native function signature: return layout + arg layouts. |
Linker |
ABI-specific bridge; creates downcall handles and upcall stubs. |
SymbolLookup |
Resolves native symbol names to function addresses. |
Linker.Option |
firstVariadicArg, captureCallState("errno"), critical/capture options. |
Arenas and lifetimes
Use arenas to allocate native/off-heap segments.
try (Arena arena = Arena.ofConfined()) {
MemorySegment buf = arena.allocate(1024, 8);
buf.set(JAVA_INT, 0, 42);
} // closes arena, frees buf backing memory
Arena kinds:
Arena.ofConfined()— deterministic lifetime, owner-thread only; default choice.Arena.ofShared()— deterministic lifetime, accessible/closable by multiple threads; close is safe/atomic.Arena.ofAuto()— GC-managed lifetime; any thread; cannot close manually.Arena.global()— never freed; any thread; cannot close manually.
Closed arena → segment access throws IllegalStateException.
Out-of-bounds → IndexOutOfBoundsException.
Wrong thread for confined arena → access/close fails.
Memory segments
Segment kinds:
- heap segment:
MemorySegment.ofArray(...), backed by Java array; - native segment: allocated by arena;
- mapped segment:
from
FileChannel.map(..., Arena).
Access:
int x = segment.get(JAVA_INT, 0); // byte offset
segment.set(JAVA_INT, 4, 123);
int y = segment.getAtIndex(JAVA_INT, 3); // logical index, offset = 3 * 4
segment.setAtIndex(JAVA_INT, 3, 456);
Strings:
MemorySegment c = arena.allocateFrom("hello"); // UTF-8 + NUL
String s = c.getString(0);
Arrays:
MemorySegment nativeInts = arena.allocateFrom(JAVA_INT, new int[] {3, 1, 2});
int[] copy = nativeInts.toArray(JAVA_INT);
Slicing:
MemorySegment slice = segment.asSlice(16, 8); // same backing memory, narrower bounds
Parallel/disjoint work:
use Arena.ofShared() and segment.elements(layout) when multiple threads need
access.
Alignment and endianness
Layouts carry byte size, byte alignment, and byte order. Segment access validates alignment.
- Native segment address + offset must satisfy layout alignment.
- Heap segment max alignment depends on backing array type.
- For packed/unaligned data use
JAVA_INT_UNALIGNED, etc. - For non-native byte order use
layout.withOrder(ByteOrder.BIG_ENDIAN).
int be = segment.get(JAVA_INT.withOrder(ByteOrder.BIG_ENDIAN), 0);
C type mapping
Always treat C mapping as ABI/platform-dependent. Prefer:
Map<String, MemoryLayout> canon = Linker.nativeLinker().canonicalLayouts();
MemoryLayout size_t = canon.get("size_t");
All native linkers provide canonical layouts for:
bool, char, short, int, long, long long, float, double,
size_t, wchar_t, void*.
Typical Linux/x64 mappings:
| C type | Java FFM layout | Java carrier |
|---|---|---|
bool |
JAVA_BOOLEAN |
boolean |
char, unsigned char |
JAVA_BYTE |
byte |
short, unsigned short |
JAVA_SHORT |
short |
int, unsigned int |
JAVA_INT |
int |
long, unsigned long |
JAVA_LONG |
long |
long long, unsigned long long |
JAVA_LONG |
long |
float |
JAVA_FLOAT |
float |
double |
JAVA_DOUBLE |
double |
size_t |
JAVA_LONG on 64-bit, JAVA_INT on 32-bit |
long/int |
T*, function pointer |
ADDRESS |
MemorySegment |
struct |
structLayout(...) |
MemorySegment |
union |
unionLayout(...) |
MemorySegment |
Unsigned types use same-size signed carriers; interpret with Java unsigned
helpers (Integer.toUnsignedLong, Long.compareUnsigned, masks) as needed.
Downcalls
Downcall = Java → native function.
Pattern:
static final Linker LINKER = Linker.nativeLinker();
static final SymbolLookup LOOKUP = LINKER.defaultLookup();
static final MethodHandle strlen = LINKER.downcallHandle(
LOOKUP.findOrThrow("strlen"),
FunctionDescriptor.of(JAVA_LONG, ADDRESS)
);
static long strlen(String s) throws Throwable {
try (Arena arena = Arena.ofConfined()) {
return (long) strlen.invokeExact(arena.allocateFrom(s));
}
}
Rules:
FunctionDescriptor.of(returnLayout, argLayouts...).FunctionDescriptor.ofVoid(argLayouts...)forvoidreturn.MethodHandle.invokeExactrequires exact Java carriers and return cast.ADDRESSargs/returns areMemorySegment.- Descriptor must match actual native signature. Linker cannot verify; mismatch can crash JVM.
Library lookup:
SymbolLookup std = Linker.nativeLinker().defaultLookup();
// restricted: loads native library; arena controls library lookup lifetime
SymbolLookup lib = SymbolLookup.libraryLookup("libc.so.6", arena);
libraryLookup is restricted because library loading may execute native code.
Upcalls
Upcall = native → Java through function pointer.
C:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
Java:
static int compareInts(MemorySegment a, MemorySegment b) {
return Integer.compare(a.get(JAVA_INT, 0), b.get(JAVA_INT, 0));
}
static final Linker LINKER = Linker.nativeLinker();
static final MethodHandle QSORT = LINKER.downcallHandle(
LINKER.defaultLookup().findOrThrow("qsort"),
FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS)
);
static void sort(int[] input) throws Throwable {
FunctionDescriptor cmpDesc = FunctionDescriptor.of(
JAVA_INT,
ADDRESS.withTargetLayout(JAVA_INT),
ADDRESS.withTargetLayout(JAVA_INT)
);
MethodHandle cmp = MethodHandles.lookup().findStatic(
MyClass.class,
"compareInts",
cmpDesc.toMethodType()
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment cmpPtr = LINKER.upcallStub(cmp, cmpDesc, arena);
MemorySegment arr = arena.allocateFrom(JAVA_INT, input);
QSORT.invokeExact(arr, (long) input.length, JAVA_INT.byteSize(), cmpPtr);
int[] sorted = arr.toArray(JAVA_INT);
}
}
Upcall stub lifetime must outlive native use. If native stores callback pointer, do not use a short-lived confined arena unless pointer is invalidated before close.
Pointers returned by native functions and pointer out-parameters
For C output parameters like T** out, allocate one pointer-sized slot, pass it
to native code, then read the actual returned pointer:
MemorySegment out = arena.allocate(ADDRESS);
int rc = some_open(..., out); // C signature: int some_open(..., T **out)
MemorySegment handle = out.get(ADDRESS, 0); // use this T* for later calls
Do not pass the slot (out) to later functions expecting T*; pass handle.
This distinction is critical for APIs such as sqlite3_open(..., sqlite3**) and
sqlite3_prepare_v2(..., sqlite3_stmt**).
Native pointer returns/read pointers usually become zero-length
MemorySegments:
- address is known;
- size is 0;
- scope is global/always alive;
- direct access throws out-of-bounds.
Attach bounds/lifetime before dereference.
Example malloc/free wrapper:
static final Linker LINKER = Linker.nativeLinker();
static final SymbolLookup C = LINKER.defaultLookup();
static final MethodHandle malloc = LINKER.downcallHandle(
C.findOrThrow("malloc"),
FunctionDescriptor.of(ADDRESS, JAVA_LONG)
);
static final MethodHandle free = LINKER.downcallHandle(
C.findOrThrow("free"),
FunctionDescriptor.ofVoid(ADDRESS)
);
static MemorySegment mallocSegment(long bytes, Arena arena) throws Throwable {
MemorySegment raw = (MemorySegment) malloc.invokeExact(bytes); // byteSize() == 0
return raw.reinterpret(bytes, arena, s -> {
try {
free.invokeExact(s);
} catch (Throwable t) {
throw new RuntimeException(t);
}
});
}
If bounds statically known for a pointer field/arg:
AddressLayout int4Ptr = ADDRESS.withTargetLayout(sequenceLayout(4, JAVA_INT));
MemorySegment p = segment.get(int4Ptr, offset); // byteSize() = 16
reinterpret and AddressLayout.withTargetLayout are restricted because wrong
bounds/lifetime can corrupt memory/crash JVM.
Structs, arrays, layouts
C:
struct Point { int x; int y; } pts[10];
Java layout + var handles:
static final SequenceLayout POINTS = sequenceLayout(10,
structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y")
)
);
static final VarHandle X = POINTS.varHandle(sequenceElement(), groupElement("x"));
static final VarHandle Y = POINTS.varHandle(sequenceElement(), groupElement("y"));
try (Arena arena = Arena.ofConfined()) {
MemorySegment pts = arena.allocate(POINTS);
X.set(pts, 0L, 3L, 10); // base offset, sequence index, value
int x = (int) X.get(pts, 0L, 3L);
}
For C padding/alignment:
// struct Example { int x; long y; } on many 64-bit ABIs
StructLayout EXAMPLE = structLayout(
JAVA_INT.withName("x"),
paddingLayout(4),
JAVA_LONG.withName("y")
);
Native linker function descriptors require well-formed layouts. Group layouts must include appropriate padding and satisfy natural alignment/size rules. Packed structs may be rejected by some linkers.
Variadic functions
FFM links specialized non-variadic forms.
Use Linker.Option.firstVariadicArg(index).
C:
int printf(const char *format, ...);
Java:
MethodHandle printf = LINKER.downcallHandle(
LINKER.defaultLookup().findOrThrow("printf"),
FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_INT, JAVA_INT, JAVA_INT),
Linker.Option.firstVariadicArg(1)
);
try (Arena arena = Arena.ofConfined()) {
int rc = (int) printf.invokeExact(
arena.allocateFrom("%d plus %d equals %d\n"), 2, 2, 4
);
}
C default promotions apply conceptually; FFM does not auto-promote.
Use promoted layouts for variadic args (float → double, small ints → int).
Linker rejects non-promoted variadic layouts depending on platform.
Capturing errno
Use Linker.Option.captureCallState("errno") on downcall handle and pass
captured-state segment as leading argument to the method handle.
static final Linker.Option CAP_ERRNO = Linker.Option.captureCallState("errno");
static final StructLayout CAPTURED = Linker.Option.captureStateLayout();
static final VarHandle ERRNO = CAPTURED.varHandle(groupElement("errno"));
static final MethodHandle fopen = LINKER.downcallHandle(
C.findOrThrow("fopen"),
FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS),
CAP_ERRNO
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment state = arena.allocate(CAPTURED);
MemorySegment file = (MemorySegment) fopen.invokeExact(
state,
arena.allocateFrom("missing.txt"),
arena.allocateFrom("r")
);
if (file.address() == 0) {
int errno = (int) ERRNO.get(state, 0L);
}
}
Use strerror(errno) to convert to message if needed; returned string pointer
likely needs reinterpret(Long.MAX_VALUE).getString(0) or known bound.
This is restricted/unsafe; prefer bounded APIs if available.
Restricted methods and native access
Restricted FFM-related operations:
Linker.downcallHandle(...)— signature mismatch is unsafe.Linker.upcallStub(...)— function pointer type/lifetime mismatch is unsafe.SymbolLookup.libraryLookup(...)— loading libraries can execute native code.MemorySegment.reinterpret(...)— changes bounds/lifetime alias.AddressLayout.withTargetLayout(...)— enables pointer dereference sizing.ModuleLayer.Controller.enableNativeAccess(...)— propagates native privilege.
Enable native access at run time; FFM itself is final in Java 26, so FFM-only
code does not need --enable-preview.
javac --release 26 Main.java
java --enable-native-access=ALL-UNNAMED Main
java --enable-native-access=my.module Main
More selective is better: put FFM code on module path and enable only that module.
Control illegal native access:
java --illegal-native-access=allow|warn|deny ...
JDK 24+ default is warn; future default expected deny.
jextract
Use jextract when binding nontrivial headers.
Obtain:
https://jdk.java.net/jextract/
Source:
https://github.com/openjdk/jextract
Typical:
jextract \
-l :/absolute/path/to/libfoo.so \
--output gensrc \
-I /path/to/include \
-t com.example.foo \
/path/to/foo.h
javac -sourcepath gensrc Main.java
java -cp gensrc:. --enable-native-access=ALL-UNNAMED Main
Generated bindings handle many descriptors, layouts, and upcall helpers. Still review ownership/lifetime/error conventions.
Common bugs
- Missing
--enable-native-access→ warning orIllegalCallerExceptiondepending config/JDK. invokeExactargs not exact carriers (intvslong, missing return cast) →WrongMethodTypeException.- Using Linux/x64 layout assumptions on Windows/x64 (
longmismatch). - Mapping
size_t/strlentoJAVA_INTon Linux/x64; useJAVA_LONGor canonicalsize_tlayout. - Guessing C macro constants such as
sysconfnames; usejextractor generated constants. - Accessing arena segment after close →
IllegalStateException. - Passing confined-arena segment to another thread → failure.
- Dereferencing zero-length pointer without sizing →
IndexOutOfBoundsException. - Confusing pointer out-parameter slots (
T**) with returned handles (T*) → nativeSQLITE_MISUSE, crashes, or corrupted state. - Wrong
FunctionDescriptor→ unspecified behavior/JVM crash. - Forgetting C struct padding/alignment → corrupted fields/native call failure.
- Upcall stub arena closes while native code still stores callback pointer → dangling function pointer.
- Native code stores pointer to arena buffer after arena close → dangling pointer.
- Treating unsigned values as signed without conversion.
- Assuming Java memory model guarantees for native memory; normal JMM guarantees do not apply to off-heap native segments.
- Using
Arena.global()orofAuto()where deterministic cleanup required.
Decision guide
- Small C function, simple signature → hand-code downcall.
- Many functions/structs/macros/header churn →
jextract. - Native returns allocated pointer → wrap with
reinterpret(size, arena, cleanup)and matching free. - Native writes into caller buffer → allocate in arena, pass segment, read after call.
- Native stores callback/buffer past call → allocate with lifetime covering native use; define explicit close/unregister.
- Cross-thread native access → shared arena or copy data to heap.