Interfacing Java with native code has traditionally been… well, a bit painful. For years, the Java Native Interface (JNI) was the go-to, but it often meant verbose boilerplate, complex memory management, and compiling glue code in C or C++.
Project Panama changes the game by introducing a modern, type-safe, and far simpler Foreign Function & Memory (FFM) API, making it easier to call native libraries directly from Java—without wrestling with JNI.
In this article, we’ll explore practical foreign-function interface examples using Project Panama’s FFM API.
1. What Is Project Panama?
Project Panama is a long-running OpenJDK project aiming to:
- Simplify access to native libraries.
- Provide safer memory access and layout handling.
- Reduce the need for manual JNI coding.
The FFM API became standard in Java 22 (after being incubated since Java 14), giving developers a direct way to:
- Call native functions.
- Allocate native memory.
- Map native data structures.
2. The Basics: FFM API Components
| Component | Purpose |
|---|---|
Linker | Links Java code to native functions. |
SymbolLookup | Finds native function symbols in libraries. |
MemorySegment | Represents off-heap/native memory. |
MemoryLayout | Describes memory structure layouts. |
FunctionDescriptor | Describes the signature of a native function. |
3. Example 1 – Calling C’s strlen from Java
C function signature:
size_t strlen(const char *s);
Java FFM API code:
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
public class PanamaStrlenExample {
public static void main(String[] args) throws Throwable {
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MethodHandle strlen = linker.downcallHandle(
stdlib.find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment cString = arena.allocateUtf8String("Hello, Panama!");
long length = (long) strlen.invoke(cString);
System.out.println("String length: " + length);
}
}
}
Key points:
- We allocate a UTF-8 string in native memory.
- No manual pointer handling—
MemorySegmentabstracts that. - Method handles map directly to C functions.
4. Example 2 – Using Native getpid to Get Process ID
C function signature:
pid_t getpid(void);
Java code:
import java.lang.foreign.*;
public class PanamaGetPidExample {
public static void main(String[] args) throws Throwable {
Linker linker = Linker.nativeLinker();
SymbolLookup libc = linker.defaultLookup();
var getpid = linker.downcallHandle(
libc.find("getpid").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_INT) // pid_t usually int
);
int pid = (int) getpid.invoke();
System.out.println("Current PID: " + pid);
}
}
5. Example 3 – Calling a Custom Native Function
Imagine you have a C library:
mathlib.c:
double add(double a, double b) {
return a + b;
}
Compile it into a shared library:
gcc -shared -fPIC -o libmathlib.so mathlib.c
Java code:
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
public class PanamaCustomLibExample {
public static void main(String[] args) throws Throwable {
Linker linker = Linker.nativeLinker();
SymbolLookup mathlib = SymbolLookup.libraryLookup("libmathlib.so", Arena.ofAuto());
MethodHandle addFunc = linker.downcallHandle(
mathlib.find("add").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE,
ValueLayout.JAVA_DOUBLE)
);
double result = (double) addFunc.invoke(5.5, 2.3);
System.out.println("Result: " + result);
}
}
6. Example 4 – Working with Native Structs
Let’s say we have a C struct:
struct Point {
double x;
double y;
};
We can map this in Java:
import java.lang.foreign.*;
import java.lang.invoke.VarHandle;
public class PanamaStructExample {
static final GroupLayout POINT_LAYOUT = MemoryLayout.structLayout(
ValueLayout.JAVA_DOUBLE.withName("x"),
ValueLayout.JAVA_DOUBLE.withName("y")
);
public static void main(String[] args) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment point = arena.allocate(POINT_LAYOUT);
VarHandle xHandle = POINT_LAYOUT.varHandle(double.class, MemoryLayout.PathElement.groupElement("x"));
VarHandle yHandle = POINT_LAYOUT.varHandle(double.class, MemoryLayout.PathElement.groupElement("y"));
xHandle.set(point, 10.0);
yHandle.set(point, 20.0);
System.out.println("Point: (" + xHandle.get(point) + ", " + yHandle.get(point) + ")");
}
}
}
7. Why This Beats JNI
| JNI | FFM API |
|---|---|
| Requires C/C++ glue code | No glue code needed |
| Manual memory management | Arena manages lifecycle |
| Verbose boilerplate | Direct linking & descriptors |
| Harder to debug | More type-safe & readable |
8. Best Practices
- Always release native memory by using
Arenain try-with-resources. - Use immutable layouts for consistency and clarity.
- Match native types correctly—misaligned layouts cause subtle bugs.
- Ship precompiled native libraries for cross-platform builds.
9. Final Thoughts
Project Panama’s Foreign Function & Memory API eliminates the friction of native interfacing in Java.
Whether you’re calling system libraries, reusing legacy C code, or integrating high-performance native modules, you now have a modern, safe, and elegant alternative to JNI.
This isn’t just about making native calls easier—it’s about unlocking entire ecosystems of native capabilities without leaving the comfort (and safety) of Java.
Thank you!
We will contact you soon.
Eleftheria DrosopoulouAugust 19th, 2025Last Updated: August 12th, 2025

This site uses Akismet to reduce spam. Learn how your comment data is processed.