How to Use the Foreign Function API in Java 22 to Call C Libraries
Use cases and goals
- FFM (Foreign Function & Memory API) replaces JNI as a safer, simpler way to call non‑Java code.
- Typical needs: calling C libraries, system calls, off‑heap data structures, fast sorting of custom records, graphics/game libraries (SDL2, raylib), database engines like SQLite, and other platforms’ native APIs.
- Some argue native calls are rare in Java partly because JNI was painful; others say Java’s rich pure‑Java ecosystem reduced the need for native code.
FFM vs JNI and other interop tools
- FFM aims to fix JNI’s ergonomics and safety issues while keeping high performance.
- JNI will remain supported; FFM is mainly for new interop code. Both can coexist in one app.
- jextract can generate bindings from C headers, significantly improving usability in newer JDKs.
- Tools like JNA remain relevant; they still handle loading native libs and provide many ready-made bindings.
Comparison with .NET / C#
- Many compare FFM unfavorably to .NET’s P/Invoke: more boilerplate, weaker value/struct support, less “direct” feeling.
- C# offers structs, spans, stack allocation, function pointers, and attributes like DllImport/LibraryImport; FFI declarations are terse.
- Java designers explicitly avoid a P/Invoke-style model to preserve runtime flexibility (moving GC, virtual threads) and avoid locking in ABI decisions.
- Some see C# as adding features too quickly and becoming complex; others view Java as overly conservative and verbose.
Packaging and distribution of native libraries
- Common pattern: ship .so/.dll/.dylib files inside JARs, extract to a temp file at runtime, then load via System.load/Library. This is what many libraries (e.g., SQLite, compression libs) and JNA do.
- This approach has quirks: delete-on-exit issues on Windows, potential unsafety of unloading DLLs, and awkwardness when binaries are large (hundreds of MB) or many platforms must be supported.
- jlink/jmod are recommended for full application images, not ordinary libraries.
- Some tools (e.g., third‑party packagers) automate detection, extraction, signing, and bundling of native libs for each platform.
Performance, memory, and runtime behavior
- FFM downcalls can compile to near-direct calls (argument shuffle + CALL), comparable to C.
- Java still shares the machine stack with C; a virtual thread that calls C code pins an underlying OS thread.
- FFM introduces arenas and controlled native memory lifecycles, with stronger guarantees against undefined behavior, but native memory management remains error-prone.
Binding generation and ecosystem concerns
- Automatic binding generation is hard; SWIG, jnigen, javacpp, and custom scripts exist but often struggle with complex C/C++ (macros, void*, ownership).
- jextract has improved but still may generate messy code; some participants report renewed success with Java 22.
- Library authors worry about slow JDK upgrade adoption: to support older Java, they must maintain JNI and FFM paths in parallel, reducing the immediate payoff of FFM.