Foreign Function & Memory API (JEP 412): First Look at Project Panama
Incubator Feature in Java 17 (JEP 412 — First Incubator). Package:
jdk.incubator.foreign. Requires--add-modules jdk.incubator.foreignat compile and runtime. This API evolved significantly across Java 18 (JEP 419), 19 (JEP 424), 20 (JEP 434), and was finalized in Java 22 (JEP 454). See the Java 21 article series for the final API. This article covers the Java 17 incubator version and the Project Panama vision.
Why Replace JNI?
Java Native Interface (JNI) has been the way to call native code from Java since Java 1.1. It works, but it is notorious for being painful:
JNI Problems:
- Header file generation:
javahgenerates C header files; you maintain two codebases (Java + C) - Manual memory management: Memory errors (use-after-free, double-free, buffer overruns) in C code can crash the JVM
- No safety: Type mismatches between Java and C cause undefined behavior — not
ClassCastExceptionbut process crashes - Slow iteration: A JNI bug requires recompiling the C library, restarting the JVM, and rebuilding
- No direct access from Java: You cannot inspect native memory contents in a debugger; you cannot safely pass Java objects to C
The Foreign Function & Memory API (Project Panama) replaces all of this with a pure-Java API that is:
- Safe: Off-heap memory is tracked and can be scoped (auto-released)
- Fast: Comparable performance to JNI at steady state; better for short-lived calls
- Pure Java: No separate C code required for most interop use cases
- Debuggable: Native memory can be inspected via Java tools
Enabling the Incubator Module
The Java 17 FFM API is in the jdk.incubator.foreign package. Add the module at compile and runtime:
Maven:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>17</release>
<compilerArgs>
<arg>--add-modules</arg>
<arg>jdk.incubator.foreign</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--add-modules jdk.incubator.foreign</argLine>
</configuration>
</plugin>
Command line:
javac --add-modules jdk.incubator.foreign Hello.java
java --add-modules jdk.incubator.foreign Hello
Core Concepts in Java 17
MemorySegment
A MemorySegment is a safe, bounded pointer to a region of memory. It tracks:
- Base address: Where the segment starts
- Byte size: How large the segment is (access outside this range throws an exception)
- Scope: Which code owns this memory and when it can be freed
import jdk.incubator.foreign.*;
// Allocate off-heap memory: 1024 bytes
try (MemorySession session = MemorySession.openConfined()) {
MemorySegment segment = MemorySegment.allocateNative(1024, session);
// Write an int at offset 0
segment.set(ValueLayout.JAVA_INT, 0, 42);
// Read it back
int value = segment.get(ValueLayout.JAVA_INT, 0);
System.out.println(value); // 42
} // memory is freed when session closes
MemorySession (Java 17) → Arena (Java 19+)
MemorySession controls the lifetime of native memory. Java 17 supports:
// Confined session: owned by one thread, freed when closed
try (MemorySession session = MemorySession.openConfined()) {
MemorySegment seg = MemorySegment.allocateNative(64, session);
// ...
} // freed here
// Shared session: can be used by multiple threads
try (MemorySession session = MemorySession.openShared()) {
MemorySegment seg = MemorySegment.allocateNative(64, session);
// Can be passed to other threads
}
// Implicit session: GC-managed (use with caution)
MemorySession session = MemorySession.openImplicit();
In Java 19+, MemorySession was renamed to Arena with the same semantics but a cleaner API.
ValueLayout
ValueLayout describes how to interpret memory bytes as Java primitives:
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 // pointer-sized (4 or 8 bytes)
Reading and Writing Native Memory
try (MemorySession session = MemorySession.openConfined()) {
// Allocate 8 bytes
MemorySegment seg = MemorySegment.allocateNative(8, session);
// Write a long
seg.set(ValueLayout.JAVA_LONG, 0, 123456789L);
// Read it back
long val = seg.get(ValueLayout.JAVA_LONG, 0);
System.out.println(val); // 123456789
// Write adjacent ints (at offsets 0 and 4)
seg.set(ValueLayout.JAVA_INT, 0, 100);
seg.set(ValueLayout.JAVA_INT, 4, 200);
System.out.println(seg.get(ValueLayout.JAVA_INT, 0)); // 100
System.out.println(seg.get(ValueLayout.JAVA_INT, 4)); // 200
}
MemoryLayout: Describing C Structures
MemoryLayout describes the layout of a C struct or array in memory:
// C struct: struct Point { int x; int y; }
MemoryLayout pointLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")
);
// Allocate and access
try (MemorySession session = MemorySession.openConfined()) {
MemorySegment point = MemorySegment.allocateNative(pointLayout, session);
// Access via VarHandle derived from layout
VarHandle xHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("x"));
VarHandle yHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("y"));
xHandle.set(point, 3);
yHandle.set(point, 4);
System.out.println(xHandle.get(point)); // 3
System.out.println(yHandle.get(point)); // 4
}
Calling Native Functions with Linker
Linker provides access to native libraries. SymbolLookup finds symbols (function addresses) by name.
Calling strlen
import jdk.incubator.foreign.*;
// Find the C standard library
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = Linker.nativeLinker().defaultLookup();
// Find strlen: size_t strlen(const char *s)
MethodHandle strlen = linker.downcallHandle(
stdlib.lookup("strlen").get(),
FunctionDescriptor.of(
ValueLayout.JAVA_LONG, // return type: size_t (long)
ValueLayout.ADDRESS // parameter: const char*
)
);
// Call strlen with a Java string
try (MemorySession session = MemorySession.openConfined()) {
// Allocate a null-terminated C string
MemorySegment cString = session.allocateUtf8String("Hello, Panama!");
long length = (long) strlen.invoke(cString);
System.out.println(length); // 14
}
Calling abs
MethodHandle abs = linker.downcallHandle(
stdlib.lookup("abs").get(),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)
);
int result = (int) abs.invoke(-42);
System.out.println(result); // 42
How the API Evolved After Java 17
The Java 17 incubator API has the same concepts as the final API but with different class names:
| Concept | Java 17 (incubator) | Java 19–21 (preview) | Java 22 (final) |
|---|---|---|---|
| Memory region | MemorySegment | MemorySegment | MemorySegment |
| Lifetime control | MemorySession | Arena | Arena |
| Layout | MemoryLayout | MemoryLayout | MemoryLayout |
| Native caller | Linker.downcallHandle | Linker.downcallHandle | Linker.downcallHandle |
| Symbol lookup | SymbolLookup | SymbolLookup | SymbolLookup |
| Module | jdk.incubator.foreign | java.lang.foreign | java.lang.foreign |
--enable-preview | Not needed (incubator flag) | Required | Not needed |
The key change is MemorySession → Arena. The rest of the API is conceptually identical.
Should You Use This in Java 17?
Reasons to Use the Incubator API
- You have a specific need to call native code without JNI today
- You are evaluating Project Panama and want early experience
- You are building a proof-of-concept for native interop
Reasons to Wait
- The API changed significantly between Java 17 and Java 22. Code written against the Java 17 incubator will not compile against Java 22’s final API without changes.
- The primary class names (
MemorySession→Arena) and module (jdk.incubator.foreign→java.lang.foreign) changed. - For production use, wait for Java 21 (preview, very close to final) or Java 22 (final).
If you are evaluating the technology, experiment in Java 17 to understand the concepts. Port to the final Java 22 API when ready for production.
JNI vs FFM API: Side-by-Side
// JNI approach: requires a .h file and .c file
// Hello.h (generated by javah)
JNIEXPORT jstring JNICALL Java_Hello_greeting(JNIEnv *, jclass, jstring);
// Hello.c
JNIEXPORT jstring JNICALL Java_Hello_greeting(JNIEnv *env, jclass cls, jstring name) {
const char *nativeName = (*env)->GetStringUTFChars(env, name, 0);
// ...
(*env)->ReleaseStringUTFChars(env, name, nativeName);
return result;
}
// FFM API approach: pure Java, no C code needed
MethodHandle greet = linker.downcallHandle(
myLib.lookup("greeting").get(),
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)
);
The FFM API eliminates the C header file, the native compilation step, and the JNI binding boilerplate.
Summary
| Concept | Java 17 API |
|---|---|
| Off-heap memory | MemorySegment.allocateNative(size, session) |
| Memory lifetime | MemorySession.openConfined() / openShared() |
| Read/write | segment.get(ValueLayout.JAVA_INT, offset) |
| C struct layout | MemoryLayout.structLayout(...) |
| Native library | Linker.nativeLinker().defaultLookup() |
| Call C function | linker.downcallHandle(symbol, FunctionDescriptor) |
| C string | session.allocateUtf8String("text") |
| Module flag | --add-modules jdk.incubator.foreign |
What’s Next
Article 13: Migrating to Java 17 — From Java 8 and Java 11 covers the complete, practical step-by-step process for upgrading an existing Java 8 or Java 11 codebase to Java 17.