Calling Native Libraries from Java with the Foreign Function & Memory API
Modern Java applications increasingly need to interact with native libraries. Image processing, machine learning runtimes, compression, cryptography, and media pipelines are all dominated by mature C and C++ codebases. Historically, Java developers had one option: JNI. Powerful, but fragile, verbose, and operationally expensive.
The Foreign Function & Memory API (FFM API), finalized in Java 22 and refined in Java 25, changes that reality. It allows Java code to call native libraries directly, safely, and without writing a single line of C.
This article shows how to integrate ImageMagick into a Quarkus application using the FFM API. The example is deliberately practical: upload an image, process it via native code, return the result. The same pattern applies to many other native libraries.
Why This Matters
Many ecosystems already rely on native code more than developers realize. Python’s data science and ML stack is largely a thin orchestration layer over C, C++, CUDA, and Fortran libraries. NumPy, PyTorch, TensorFlow, and most vector databases all depend on optimized native kernels.
Java has always been capable of doing the same, but JNI raised the barrier too high for many teams. The FFM API lowers that barrier significantly:
- No JNI glue code
- No manual memory lifecycle tracking
- Strong typing and predictable failure modes
- Cleaner builds and simpler operations
For enterprise Java teams, this means native performance without sacrificing maintainability.
Architecture Overview
The FFM API consists of three core building blocks:
- SymbolLookup locates native functions inside loaded libraries
-
Linker converts native symbols into Java
MethodHandles - Arena manages off-heap memory with deterministic cleanup
The flow is straightforward:
- Load the native library
- Look up function symbols
- Bind them using a
FunctionDescriptor - Allocate off-heap memory in an arena
- Invoke native functions
- Copy results back to the Java heap
This example uses ImageMagick’s MagickWand API, which follows a simple object-style model around opaque pointers.
Binding Native Functions
The first step is loading the ImageMagick library and binding a small set of functions.
@ApplicationScoped
public class MagickBinder {
private MethodHandle newMagickWand;
private MethodHandle readImageBlob;
private MethodHandle getImageBlob;
private MethodHandle setFormat;
private MethodHandle relinquishMemory;
@PostConstruct
void init() {
System.load("/opt/homebrew/lib/libMagickWand-7.Q16HDRI.dylib");
Linker linker = Linker.nativeLinker();
SymbolLookup lib = SymbolLookup.loaderLookup();
newMagickWand = linker.downcallHandle(
lib.find("NewMagickWand").orElseThrow(),
FunctionDescriptor.of(ValueLayout.ADDRESS)
);
readImageBlob = linker.downcallHandle(
lib.find("MagickReadImageBlob").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
ValueLayout.ADDRESS,
ValueLayout.JAVA_LONG
)
);
getImageBlob = linker.downcallHandle(
lib.find("MagickGetImageBlob").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.ADDRESS,
ValueLayout.ADDRESS,
ValueLayout.ADDRESS
)
);
setFormat = linker.downcallHandle(
lib.find("MagickSetImageFormat").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
ValueLayout.ADDRESS
)
);
relinquishMemory = linker.downcallHandle(
lib.find("MagickRelinquishMemory").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.ADDRESS,
ValueLayout.ADDRESS
)
);
}
}
This is the only place where native bindings are defined. The rest of the application remains ordinary Java.
Processing an Image Safely
Image processing happens inside a confined Arena. This ensures that all off-heap memory is released deterministically at the end of the request.
@ApplicationScoped
public class PolaroidService {
@Inject
MagickBinder binder;
public byte[] process(byte[] imageData) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment wand =
(MemorySegment) binder.getNewMagickWand().invoke();
MemorySegment blob =
arena.allocateFrom(ValueLayout.JAVA_BYTE, imageData);
int ok = (int) binder.getReadImageBlob().invoke(
wand, blob, (long) imageData.length
);
if (ok == 0) {
throw new IllegalStateException("ImageMagick read failed");
}
binder.getSetFormat().invoke(
wand, arena.allocateFrom("PNG")
);
MemorySegment lengthPtr =
arena.allocate(ValueLayout.JAVA_LONG);
MemorySegment resultPtr =
(MemorySegment) binder.getGetImageBlob()
.invoke(wand, lengthPtr);
long size =
lengthPtr.get(ValueLayout.JAVA_LONG, 0);
byte[] result =
resultPtr.reinterpret(size)
.toArray(ValueLayout.JAVA_BYTE);
binder.getRelinquishMemory().invoke(resultPtr);
return result;
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
}
Key points:
- The arena owns all memory allocated by Java
- ImageMagick-owned memory must still be freed explicitly
- All native data is copied back to the heap before the arena closes
Exposing a REST Endpoint
Quarkus handles the HTTP side as usual. Native integration stays fully encapsulated.
@Path("/polaroid")
public class PolaroidResource {
@Inject
PolaroidService service;
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces("image/png")
public Response create(@RestForm("image") FileUpload upload)
throws IOException {
byte[] input =
Files.readAllBytes(upload.uploadedFile());
byte[] output = service.process(input);
return Response.ok(output)
.type("image/png")
.build();
}
}
From the REST layer’s perspective, this is just another service call.
Verifying the Result
Running the application and posting an image produces a processed PNG generated entirely through native ImageMagick calls.
No JNI code.
No C compiler.
No unsafe memory access.
The JVM remains in full control of lifecycle and error handling.
Production Notes
A few points matter in real systems:
- Avoid hardcoded library paths. Use system properties or environment variables.
- Replace
--enable-native-access=ALL-UNNAMEDwith module-specific access. - Never share native handles across threads unless explicitly documented as safe.
- Always copy native memory back to the heap before closing an arena.
Used correctly, the FFM API is significantly easier to operate than JNI.
Further Reading
This article is part of The Main Thread, a publication focused on modern Java architecture, real-world systems, and production-grade engineering.
Read the full version here:
https://www.the-main-thread.com/p/java-ffm-api-quarkus-imagemagick-tutorialBecause modern Java deserves better content.

Top comments (0)