Java 21 introduces the Foreign Function and Memory (FFM) API as a stable feature, providing a modern alternative to JNI for native code integration. In this post, we’ll explore how to use the FFM API to integrate with native C libraries for tasks like compression or cryptography.
The Problem: Native Library Integration
Many applications need to interact with native libraries for performance-critical operations, system-level functionality, or leveraging existing C/C++ codebases. Traditional approaches using JNI (Java Native Interface) are:
| Challenge | Impact |
|---|---|
| Complexity | Requires writing C/C++ glue code |
| Build Process | Need native compilers and build tools |
| Safety | Manual memory management, no bounds checking |
| Portability | Platform-specific binaries |
| Debugging | Difficult to trace issues across JNI boundary |
Before: Traditional JNI Approach
Here’s what JNI typically required for calling a simple C function:
// Step 1: Declare native method
public class NativeLib {
static { System.loadLibrary("mylib"); }
public native int strlen(String s);
}
// Step 2: Generate header with javah
// Step 3: Implement in C:
// JNIEXPORT jint JNICALL Java_NativeLib_strlen(JNIEnv *env, jobject obj, jstring s) {
// const char *str = (*env)->GetStringUTFChars(env, s, 0);
// int len = strlen(str);
// (*env)->ReleaseStringUTFChars(env, s, str);
// return len;
// }
// Step 4: Compile with native compiler
// Step 5: Package and distribute native library
Problems: Multiple languages, complex build, manual memory management, platform-specific binaries.
After: FFM API Approach
With the FFM API, the same functionality is pure Java:
Java 21
public class NativeLibFFM {
private static final Linker LINKER = Linker.nativeLinker();
private static final SymbolLookup STDLIB = LINKER.defaultLookup();
public static long strlen(String s) throws Throwable {
// Find the native strlen function
MemorySegment strlenSymbol = STDLIB.find("strlen")
.orElseThrow(() -> new RuntimeException("strlen not found"));
// Define the function signature: size_t strlen(const char*)
FunctionDescriptor descriptor = FunctionDescriptor.of(
ValueLayout.JAVA_LONG, // return type (size_t)
ValueLayout.ADDRESS // parameter (const char*)
);
// Create a method handle for the native function
MethodHandle strlen = LINKER.downcallHandle(strlenSymbol, descriptor);
// Call the native function with an Arena for memory management
try (Arena arena = Arena.ofConfined()) {
MemorySegment nativeString = arena.allocateUtf8String(s);
return (long) strlen.invokeExact(nativeString);
}
}
}
Scala 3
object NativeLibFFM:
private val linker: Linker = Linker.nativeLinker()
private val stdlib: SymbolLookup = linker.defaultLookup()
def strlen(s: String): Long =
val strlenSymbol = stdlib.find("strlen")
.orElseThrow(() => RuntimeException("strlen not found"))
// Function signature: size_t strlen(const char*)
val descriptor = FunctionDescriptor.of(
ValueLayout.JAVA_LONG, // return type
ValueLayout.ADDRESS // parameter
)
val strlenHandle = linker.downcallHandle(strlenSymbol, descriptor)
Using.resource(Arena.ofConfined()) { arena =>
val nativeString = arena.allocateUtf8String(s)
strlenHandle.invokeExact(nativeString).asInstanceOf[Long]
}
Kotlin
object NativeLibFFM {
private val linker: Linker = Linker.nativeLinker()
private val stdlib: SymbolLookup = linker.defaultLookup()
fun strlen(s: String): Long {
val strlenSymbol = stdlib.find("strlen")
.orElseThrow { RuntimeException("strlen not found") }
// Function signature: size_t strlen(const char*)
val descriptor = FunctionDescriptor.of(
ValueLayout.JAVA_LONG,
ValueLayout.ADDRESS
)
val strlenHandle = linker.downcallHandle(strlenSymbol, descriptor)
return Arena.ofConfined().use { arena ->
val nativeString = arena.allocateUtf8String(s)
strlenHandle.invokeExact(nativeString) as Long
}
}
}
Key FFM API Concepts
Arena for Memory Lifecycle Management
An Arena controls the lifecycle of native memory allocations. When an arena is closed, all associated memory is automatically freed.
// Confined arena - single-threaded, deterministic cleanup
try (Arena arena = Arena.ofConfined()) {
MemorySegment segment = arena.allocate(1024);
// Use the segment...
} // Memory automatically freed here
// Arena types:
// - Arena.ofConfined() - single-threaded, must close in same thread
// - Arena.ofShared() - multi-threaded, can close from any thread
// - Arena.ofAuto() - automatically freed by GC
// - Arena.global() - never freed, for permanent allocations
For Scala developers: Think of Arena as similar to ZIO’s Scope or Cats Effect’s Resource - it provides automatic resource cleanup.
MemorySegment for Native Memory Access
MemorySegment represents a contiguous region of native memory with bounds checking:
try (Arena arena = Arena.ofConfined()) {
// Allocate 1024 bytes
MemorySegment segment = arena.allocate(1024);
// Write data with type safety
segment.set(ValueLayout.JAVA_INT, 0, 42);
segment.set(ValueLayout.JAVA_LONG, 4, 123456789L);
// Read data back
int first = segment.get(ValueLayout.JAVA_INT, 0);
long second = segment.get(ValueLayout.JAVA_LONG, 4);
// Bounds checking prevents buffer overflows
// segment.get(ValueLayout.JAVA_LONG, 1020); // Would throw!
}
Linker and FunctionDescriptor for Native Calls
The Linker creates method handles for native functions, while FunctionDescriptor describes function signatures:
// Get the native linker for the current platform
Linker linker = Linker.nativeLinker();
// Define function signature: double sqrt(double)
FunctionDescriptor sqrtDescriptor = FunctionDescriptor.of(
ValueLayout.JAVA_DOUBLE, // return type
ValueLayout.JAVA_DOUBLE // parameter
);
// Create method handle
MethodHandle sqrt = linker.downcallHandle(
linker.defaultLookup().find("sqrt").orElseThrow(),
sqrtDescriptor
);
// Call the native function
double result = (double) sqrt.invokeExact(16.0); // Returns 4.0
SymbolLookup for Finding Native Functions
SymbolLookup locates native functions in loaded libraries:
// Default lookup includes standard C library
SymbolLookup stdlib = Linker.nativeLinker().defaultLookup();
// Find common C functions
MemorySegment strlen = stdlib.find("strlen").orElseThrow();
MemorySegment abs = stdlib.find("abs").orElseThrow();
MemorySegment sqrt = stdlib.find("sqrt").orElseThrow();
// Load a specific library
// SymbolLookup customLib = SymbolLookup.libraryLookup("libcustom.so", Arena.global());
Working with Structured Data
Define C-like structs using MemoryLayout:
Java 21
// Define: struct Point3D { double x; double y; double z; }
MemoryLayout point3DLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_DOUBLE.withName("x"),
ValueLayout.JAVA_DOUBLE.withName("y"),
ValueLayout.JAVA_DOUBLE.withName("z")
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment point = arena.allocate(point3DLayout);
// Get VarHandles for field access
var xHandle = point3DLayout.varHandle(
MemoryLayout.PathElement.groupElement("x"));
var yHandle = point3DLayout.varHandle(
MemoryLayout.PathElement.groupElement("y"));
var zHandle = point3DLayout.varHandle(
MemoryLayout.PathElement.groupElement("z"));
// Set and get field values
xHandle.set(point, 0L, 1.0);
yHandle.set(point, 0L, 2.0);
zHandle.set(point, 0L, 3.0);
double x = (double) xHandle.get(point, 0L); // 1.0
}
Calling Multiple Native Functions
Here’s a complete example calling several C library functions:
public class FFMDemo {
private static final Linker LINKER = Linker.nativeLinker();
private static final SymbolLookup STDLIB = LINKER.defaultLookup();
public static void main(String[] args) throws Throwable {
// abs(int) -> int
var abs = LINKER.downcallHandle(
STDLIB.find("abs").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT)
);
System.out.println("abs(-42) = " + (int) abs.invokeExact(-42));
// sqrt(double) -> double
var sqrt = LINKER.downcallHandle(
STDLIB.find("sqrt").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_DOUBLE, ValueLayout.JAVA_DOUBLE)
);
System.out.println("sqrt(16.0) = " + (double) sqrt.invokeExact(16.0));
// time(time_t*) -> time_t
var time = LINKER.downcallHandle(
STDLIB.find("time").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
long timestamp = (long) time.invokeExact(MemorySegment.NULL);
System.out.println("Current Unix timestamp: " + timestamp);
// strlen with string handling
try (Arena arena = Arena.ofConfined()) {
var strlen = LINKER.downcallHandle(
STDLIB.find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
MemorySegment str = arena.allocateUtf8String("Hello, FFM!");
System.out.println("strlen result: " + (long) strlen.invokeExact(str));
}
}
}
Safety Improvements Over JNI
The FFM API provides significant safety improvements:
| Feature | JNI | FFM API |
|---|---|---|
| Bounds Checking | None | Built-in |
| Memory Management | Manual | Arena-based |
| Type Safety | Weak | Strong |
| Null Safety | Manual checks | Integrated |
| Thread Safety | Manual | Confined arenas |
Example: Bounds Checking
try (Arena arena = Arena.ofConfined()) {
MemorySegment small = arena.allocate(8);
// This is safe
small.set(ValueLayout.JAVA_LONG, 0, 42L);
// This throws IndexOutOfBoundsException - prevented!
// small.set(ValueLayout.JAVA_LONG, 8, 999L);
}
Example: Thread Confinement
try (Arena confined = Arena.ofConfined()) {
MemorySegment segment = confined.allocate(16);
// Safe: same thread
segment.set(ValueLayout.JAVA_INT, 0, 42);
// Would throw WrongThreadException if accessed from another thread
// new Thread(() -> segment.get(ValueLayout.JAVA_INT, 0)).start();
}
FFM API vs JNI Comparison
| Aspect | JNI | FFM API |
|---|---|---|
| Native Code | Requires C/C++ glue code | Pure Java |
| Build Process | javah + C compiler | None needed |
| Memory Safety | Manual, error-prone | Arena-managed |
| Type Safety | Weak | Strong with layouts |
| Bounds Checking | None | Built-in |
| Thread Safety | Manual synchronization | Confined arenas |
| Performance | Excellent | Comparable |
| Debugging | Difficult | Better tooling |
| Learning Curve | Steep | Moderate |
For Scala Developers
The FFM API provides similar benefits to what you get from effect systems:
| Feature | FFM API | ZIO/Cats Effect |
|---|---|---|
| Resource Management | Arena | Scope/Resource |
| Memory Safety | Built-in | Not applicable |
| Error Handling | Exceptions | Effect types |
| Composability | Method handles | Monadic |
Scala’s Using.resource integrates naturally with Arena:
import scala.util.Using
Using.resource(Arena.ofConfined()) { arena =>
val segment = arena.allocate(1024)
// Use segment safely
} // Automatic cleanup
For Kotlin Developers
Kotlin’s use extension works seamlessly with Arena:
Arena.ofConfined().use { arena ->
val segment = arena.allocate(1024)
// Use segment safely
} // Automatic cleanup
Kotlin’s null-safety complements FFM’s safety features.
When to Use FFM API
Use FFM API For:
✅ Calling standard C library functions
✅ Integrating with existing native libraries
✅ Performance-critical native code
✅ System-level operations
✅ Replacing existing JNI code
Consider Alternatives For:
❌ Simple tasks that don’t need native code
❌ When pure Java solutions exist
❌ Cross-platform portability is critical
Migration from JNI
- Identify native calls in your JNI code
- Define FunctionDescriptors for each native function signature
- Replace JNI calls with FFM method handle invocations
- Use Arenas for memory management instead of manual allocation
- Remove native C/C++ glue code
- Simplify build by removing native compilation steps
Conclusion
The FFM API in Java 21 represents a major improvement in native code integration:
- Simpler: Pure Java, no native code required
- Safer: Built-in bounds checking, arena-based memory management
- Cleaner: Method handles instead of JNI functions
- Modern: Designed for contemporary Java development
For applications requiring native library integration, the FFM API provides a much more developer-friendly experience while maintaining the performance characteristics needed for production use.
Code Samples
See the complete implementations in our repository:
This is part of our Java 21 Interview Preparation series. Check out the full preparation plan for more topics.