This is a cache of https://developer.ibm.com/articles/j-ffm/. It is a snapshot of the page as it appeared on 2025-11-15T03:30:20.322+0000.
From JNI to FFM: The future of Java‑native interoperability - IBM Developer

Article

From JNI to FFM: The future of Java‑native interoperability

Explore why developers are migrating to the Java Foreign Function and Memory (FFM) interface, its key benefits, and step‑by‑step guidance for integrating native code in modern Java projects

By

Ben Evans

The Java environment and language are safe and efficient for application development. However, some applications need to perform tasks that go beyond what can be done from within a pure-Java program, such as:

  • Integrating with existing legacy code to avoid rewriting it in Java.
  • Implementing functionality that is not available in Java class libraries.
  • Integrating with code that's best written in C/C++ or languages such as Rust to exploit performance or other environment-specific system characteristics.

While the Java Native Interface (JNI) has been around a long time, JNI is increasingly seen as a product of its time, and might not be what is required for more modern projects. In particular, the following aspects are sometimes regarded as major problems in using JNI:

  • JNI requires numerous ceremony and extra artifacts.
  • The API only interoperates well with libraries that are written in C and C++.
  • Nothing automatic is done to map the Java type system to the C type system.

The extra artifacts issue is reasonably well understood by developers. In addition to the Java API of native methods, JNI requires a C header (.h) file that is derived from the Java API and a C implementation file, which calls into the native library. Some other aspects are less well known, such as the fact that a native method cannot be used to invoke a function written in a language that uses a different calling convention than the one the JVM was built against.

With the arrival of Java's Foreign Function and Memory Interface (FFM), a modern take on this capability has arrived. FFM made its debut in Java 22, which means that the brand-new Java 25 is the first long-term support (LTS) release to support FFM as a final feature, although it was developed gradually as a part of OpenJDK's Project Panama.

Overview of FFM's design

Let's start our discussion of FFM by putting the design of JNI in context so we can contrast it with FFM.

In the article, Best practices for using the Java Native Interface, we introduced the distinction between the fully managed execution environment provided by the JVM and the foreign or native capabilities that require access to the underlying operating system beyond the control of the JVM.

A JNI call works by providing a point where execution control transfers out of the JVM and into native code. In addition to any required parameters, one or two extra parameters are passed:

  • A JNIEnv * that represents the JNI environment
  • A jobject representing the this object (for instance methods)

The intention is that this is a demarcation point, where the work to be done is carried out in native code. So, a JNI call represents an encapsulation of native operations and a handoff to them.

One specific area where we can see the implications of this design is the action of the JVM's garbage collection (GC) subsystem. For the JVM to perform memory recovery (that is, garbage collection) the Java heap must be in a self-consistent state and not modified during the critical phases of GC.

To enable this, Java's model of GC explicitly defines safepoints, which are points of execution where individual threads are in a safe state for GC to occur. When it is time to perform a collection, a global flag is raised and all user threads must reach a safepoint before the GC operation can begin. The standard safepoint for Java code is just before each bytecode in a method executes (that is, "the bottom of the bytecode interpreter loop").

For our purposes, whenever a thread is executing JNI code, it is automatically in a safepoint. This design leads to a simple model, a conceptual separation of JVM and native execution with JNI. Java code operates on the Java heap, and JNI code concerns itself with native memory that is not a part of the Java heap.

This setup has the sometimes-surprising side effect that user threads can continue to execute native code even while a stop-the-world garbage collection runs. This implementation is only possible because the Java heap is not being modified and GC does not touch any memory located outside of the Java heap.

If a Java program needs to retain a long-lived handle to a native data structure or resource after the initial native operation has concluded, then this can be done by passing back an opaque ID (often a pointer that is represented as an int) that can be stored or wrapped into a Java domain object. This handle can then be passed back to native code for subsequent operations. This provides a lifecycle whereby native resources are created and managed by native code and must be explicitly released to be freed.

FFM callbacks as a sequence diagram

The new FFM API aims to provide a much more tightly integrated approach than JNI. For example, FFM allows Java methods to manipulate blocks of memory located outside of the Java heap directly from Java without crossing into native code.

More broadly, FFM is an attempt to bring ease of use improvements for native code by allowing direct support in Java for the following capabilities:

  • Foreign memory allocation
  • Manipulation of structured foreign memory
  • Lifecycle management of foreign resources
  • Calling foreign functions

The API lives in the java.lang.foreign package in the java.base module. It builds upon the Method and Var Handles APIs that are the preferred way to handle advanced capabilities in modern Java, but has a distinctly different design philosophy to JNI, as we will see.

Off-heap memory with FFM

The first piece of the FFM API that we'll discuss is the foreign memory component. The aim is that FFM foreign memory should provide a better alternative to the use of the existing alternatives:

  • JNI
  • ByteBuffer
  • Unsafe

Specifically, the FFM API avoids the limitations of ByteBuffer, such as being limited to segments 2 GB (which existed to match Java's primitive arrays).

At the same time, FFM is much safer than the use of the internal Unsafe API, which allows basically unrestricted memory access, making it very easy for bugs to crash the JVM. In fact, the Unsafe API (which has always been an unsupported API that Java programmers should not rely upon directly) has been the subject of recent work in the platform, which was aimed at removing the need for it. This is being achieved by providing alternative, fully supported, APIs as part of the "Integrity by Default" project, which FFM can be viewed as part of.

Memory Segments

The basis of the foreign memory API relies on types like Arena, MemorySegment, and SegmentAllocator, which all provide access to allocation and handling of off-heap memory.

Memory segments are abstractions that can be used to model contiguous memory regions, which are located either on-heap (also known ask heap segments) or off-heap (also known as native segments). Memory segments provide strong guarantees that make memory dereference operation safe.

In general, the expectation is that almost all memory segments will be native segments, with heap segments being used only in special circumstances. If your use case does require heap segments, then they can be created by using one of the MemorySegment::ofArray factory methods.

The key to accessing native segments is the new Arena interface, which provides five different possibilities for obtaining memory segments, each of which represents a different set of choices and guarantees that are appropriate for different circumstances:

  • Global. The global arena is a native arena where segments are never deallocated until the process exits. This is useful for long-lived segments that are used throughout the lifetime of the application. Attempting to close() the global arena will throw an UnsupportedOperationException.
  • Auto. The auto arena provides native segments that are still managed by the garbage collector, so they do not need explicit deallocation either. Instead, once the segment object goes out of scope, then the garbage collector will at some later time, collect the off-heap memory as though it was a heap object. Segments from an auto arena can be used in any thread; that is, they are not subject to thread confinement.
  • Confined. The confined arena is a per-thread private option for allocation of native segments.
  • Shared. To share native segments between threads, we can use a shared arena.
  • Custom. The fact that Arena is an interface means that if none of the implementations that the JDK ships with suffice, then we can implement our own. For example, we could have a slicing or bump allocator built as a decorator on top of a confined arena.

Arena objects are AutoCloseable, so they can be constructed in a try-with-resources block (except for the global arena) to provide automated cleanup of native segments. When the arena is closed, all segments that are allocated from it are also closed, and their memory is freed.

Java gives the programmer the flexibility to choose a different approach to closing and cleaning up if required. However, as usual, it is good design practice to stop and consider whether the use of a lexically scoped arena is appropriate before you implement anything more elaborate.

Simple use of FFM memory segments

Let's meet a first example of how to use arenas, which does the following:

  1. Creates a native arena that is lexically scoped (by using the standard Java idiom of a try-with-resources block).
  2. Creates a MemorySegment that represents a C string (that is, is null-terminated) and has the same chars as a Java string.
  3. Turn a subset of the C string back into a new Java string.
  4. Print out the new Java string.

    try (var offHeap = Arena.ofConfined()) {
         MemorySegment cStr = offHeap.allocateFrom("hello from Java 25!");
    
         String subStr = cStr.getString(1);
         System.out.println(subStr);
     }

This code, predictably, prints out hello from Java 25!, but we do need to be a little bit careful. For example, if we instead pass in the string 常磐 (which means "eternal" or "unchanging" in Japanese) to allocateFrom() then we will print out a mangled string, because the argument to getString() is in bytes, so the substring is not a complete set of characters.

We've met the simple example of an anonymous native memory segment allocated from an arena by Java code. To do more, we will need to refer to named resources present in a native library. This will require us to load a native library and look up the resources we need, such as global variables and functions.

Loading and linking native libraries

In our next example, let's load a shared lib and get access to one of its global variables and use it from Java. For simplicity, we're going to reuse the jni-examples code that we used in the JNI article. The only slight modification that we've made is to move the magic constant into a global variable called hash_base, like this:

extern unsigned int hash_base = 5381;

Let's have a quick look at the symbols in the library (this is on a Mac, but Linux .so files would look similar):

$ nm -p build/lib/libjni-examples.dylib 
0000000000008018 d __dyld_private
00000000000005a0 T _Java_com_ibm_examples_jni_JNIExamples_improveRNGSeed0
00000000000006e0 T _callback
00000000000009ac T _getElement
0000000000000a18 T _getElement2
000000000000059c T _hash2
0000000000008010 D _hash_base
0000000000000570 T _hash_string
0000000000000ddc T _lostGlobalRef
0000000000000cbc T _modifyArrayWithRelease
0000000000000c64 T _modifyArrayWithoutRelease
0000000000000b6c T _noexceptions
00000000000007c0 T _sumValues2
0000000000000bd0 T _withexceptions
0000000000000a50 T _workOnArray
0000000000000acc T _workOnArray2
0000000000000d4c T _workOnPrimitiveArray
                 U _processBufferHelper
                 U _strlen
                 U dyld_stub_binder

Sure enough, we can see all the functions that we defined for the JNI examples, and the constant we're looking for, so let's use FFM to get at it:

private static final String LIB_NAME = System.mapLibraryName("jni-examples");
    private static final String PATH_TO_LIB = System.getProperty("java.library.path") +"/"+ LIB_NAME;
    private static final SymbolLookup libraryLookup = SymbolLookup.libraryLookup(PATH_TO_LIB, Arena.global());

    public static int getHashBase() {
        try (Arena offHeap = Arena.ofConfined()) {
            SymbolLookup libJniExamples = SymbolLookup.libraryLookup(PATH_TO_LIB, offHeap);
            MemorySegment msHashBase = libJniExamples.findOrThrow("hash_base").reinterpret(4);
            return msHashBase.get(ValueLayout.JAVA_INT,0);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

Note the use of a confined arena here. This implies single threaded use of the library that is lexically scoped to the block. Another alternative would be to use the global arena, loading the library at startup and caching the lookup object in a static or instance field. We'll see this approach presently.

Two immediately obvious differences from JNI are:

  • No System.loadLibrary() call is required.
  • System-dependent library. We need to use System.mapLibraryName() to resolve to a real filesystem path.

Apart from these differences, we proceed by creating a SymbolLookup object, and then use the lookup to obtain a MemorySegment, which is zero-sized (that is, a pointer). The call to reinterpret() turns this base pointer into a segment of 4 bytes (that is, an integer), which we read back into a Java int by using the call to get(). We need to specify the memory layout of the segment so we know how to read it back into Java. Some characteristics of the layout constants are platform-dependent, e.g. byte order.

Foreign function calls with FFM

Just as we did for accessing the global variables in a native library, we can interact with native code through FFM by loading a native library (for example, a .so or .dll or .dylib file). This is essentially an archive of native functions, and user code is then able to look up the functions to be called by using a SymbolLookup, and then link them by using the Linker::downcallHandle method.

Let's rewrite the improveRNGSeed() Java method that we met in the JNI article to use FFM instead.

static {
    PATH_TO_LIB = System.getProperty("java.library.path") +"/"+ LIB_NAME;
    libraryLookup = SymbolLookup.libraryLookup(PATH_TO_LIB, Arena.global());
}

public static int improveRNGSeed(String initialSeed) {
    if (initialSeed.length() > 1024) {
        initialSeed = initialSeed.substring(0, 1023);
    }
    if (initialSeed == null) {
        initialSeed = "";
    }

    MemorySegment hsHash = libraryLookup.findOrThrow("hash_string");
    FunctionDescriptor funcDef = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS);
    Linker linker = Linker.nativeLinker();
    MethodHandle hHashString = linker.downcallHandle(hsHash, funcDef);

    MemorySegment cString = Arena.global().allocateFrom(initialSeed);
    try {
        return (int)hHashString.invoke(cString);
    } catch (Throwable e) {
        throw new RuntimeException(e);
    }
}

We're storing the SymbolLookup object in a variable called libraryLookup, and it's in the global arena, as we might need it throughout the life of the program. As before, we use it to locate a pointer (again represented as a zero-length MemorySegment), but this time it's to the C function hash_string(). We need a FunctionDescriptor of it, and as it's declared as unsigned int hash_string(const char* str), then the appropriate descriptor is "Takes a pointer (to const char) and returns int". We pass the function pointer and descriptor to the linker, and receive a method handle to the native method.

The Method Handles API, despite arriving in Java almost 15 years ago, is still not as widely known among Java developers as the much older Reflection API. This is somewhat curious, as method handles provide a more cleaner, more complete, and more JVM-friendly approach to indirect, runtime invocation, for example when method names are not known at compile time.

For this case, the advantage of using method handles is that they can refer directly to methods implemented in native libraries. There is no need to have a Java method with the native modifier and then a glue method implemented in C.

In our example, we lookup the hash_string() function directly and then invoke it without any additional ceremony (apart from copying the contents of our Java string into a C-style string allocated in the native arena). Another capability that FFM provides is the ability to call back into Java from native code.

Another capability that FFM provides is the ability to call back into Java from native code. Let's see it in action:

public static int printObj(MemorySegment s) {
        System.out.println(s);
        System.out.println(s.reinterpret(Integer.MAX_VALUE).getString(0));
        return 42;
    }

    public static void upcallExample(String param) {
        // First get a native method handle for the C function we will call from Java
        var linker = Linker.nativeLinker();
        // The last parameter in the function descriptor is the callback
        var funcDefWithCallback = FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS);
        var callbackFfm = libraryLookup.findOrThrow("callback_ffm");
        MethodHandle mhCallback = linker.downcallHandle(callbackFfm, funcDefWithCallback);

        // Now get a Java method handle for the Java method we'll call back to
        var lookup = MethodHandles.lookup();
        MethodHandle mh = null;
        try {
            mh = lookup.findStatic(lookup.lookupClass(), "printObj", MethodType.methodType(int.class, MemorySegment.class));
        } catch (NoSuchMethodException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        // Convert the callback MH to an upcall stub
        var funcDef = FunctionDescriptor.of(JAVA_INT, ADDRESS);
        MemorySegment pPrintObj = linker.upcallStub(mh, funcDef, Arena.global());

        MemorySegment cString = Arena.global().allocateFrom(param);
        try {
            var res = mhCallback.invoke(cString, pPrintObj);
            System.out.println(res);
        } catch (Throwable e) {
            e.printStackTrace(System.out);
            throw new RuntimeException(e);
        }
    }

As we can see, to perform a callback, Java client code needs to create function pointers for Java functions by using Linker::upcallStub. The Java callback method signature must expect a MemorySegment, and it's up to the Java code to convert it as needed back to whatever Java data structures they actually want.

In our example, printObj() receives a pointer (represented as a zero-sized MemorySegment) that should contain a C string. We don't know how large the string is, because C strings are null-terminated, so we have to scan a block of memory to locate the terminating null.

Java strings are a maximum of 2G in size, so we reinterpret the segment as being of size Integer.MAX_VALUE and then use getString(0) to start scanning from offset zero. The reinterpret call does not change the address, or perform any copying; it just changes the bounds of the segment, so it is a very cheap operation.

In the event of a malformed string being passed back into Java, then the getString() call will potentially have to read a full 2G of memory to fail to find a null byte. However, on modern hardware, this is not too expensive and is preferable under most circumstances to having a hard-coded "magic limit" that is lower.

Java 25, additional warnings and Integrity by Default

One aspect of the new FFM API that you might have noticed is that using it seems to produce warnings when certain methods are called. For example:

WARNING: A restricted method in java.lang.foreign.SymbolLookup has been called
WARNING: java.lang.foreign.SymbolLookup::libraryLookup has been called by com.ibm.examples.ffm.FFMExampleMain in an unnamed module (file:/Users/ben/projects/writing/IBM_Developer/jni-ffm/build/classes/)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

This is part of the "Integrity by Default" strategy that is being rolled out in OpenJDK. The aim is to quantify and surface those parts of the Java platform that are capable of invalidating properties such as encapsulation, which are the building blocks of some of the most important program properties, such as correctness and security, as well as maintainability, scalability and potentially performance.

With this perimeter established, the aim is to gradually introduce an opt-in mechanism for these useful, but potentially hazardous capabilities. For FFM, this means that methods such as libraryLookup(), downcallHandle(), upcallStub() and reinterpret(), which are potentially unsafe, are in scope for ultimately needing an explicit opt-in by the application author.

For Java 25, this takes the form of a warning on first use, which can be got rid of with a runtime JVM switch. These restrictions also apply to JNI code as well from Java 25 onwards, for example:

WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::loadLibrary has been called by com.ibm.examples.jni.JNIExamples in an unnamed module (file:/Users/ben/projects/writing/IBM_Developer/jni-ffm/build/classes/)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

In fact, this is just the latest in a series of modifications to the platform that are designed to remove sources of unexpected errors and ensure that Java applications do not unexpectedly violate the "Principle of Least Surprise".

Let's look at an example of one of these modifications. In Java 8 and before, you can call public methods on any public class you like, both directly and reflectively, but after the Java module system arrived these calls became subject to additional restrictions.

This is a fundamental change in the way Java access control works, but if you're a Java developer who plays by the rules, you might never have called code in an internal package directly, so you might not even have noticed when this change occurred.

The direct-calling restriction is in effect for every Java version after Java 8, and as of Java 17, the default permission for reflective access to the JDK internals has also changed to deny (from previously issuing a warning). This is explained in detail by JEP 396.

Another loophole that the Integrity project aims to close is the ability to reflectively modify final fields, under the title of "Prepare to Make Final Mean Final". This is not only important for integrity, but it should also enable potentially significant performance optimizations, via enhanced constant folding.

The Thread.stop() method (deprecated since Java 1.1) has also been modified so that it now unconditionally throws UnsupportedOperationException, which means that the ability for one thread to arbitrarily stop another has been removed. Similarly, the methods that library authors have long exploited in sun.misc.Unsafe are gradually being replaced by safe alternatives and access is being removed from users.

Lastly, the integrity project also intends to eventually make the use of Java agents opt-in as well.

Automatic linking

As our final topic, we'll discuss the topic of automatic linking, where rather than using a SymbolLookup to ask for handles to specific native functions, we use code generation to automatically create Java methods that mirror the contents of the library.

The main approach that we will consider is the jextract tool, which aims to automate many of the steps that are involved with the translation between down and upcalls, so that a client can immediately start using the aspects of native libraries they are interested in.

Unfortunately, the tool is not bundled with the JDK, so we have to download a build of it separately, from jdk.java.net. Once you have set up jextract, test it with jextract -h. You should see output like this:

Usage: jextract <options> <header file> [<header file>] [...]

Option                             Description
------                             -----------                                                  
-?, -h, --help                     print help                                                   
-D --define-macro <macro>=<value>  define <macro> to <value> (or 1 if <value> omitted)          
-I, --include-dir <dir>            add directory to the end of the list of include search paths
--dump-includes <file>             dump included symbols into specified file                    
--header-class-name <name>         name of the generated header class. If this option is not specified, then header class name is derived from the header file name. For example, class "foo_h" for header "foo.h".   
--include-function <name>          name of function to include                                  
--include-constant <name>          name of macro or enum constant to include                    
--include-struct <name>            name of struct definition to include                         
--include-typedef <name>           name of type definition to include                           
--include-union <name>             name of union definition to include                          
--include-var <name>               name of global variable to include                           
-l, --library <libspec>            specify a shared library that should be loaded by the generated header class. If <libspec> starts with :, then what follows is interpreted as a library path. Otherwise, <libspec> denotes a library name. Examples:
-l GL                                                    
-l :libGL.so.1                                           
-l :/usr/lib/libGL.so.1                                  
--use-system-load-library          libraries specified using -l are loaded in the loader symbol lookup (using either System::loadLibrary, or System::load). Useful if the libraries must be loaded from one of the paths  in java.library.path.                                     
--output <path>                    specify the directory to place generated files. If this option is not specified, then current directory is used.    
-t, --target-package <package>     target package name for the generated classes. If this option is not specified, then unnamed package is used.             
--symbols-class-name <name>        override the name of the root header class                   
--version                          print version information and exit

macOS platform options for running jextract (available only when running on macOS):             
-F <dir>            specify the framework directory                                     
--framework <framework>                     specify framework library. --framework libGL is equivalent to -l :/System/Library/Frameworks/libGL.framework/libGL

To use jextract, we need to give it a header file, so let's take the declarations from our existing examples and put them into a header file:

unsigned int hash_base;

unsigned int hash_string(const char* str);

int callback_ffm(const char * str, int (*printObj)(const void *));

Then, run jextract like this:

jextract --output src/main/java/ -t com.ibm.examples.auto --header-class-name FFMAuto --library :build/lib/libjni-examples.dylib src/main/C/jni_examples.h

This command will create a new package com.ibm.examples.auto with a class called FFMAuto as the entry point, and a couple of additional auxiliary classes.

We can drive this auto-generated code with a main class like this:

public class FFAutoMain {

    public static void main(String[] args) {
        System.out.println("=== FFM Auto Examples Demo ===");
        System.out.println();

        int hashBase = FFMAuto.hash_base();
        System.out.printf("Hash Base -> %d%n", hashBase);
        System.out.println();

        // Test cases with different seed strings
        String[] testSeeds = {
                "hello",
                "world",
                "java",
                "native",
                "interface",
                "random-seed-string",
                "this-is-a-longer-seed-value-for-testing",
                "",
                "123456789"
        };

        System.out.println("Testing hash_string function:");
        System.out.println("Input String -> Improved Seed");
        System.out.println("-----------------------------");

        for (String seed : testSeeds) {
            try {
                MemorySegment cString = Arena.ofAuto().allocateFrom(seed);
                int improvedSeed = FFMAuto.hash_string(cString);
                System.out.printf("'%s' -> %d%n", seed, improvedSeed);
                // Do upcalls next
                MemorySegment upcall = callback_ffm$printObj.allocate(s -> FFMExampleMain.printObj(s), Arena.ofAuto());
                FFMAuto.callback_ffm(cString, upcall);
            } catch (Exception e) {
                System.err.printf("Error processing seed '%s': %s%n", seed, e.getMessage());
            }
        }
        System.out.println();

        System.out.println("=== Demo Complete ===");
    }
}

Each of the native symbols that we included in the header file has been used to generate a private static inner class in the main header class (FFMAuto).

Memory (such as the hash_base constant in our example) is handled by a pair of static methods (getter and setter) that are named the same as the native symbol. We access the native methods by using public static Java methods, which are named the same as the native symbol.

Finally, upcalls are handled by using a functional interface to represent the Java callback, which we pass to the alllocate() method in the auxiliary class. This returns a MemorySegment which represents the function pointer corresponding to the upcall. This object is then passed to the Java method corresponding to the callback-expecting native method as another argument.

The generated code helps to simplify what is happening under the hood with FFM, but it can quickly become complicated and overwhelming for non-trivial libraries. In general, we recommend using jextract --dump-includes to get a complete list of symbols, expressed as one --include-* option per line, and then create a smaller file of only those symbols that you actually want.

This filtered list of symbols can then be used in the @argfile syntax common to Java command-line tools.

In our example, these commands will generate Java bindings to the constant hash_base and the hash_string() function, but not the callback:

/opt/jextract/bin/jextract --dump-includes includes.txt src/main/C/jni_examples.h
grep hash includes.txt > filtered.txt
/opt/jextract/bin/jextract --output src/main/java/ -t com.ibm.examples.auto --header-class-name FFMAuto --library :build/lib/libjni-examples.dylib @filtered.txt src/main/C/jni_examples.h

When you test this example, don't forget to delete the old auto-generated code before you rerun the jextract step.

To support other, non-C languages, C++ should use the extern "C" mechanism to declare a C interface that jextract can traget, and Rust libraries should use cbindgen. Rust will probably declare a lot of additional symbols that Java programmers don't need access to, so the use of the dump-includes method in the previous command might be even more important when you deal with Rust code.

Summary

In this article, we've introduced the new FFM API and how it represents a major step forward for the Java platform, by rethinking the way that Java can interoperate with native code. While there is no need to immediately reimplement existing, working JNI code, new projects should consider using FFM as the preferred way to interoperate with native libraries.