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.