Part 12 of 14

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.foreign at 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:

  1. Header file generation: javah generates C header files; you maintain two codebases (Java + C)
  2. Manual memory management: Memory errors (use-after-free, double-free, buffer overruns) in C code can crash the JVM
  3. No safety: Type mismatches between Java and C cause undefined behavior — not ClassCastException but process crashes
  4. Slow iteration: A JNI bug requires recompiling the C library, restarting the JVM, and rebuilding
  5. 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:

ConceptJava 17 (incubator)Java 19–21 (preview)Java 22 (final)
Memory regionMemorySegmentMemorySegmentMemorySegment
Lifetime controlMemorySessionArenaArena
LayoutMemoryLayoutMemoryLayoutMemoryLayout
Native callerLinker.downcallHandleLinker.downcallHandleLinker.downcallHandle
Symbol lookupSymbolLookupSymbolLookupSymbolLookup
Modulejdk.incubator.foreignjava.lang.foreignjava.lang.foreign
--enable-previewNot needed (incubator flag)RequiredNot needed

The key change is MemorySessionArena. 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 (MemorySessionArena) and module (jdk.incubator.foreignjava.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

ConceptJava 17 API
Off-heap memoryMemorySegment.allocateNative(size, session)
Memory lifetimeMemorySession.openConfined() / openShared()
Read/writesegment.get(ValueLayout.JAVA_INT, offset)
C struct layoutMemoryLayout.structLayout(...)
Native libraryLinker.nativeLinker().defaultLookup()
Call C functionlinker.downcallHandle(symbol, FunctionDescriptor)
C stringsession.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.