Java FFM - Foreign Function & Memory Access API (Project Panama)
TL;DR
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.
Overview
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.
FFM Use cases
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:
- Performance-Critical Applications: Applications that require high performance such as game engines, scientific computing, Ultra Low Level (ULL) High Frequency Trading (HFT) systems and real-time systems can benefit from direct access to native code and memory.
- Big Data and Machine Learning: Libraries like TensorFlow and PyTorch often require efficient memory management and native code execution, making FFM a suitable choice for integrating these libraries into Java applications.
- Interoperability with Other Languages: FFM allows Java applications to easily call functions written in other languages, such as C or C++, enabling developers to leverage existing libraries and codebases
- Low-Level System Access: Applications that need to interact with low-level system resources, such as hardware devices or operating system APIs, can use FFM to access these resources directly from Java e.g. accessing NIC for Kernel bypass networking, accessing GPU for parallel processing, etc.
- High Performance Computing (HPC): FFM can be used in HPC applications where performance is critical, such as simulations, numerical computations, and data processing tasks that require efficient memory management and native code execution.
- High Performance File/Data I/O: Database files, large binary files, and other data-intensive applications can benefit by using techniques like memory-mapped files which allow direct access to file contents in memory, random access, bulk read, minimize OS calls, Zero copy & low GC pressure by avoiding Heap, multi-process file coordination with locks improving I/O performance and reducing latency.
- Custom High Performance Low Latency RPC: Remote Procedure Calls (RPC) can be optimized using FFM to reduce serialization/deserialization overhead, enabling faster communication with high throughput between distributed systems.
Demo Example
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.
TCP server in C using async io-uring
Let’s look at the C code first especially the below APIs:
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 operations
Java - FFM Integration
Now let’s look at the Java side which is the point of interest of this article. We have 2 classes:
FfmReceiver
- Responsible for receiving data from the TCP serverFfmSender
- Responsible for sending data to the TCP server
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.
FfmReceiver
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 }
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
- Arena: Manages native memory lifecycle - automatically frees all allocated memory when closed (safe malloc/free).
- SymbolLookup: This is how Java finds function pointers in a native library. Think of SymbolLookup as a “directory of functions” inside the .so file.
- Linker: The bridge between Java and native code, which knows how to convert between Java calling conventions and the platform’s native ABI (x86_64 Linux, ARM64, etc.).
- FunctionDescriptor: Defines the C/native function signature (return type + parameter types)
- ValueLayout: Maps Java types to native types (Java_INT → C int, ADDRESS → C pointer)
- MethodHandle: Type-safe wrapper for C/native function calls
- MemorySegment: Represents native memory buffers for data exchange; essentially a safe pointer with guardrails.
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 & 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 (using function descriptor) 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:
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 }
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.
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 |
Explanation
- Incubator Phase: The initial steps of the FFM API were as incubating features, meaning they were experimental and subject to change. These releases introduced the core concepts like memory access and foreign function invocation. JEP 434 mentions that the FFM API was in its incubator phase in JDK 17 and JDK 18.
- Preview Phase: In the preview phase, the API's design and implementation were complete, but still subject to potential changes based on user feedback. OpenJDK JEP 424 describes the Foreign Function & Memory API as a preview API in JDK 19.
- Finalized: In JDK 22, the FFM API was declared stable and ready for production use, removing the need for preview flags and providing a more robust and reliable native interoperability solution.
- Key Enhancements: This section highlights the significant changes and refinements introduced in each version of the FFM API as it evolved.
📝 Walking Through FfmReceiver — A First Taste of Java FFM (AI generated)
In this demo, we’re calling native io_uring functions in C from Java, to build a high-performance TCP receiver.
1. Arena
— Memory Lifetime Manager
try
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
try
block exits, the arena frees them automatically.Variants:
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. |
Here we use ofShared()
because multiple native calls may work with the allocated memory.
2. SymbolLookup
— Finding Functions in a Shared Library
SymbolLookup lib ;
SymbolLookup
is how Java finds functions or variables inside a native shared library (.so
,.dll
,.dylib
).- You pass the library name and a scope (
arena
). - Later, we’ll do
lib.find("io_uring_listen")
to grab the raw address of that function.
👉 Without SymbolLookup
, Java wouldn’t know where in memory the C function lives.
3. Linker
— Bridging Java ↔ Native
Linker linker ;
- Linker builds a bridge between Java and C functions.
- It knows how to:
- Translate Java values to C values (e.g.,
int
↔int32_t
) - Call into native functions and return values back
- Translate Java values to C values (e.g.,
nativeLinker()
automatically picks the system ABI (x86_64, ARM64, etc.).
4. MemorySegment
— Safe, Structured Native Memory
MemorySegment buffer ;
- MemorySegment is a safe view of native memory.
- Unlike
ByteBuffer
(which is limited and unsafe), aMemorySegment
tracks bounds, lifetime, and thread access. - Example:
byte b ;
buffer.;
- Here, we allocate an 8 MB receive buffer per client.
- Later, we copy received bytes into a Java
byte[]
for debugging.
5. MethodHandle
— Calling Native Functions
Every native function we call has 3 steps:
- Find its address:
MemorySegment addr ;
- Describe its signature using
FunctionDescriptor
:
FunctionDescriptor.
→ “returns int, takes an int argument”
- Bind with a
MethodHandle
:
MethodHandle mhGlobalInit ;
- Call it like a Java method:
int ret ;
👉 A MethodHandle
is like a strongly-typed function pointer.
It hides the messy details of JNI — no boilerplate, no unsafe casts.
6. ValueLayout
— Mapping Java ↔ C Types
ValueLayout
tells the FFM API how Java types map to C types.- Examples:
Java_INT
→ Cint32_t
Java_LONG
→ Cint64_t
Java_BYTE
→ Cint8_t
ADDRESS
→ C pointers
So when we say:
FunctionDescriptor.
We mean:
int ;
7. Putting It All Together
In the demo flow:
- Initialize io_uring
mhGlobalInit.;
- Create a TCP server socket
int listenFd ;
- Accept connections
int clientFd ;
- Receive data into a native buffer
int bytesReceived ;
- Inspect data in Java (first 128 bytes)
byte[] arr ;
arr[i] ;
- Close the socket
mhClose.;
8. Why This Matters
Traditionally, Java needed JNI for native calls — painful, verbose, unsafe.
With FFM, we now have:
- Memory safety (bounds checks, lifetime control)
- Performance (zero-copy native memory access)
- Clarity (call native functions like Java methods)
- Portability (same code runs across OS/CPU with correct ABI handling)
This demo shows: pure Java controlling a high-performance Linux feature (io_uring) — something unimaginable/complex with old JNI boilerplate.