Foreign Function and Memory API in Java
Think of a Java program as a person who speaks only Java, and a C library as a shop where everyone speaks C. For years the only way to shop there was JNI, which meant writing glue code in C, compiling it for every operating system, and hoping nothing crashed. The Foreign Function and Memory API (FFM API) is like a good interpreter standing at the door: you describe what you want in plain Java, and it talks to the shop for you. In this lesson, you will learn what the FFM API is, its main classes, and how to call the C function strlen straight from Java.
What Is the FFM API?
The Foreign Function and Memory API in Java is a set of classes in the java.lang.foreign package that is used to call native functions (code written in C and similar languages) and to work with native memory (memory outside the Java heap). It became a final, standard feature in Java 22, so every Java 25 JDK has it with no extra flags.
It does two jobs:
- Foreign functions: find a function in a native library, describe its parameters and return type, and call it like a Java method.
- Foreign memory: allocate, read, write and free memory that the garbage collector doesn't manage, with bounds checks and a clear owner for every block.
Why Use It Instead of JNI?
| Point | JNI | FFM API |
|---|---|---|
| Glue code | You write and compile C code for every platform | Pure Java; no C to write |
| Safety | A wrong pointer can crash the JVM silently | Memory access is bounds-checked; freed memory can't be used |
| Freeing memory | Manual, easy to forget | Tied to an Arena; closing it frees everything |
| Speed | Good | As good or better, because the JIT understands the calls |
| Tools | Hand-written headers | jextract can generate the Java bindings from a C header |
Note that you rarely need native code at all. The FFM API is for the cases where you must use an existing C library (image processing, a hardware driver, a database client) or handle very large blocks of memory off the heap.
The Main Classes
| Class or interface | Description |
|---|---|
Linker | It is used to connect Java to native code. Linker.nativeLinker() returns the linker for the current platform. |
SymbolLookup | It is used to find a function by name in a library. linker.defaultLookup() covers the standard C library. |
FunctionDescriptor | It is used to describe a native function's return type and parameter types. |
MethodHandle | It is the callable result: a Java handle that invokes the native function. |
Arena | It is used to control the lifetime of native memory. Closing the arena frees all memory it allocated. |
MemorySegment | It is a block of memory with a known size, on or off the heap. All reads and writes are checked against its bounds. |
ValueLayout | It is used to describe one value's type and size, such as JAVA_INT, JAVA_LONG or ADDRESS (a pointer). |
Calling a C Function from Java
The C function strlen takes a pointer to text that ends with a zero byte, and returns how many characters come before that zero. Its C signature is size_t strlen(const char *s). On a 64-bit system size_t is 64 bits wide, which matches Java's long.
Calling it takes three steps: find the function, describe it, and call it with memory that holds the text. Let's see an example.
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
public class Main {
public static void main(String[] args) throws Throwable {
Linker linker = Linker.nativeLinker();
MemorySegment address = linker.defaultLookup()
.find("strlen")
.orElseThrow();
FunctionDescriptor signature = FunctionDescriptor.of(
ValueLayout.JAVA_LONG, // return type: size_t
ValueLayout.ADDRESS); // parameter: const char *
MethodHandle strlen = linker.downcallHandle(address, signature);
try (Arena arena = Arena.ofConfined()) {
MemorySegment name = arena.allocateFrom("Navika");
long length = (long) strlen.invokeExact(name);
System.out.println("Bytes in the segment: " + name.byteSize());
System.out.println("strlen returned: " + length);
}
}
}Bytes in the segment: 7
strlen returned: 6Explanation:
defaultLookup().find("strlen")searches the standard C library for the function and returns its address as aMemorySegment.FunctionDescriptor.of(JAVA_LONG, ADDRESS)tells Java the shape of the function: it returns alongand takes one pointer.downcallHandlecombines the address and the shape into aMethodHandle. A downcall is a call from Java down into native code.arena.allocateFrom("Navika")copies the string into native memory as UTF-8 and adds the zero byte at the end. That is why the segment holds 7 bytes whilestrlencounts 6.invokeExactmakes the call. The cast tolongmust match the descriptor exactly, or the call fails.- When the
tryblock ends, the arena closes and the memory is freed.
Working with Native Memory
You can also use a MemorySegment on its own, with no C function involved, for example to hold a large array outside the heap. Every access is checked, so reading past the end throws an exception instead of reading random memory.
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
public class Marks {
public static void main(String[] args) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment marks = arena.allocate(ValueLayout.JAVA_INT, 3);
marks.setAtIndex(ValueLayout.JAVA_INT, 0, 91);
marks.setAtIndex(ValueLayout.JAVA_INT, 1, 78);
marks.setAtIndex(ValueLayout.JAVA_INT, 2, 85);
int total = 0;
for (long i = 0; i < 3; i++) {
total += marks.getAtIndex(ValueLayout.JAVA_INT, i);
}
System.out.println("Size in bytes: " + marks.byteSize());
System.out.println("Total marks: " + total);
try {
marks.getAtIndex(ValueLayout.JAVA_INT, 3);
} catch (IndexOutOfBoundsException e) {
System.out.println("Index 3 is outside the segment");
}
}
}
}Size in bytes: 12
Total marks: 254
Index 3 is outside the segmentExplanation: allocate(JAVA_INT, 3) reserves room for three int values (4 bytes each) outside the heap. setAtIndex and getAtIndex work like array indexing. Asking for index 3 throws IndexOutOfBoundsException, so a bug can't quietly read memory it doesn't own.
Kinds of Arena
| Arena | Who can use it | When memory is freed |
|---|---|---|
Arena.ofConfined() | Only the thread that created it | When you call close() (use try-with-resources) |
Arena.ofShared() | Any thread | When you call close() |
Arena.ofAuto() | Any thread | When the garbage collector finds it unreachable |
Arena.global() | Any thread | Never; it lives as long as the program |
In Practice
For a real C library with dozens of functions, writing every FunctionDescriptor by hand is slow and easy to get wrong. The jextract tool (a separate download from the OpenJDK project) reads a C header file and generates the Java classes for you. You then call strlen(...) like a normal static method.
To load your own library instead of the C standard library, use SymbolLookup.libraryLookup(Path.of("libfees.so"), arena) and find functions in it the same way.
Common Mistakes
A descriptor that doesn't match the C function. Saying JAVA_INT when the function returns size_t gives wrong results or a crash. Check the C signature carefully, or let jextract do it.
Casting the result to the wrong type. invokeExact needs the exact type: (long) strlen.invokeExact(name), not (int).
Using a segment after its arena is closed. Any access throws IllegalStateException. Keep the work inside the try block.
Passing a Java String directly. Native code needs a pointer to bytes. Convert first with arena.allocateFrom(text).
Practice
- Easy: Change the first example to measure the length of
"Srihitha Anamaneni". Predict both numbers before you run it. - Harder: Look up the C function
abs(it takes and returns anint). Write the descriptor withJAVA_INTand call it with-42.
Key Takeaways
Was this tutorial helpful?