Java FFM - Foreign Function & Memory Access API (Project Panama)
Sharing my initial learnings using Java FFM, Foreign Function & Memory Access. Java’s Foreign Function & Memory (FFM) API enables safe, high-performance interaction with native code and memory, improving interoperability with other languages and access to low-level system resources. Entire source code available on GitHub. The Foreign Function & Memory (FFM) API in Java, finalized in JDK 22 with JEP 454, provides a robust and safe mechanism for Java code to interact with foreign functions (native code) and foreign memory (memory outside the Java heap). The FFM API is designed to be a safer, more efficient, and more user-friendly alternative to the Java Native Interface (JNI). JNI is known for its complexity and potential for introducing errors, whereas FFM aims to provide a more "Java-first" programming model for native interoperability. The FFM API introduces several key concepts and components that facilitate foreign function and memory access in Java: Memory Segments: A memory segment is a contiguous block of memory that can be accessed by Java programs. The FFM API provides a way to create and manage memory segments, allowing developers to allocate and deallocate memory as needed. Arena Allocation: The FFM API introduces the concept of arenas, which are memory pools that can be used for efficient allocation and deallocation of memory. Arenas help reduce fragmentation and improve performance when working with native memory. Value Layouts: Value layouts define the memory layout of data structures in a platform-independent way. The FFM API allows developers to create value layouts for both Java and foreign types, ensuring compatibility between the two. Memory Access: The FFM API provides a set of APIs for reading and writing data to memory segments, making it easy to manipulate native data from Java. Foreign Functions: Foreign functions are native functions written in languages like C or C++ that can be called from Java. The FFM API provides a way to define and invoke foreign functions, enabling seamless integration between Java and native code. Note: While the primary use case of the Java Foreign Function & Memory (FFM) API is to call C functions from Java (downcalls), it also supports the reverse: calling Java functions from C (upcalls). This is particularly useful for scenarios like C callbacks, where a C library needs to invoke a function provided by Java code. By leveraging these components, developers can build high-performance applications that take advantage of native libraries and system resources while maintaining the safety and ease of use that Java provides especially in below use cases: Let’s see a small example of effective usage of FFM. As a Proof of Concept, we will create a TCP server using io_uring (Linux kernel interface for asynchronous I/O operations) and Java FFM API. The actual TCP server using io_uring is implemented in C. We will use various APIs (standard TCP operations) viz. accept, listen, connect, send, receive etc. in Java through Foreign Functions mapping the actual C functions. This is by no means a production ready code. The goal is to showcase the ease of use and integration of FFM with existing C libraries. We also won’t be going into details of TCP server implementation in C nor what is io_uring as it’s beyond the scope of this article. Let’s look at the C code first especially the below APIs: Now let’s look at the Java side which is the point of interest of this article. We have 2 classes: Note: The actual TCP server is implemented in C using io_uring. The C library exposes lifecycle methods for managing the server. These methods are then called in Java using FFM. This FfmReceiver class demonstrates a complete FFM workflow for calling native C functions from Java. Here’s the high-level technical breakdown: Library Loading & Symbol Resolution FFM loads the compiled C shared library containing io_uring functions. SymbolLookup provides a bridge to find C function symbols by name. Key FFM Components in Action Memory Management Native memory is allocated outside the Java heap. Data is directly read/written to native buffers without copying (zero copy). Arena ensures safe & automatic cleanup when the try-with-resources block exits. Here, arena is used both to load the library and allocate buffers for receiving data. Here, libraryLookup loads our custom shared library (.so) and lets us find symbols like io_uring_global_init, io_uring_listen, etc. Then with linker, we can create MethodHandles that behave like normal Java methods but actually invoke C functions. We can think of a MethodHandle as a Java wrapper around a native C function. A linker can be thought of as a translator between Java’s JVM world and C’s native/binary code. Next, we define the function signature with FunctionDescriptor.of(Java_INT, Java_INT) which includes parameter(input) types & all return types. In this case, the function says it takes one int argument & returns one int as the result. invokeExact(queueDepth) → actually executes the C function. Safety & Type Checking invokeExact() enforces exact type matching between Java and C function signatures. Compile-time validation prevents type mismatches that would cause runtime errors. No manual memory management or pointer arithmetic required. This approach eliminates JNI’s complexity while providing direct, high-performance access to native libraries with Java’s safety guarantees intact. Click here to see a more detailed AI generated walk through of FfmReceiver implementation. Sample Output: Sample Output: This FfmSender class demonstrates sending data to the TCP server using FFM. The flow is similar to FfmReceiver. We send “ping” text to the TCP Server > FfmReceiver. Explanation In this demo, we’re calling native io_uring functions in C from Java, to build a high-performance TCP receiver. 1. Arena is a memory allocator with automatic cleanup. Think of it as a “scope for native memory”: allocate native buffers inside it, and when the Variants: Here we use 2. 👉 Without 3. 4. 5. Every native function we call has 3 steps: → “returns int, takes an int argument” 👉 A It hides the messy details of JNI — no boilerplate, no unsafe casts. 6. So when we say: We mean: 7. Putting It All Together In the demo flow: 8. Why This Matters Traditionally, Java needed JNI for native calls — painful, verbose, unsafe. With FFM, we now have: This demo shows: pure Java controlling a high-performance Linux feature (io_uring) — something unimaginable/complex with old JNI boilerplate.TL;DR
Overview
FFM Use cases
Demo Example
TCP server in C using async io-uring
io_uring_global_init - Initializes the global io_uring context and sets up the underlying ring buffers for asynchronous I/O operationsio_uring_listen - Creates a TCP socket, binds it to a specified port, and starts listening for incoming connectionsio_uring_accept - Accepts an incoming connection request and returns a new socket file descriptor for the clientio_uring_connect - Establishes an outbound TCP connection to a remote server at the specified address and portio_uring_send - Sends data through an established TCP connection using io_uring’s asynchronous write operationsio_uring_receive - Receives data from an established TCP connection using io_uring’s asynchronous read operationsJava - FFM Integration
FfmReceiver - Responsible for receiving data from the TCP serverFfmSender - Responsible for sending data to the TCP serverFfmReceiver
1 import java.lang.foreign.Arena;
2 import java.lang.foreign.FunctionDescriptor;
3 import java.lang.foreign.Linker;
4 import java.lang.foreign.MemorySegment;
5 import java.lang.foreign.SymbolLookup;
6 import java.lang.foreign.ValueLayout;
7 import java.lang.invoke.MethodHandle;
8 import java.nio.charset.StandardCharsets;
9
10 public class FfmReceiver {
11
12 private static final ValueLayout.OfByte BYTE = ValueLayout.Java_BYTE;
13
14 public static void main(String[] args) throws Throwable {
15 int queueDepth = 32;
16 int port = 22345;
17 int backlog = 128;
18 long bufferSize = 8 * 1024 * 1024; // 8 MB buffer per client
19
20 try (Arena arena = Arena.ofShared()) {
21
22 // Load the shared library
23 SymbolLookup lib = SymbolLookup.libraryLookup("./libiouring_tcp.so", arena);
24 Linker linker = Linker.nativeLinker();
25
26 // 1️⃣ Global Init
27 MemorySegment globalInitAddr = lib.find("io_uring_global_init").get();
28 MethodHandle mhGlobalInit = linker.downcallHandle(globalInitAddr,
29 FunctionDescriptor.of(ValueLayout.Java_INT, ValueLayout.Java_INT));
30 int ret = (int) mhGlobalInit.invokeExact(queueDepth);
31 System.out.println("io_uring_global_init returned: " + ret);
32
33 // 2️⃣ Listen (server socket)
34 MemorySegment listenAddr = lib.find("io_uring_listen").get();
35 MethodHandle mhListen = linker.downcallHandle(listenAddr,
36 FunctionDescriptor.of(ValueLayout.Java_INT, ValueLayout.Java_INT, ValueLayout.Java_INT));
37 int listenFd = (int) mhListen.invokeExact(port, backlog);
38 if (listenFd < 0) {
39 throw new RuntimeException("io_uring_listen failed, fd=" + listenFd);
40 }
41 System.out.println("Server listening on port " + port + ", fd=" + listenFd);
42
43 // 3️⃣ Accept clients in a loop
44 MemorySegment acceptAddr = lib.find("io_uring_accept").get();
45 MethodHandle mhAccept = linker.downcallHandle(acceptAddr,
46 FunctionDescriptor.of(ValueLayout.Java_INT, ValueLayout.Java_INT));
47
48 MemorySegment recvAddr = lib.find("io_uring_recv").get();
49 MethodHandle mhRecv = linker.downcallHandle(recvAddr,
50 FunctionDescriptor.of(ValueLayout.Java_INT, ValueLayout.Java_INT, ValueLayout.ADDRESS, ValueLayout.Java_LONG));
51
52 while (true) {
53 int clientFd = (int) mhAccept.invokeExact(listenFd);
54 if (clientFd < 0) {
55 System.err.println("Accept failed, fd=" + clientFd);
56 continue;
57 }
58 System.out.println("Client connected, fd=" + clientFd);
59
60 // Allocate buffer for this client
61 MemorySegment buffer = arena.allocate(bufferSize);
62
63 int bytesReceived = (int) mhRecv.invokeExact(clientFd, buffer, bufferSize);
64 if (bytesReceived < 0) {
65 System.err.println("io_uring_recv failed, bytes=" + bytesReceived);
66 continue;
67 }
68 System.out.println("Received bytes: " + bytesReceived);
69
70 // Print first 128 bytes for debugging (optional)
71 int displayLen = Math.min(128, bytesReceived);
72 byte[] arr = new byte[displayLen];
73 for (int i = 0; i < displayLen; i++) {
74 arr[i] = buffer.get(BYTE, i);
75 }
76 System.out.println("First " + displayLen + " bytes:\n" +
77 new String(arr, StandardCharsets.US_ASCII));
78
79 // Close client socket
80 MemorySegment closeAddr = lib.find("io_uring_close").get();
81 MethodHandle mhClose = linker.downcallHandle(closeAddr,
82 FunctionDescriptor.ofVoid(ValueLayout.Java_INT));
83 mhClose.invokeExact(clientFd);
84 System.out.println("Client fd " + clientFd + " closed.");
85 }
86
87 // 4️⃣ Optional: shutdown ring (never reached in infinite loop)
88 // MemorySegment shutdownAddr = lib.find("io_uring_global_shutdown").get();
89 // MethodHandle mhShutdown = linker.downcallHandle(shutdownAddr, FunctionDescriptor.ofVoid());
90 // mhShutdown.invokeExact();
91 }
92 }
93 }

FfmSender
1 import java.lang.foreign.*;
2 import java.lang.invoke.MethodHandle;
3 import java.nio.charset.StandardCharsets;
4
5 public class FfmSender {
6 public static void main(String[] args) throws Throwable {
7 int queueDepth = 32;
8 int port = 22345;
9 long bufferSize = 4 * 1024; // 4 KB buffer per client
10
11 System.out.println("FfmSender running...");
12 try (Arena arena = Arena.ofShared()) {
13 String hi = "ping";
14
15 // Load the shared library
16 SymbolLookup lib = SymbolLookup.libraryLookup("./libiouring_tcp.so", arena);
17 Linker linker = Linker.nativeLinker();
18
19 // Global IO URing Init
20 MemorySegment globalInitAddrMS = lib.find("io_uring_global_init").get();
21 FunctionDescriptor globalInitFD = FunctionDescriptor.of(
22 ValueLayout.Java_INT,
23 ValueLayout.Java_INT);
24 MethodHandle globalInitMH = linker.downcallHandle(globalInitAddrMS, globalInitFD);
25 int ret = (int) globalInitMH.invokeExact(queueDepth);
26 System.out.println("io_uring_global_init returned: " + ret);
27
28 // TCP Open Socket FD
29 MemorySegment tcpSocketAddrMS = lib.find("io_uring_connect").get();
30 FunctionDescriptor tcpConnectFD = FunctionDescriptor.of(
31 ValueLayout.Java_INT, // return value of socket File Descriptor
32 ValueLayout.ADDRESS, // server address
33 ValueLayout.Java_INT); // server port
34 MethodHandle tcpConnectMH = linker.downcallHandle(tcpSocketAddrMS, tcpConnectFD);
35
36 String ip = "127.0.0.1"; // destination IP
37 byte[] ipBytes = ip.getBytes(StandardCharsets.UTF_8);
38 MemorySegment ipStr = arena.allocate(ipBytes.length + 1);
39 ipStr.asSlice(0, ipBytes.length).copyFrom(MemorySegment.ofArray(ipBytes));
40 ipStr.set(ValueLayout.Java_BYTE, ipBytes.length, (byte) 0); // Null-terminate for C
41
42 int socketFD = (int) tcpConnectMH.invokeExact(ipStr, port);
43
44 // IO URing send data through memory segment off-heap buffer
45 MemorySegment sendBufAddrMS = lib.find("io_uring_send_all").get();
46 FunctionDescriptor sendBufFD = FunctionDescriptor.of(
47 ValueLayout.Java_INT, // return value: total sent bytes
48 ValueLayout.Java_INT, // socket fd
49 ValueLayout.ADDRESS, // buffer address
50 ValueLayout.Java_LONG); // buffer length
51 MethodHandle sendBufMH = linker.downcallHandle(sendBufAddrMS, sendBufFD);
52
53 // Prepare buffer
54 MemorySegment sendBuf = arena.allocateFrom(hi);
55
56 int totalBytesSent = (int) sendBufMH.invokeExact(socketFD, sendBuf, (long) hi.length());
57
58 System.out.println("Total bytes sent Java FFM: " + totalBytesSent);
59
60 }
61
62 }
63 }

Appendix
Java Versions support
JDK Version API Status Enabling Preview Features Key Enhancements (FFM API versions) Package JDK 14 Foreign-Memory Access API (Incubator) Not applicable (Incubator) Memory segments, Arenas (via Foreign-Memory Access API) jdk.incubator.foreign JDK 16 Incubator Not applicable (Incubator) Calling native functions (via Foreign Linker API) jdk.incubator.foreign JDK 17 Incubator Not applicable (Incubator) Unified FFM API, enhancements to MemorySegment and MemoryAddress abstractions, improved memory layout hierarchy jdk.incubator.foreign JDK 18 Incubator Not applicable (Incubator) Refinements based on feedback jdk.incubator.foreign JDK 19 Preview –enable-preview flag required Refinements based on feedback java.lang.foreign JDK 20 Second Preview –enable-preview flag required Refinements based on feedback, including linker option for heap segments, Enable-Native-Access attribute, programmatic function descriptors java.lang.foreign JDK 21 Third Preview –enable-preview flag required Further refinements based on feedback java.lang.foreign JDK 22+ Finalized Not required (Finalized) API stabilization, minor refinements java.lang.foreign 📝 Walking Through FfmReceiver — A First Taste of Java FFM (AI generated)
Arena — Memory Lifetime Managertry
try block exits, the arena frees them automatically.Arena Type Bounded Lifetime Manual Close (arena.close()) Thread Access Common Use Case Global No No Yes (accessible by any thread) For memory that needs to persist for the entire lifetime of the application. Automatic Yes No (managed by Garbage Collector) Yes (accessible by any thread) Simplest memory management for bounded lifetimes. The memory is deallocated when the arena becomes unreachable by the garbage collector. Confined Yes Yes (mandatory) No (only by the creating thread) Perfect for single-threaded applications. Provides deterministic deallocation of memory when the arena is closed, often used with try-with-resources. Shared Yes Yes (mandatory) Yes (accessible by multiple threads) For concurrent applications where memory segments must be shared between threads. Closing the arena is safe and atomic, even when accessed by multiple threads. ofShared() because multiple native calls may work with the allocated memory.SymbolLookup — Finding Functions in a Shared LibrarySymbolLookup lib ;
SymbolLookup is how Java finds functions or variables inside a native shared library (.so, .dll, .dylib).arena).lib.find("io_uring_listen") to grab the raw address of that function.SymbolLookup, Java wouldn’t know where in memory the C function lives.Linker — Bridging Java ↔ NativeLinker linker ;
int ↔ int32_t)nativeLinker() automatically picks the system ABI (x86_64, ARM64, etc.).MemorySegment — Safe, Structured Native MemoryMemorySegment buffer ;
ByteBuffer (which is limited and unsafe), a MemorySegment tracks bounds, lifetime, and thread access.byte b ;
buffer.;
byte[] for debugging.MethodHandle — Calling Native FunctionsMemorySegment addr ;
FunctionDescriptor:FunctionDescriptor.
MethodHandle:MethodHandle mhGlobalInit ;
int ret ;
MethodHandle is like a strongly-typed function pointer.ValueLayout — Mapping Java ↔ C TypesValueLayout tells the FFM API how Java types map to C types.Java_INT → C int32_tJava_LONG → C int64_tJava_BYTE → C int8_tADDRESS → C pointersFunctionDescriptor.
int ;
mhGlobalInit.;
int listenFd ;
int clientFd ;
int bytesReceived ;
byte[] arr ;
arr[i] ;
mhClose.;