DEV Community

Cover image for Java + Rust with FFM
Florian Bernard
Florian Bernard

Posted on

Java + Rust with FFM

The Java Foreign Function and Memory API simplifies the secure execution of native code and memory outside the JVM's management. This article will demonstrate how to effortlessly invoke a Rust native function and facilitate memory sharing between Java and Rust.

Requirements

FFM API

The Java Foreign Function & Memory API (FFM API), introduced as part of Project Panama, is a modern Java API designed to enable seamless interoperability between Java and native code. It allows Java applications to efficiently call native functions (written in languages like C, C++, or Rust) and safely access native memory, without the overhead and complexity of the traditional Java Native Interface (JNI).

This API comes with two main components:

  • Foreign Function Interface (FFI) API: This allows Java code to invoke native functions directly, using a more intuitive and type-safe approach than JNI
  • Memory Access API: This provides controlled access to off-heap memory, enabling Java programs to interact with native memory layouts (e.g., structs, unions, and pointers) in a structured and safe manner.

πŸ‘‹πŸ¦€ Hello Rust

The FFM Api rely on the native linker to dynamically load and bind native library ensuring that function calls are resolved efficiently.
To do so we will need to build a dynamic library in rust.

Let's create a new rust project:

cargo init --lib
Enter fullscreen mode Exit fullscreen mode

Configure the project to make a dynamic library (Cargo.toml):

[package]
name = "rust_lib"
version = "0.1.0"
edition = "2024"

[lib]
name = "rust_lib"
crate-type = ["cdylib"]

[dependencies]
Enter fullscreen mode Exit fullscreen mode

Then we need to write a C-compatible native function in order to be call with the Java FFM api (src/lib.rs):

#[unsafe(no_mangle)] //preserve the function name
pub extern "C" fn hello_rust(){
    println!("πŸ¦€πŸ‘‹ Hello from native rust lib πŸ¦€")
}
Enter fullscreen mode Exit fullscreen mode

Build the library:

cargo build --release
Enter fullscreen mode Exit fullscreen mode

The library is built in the ./target/release folder with a different extension according to the operating system (Linux, MacOS, Windows) librust_lib.so|dylib|dll

β˜• Java App

Let's create a basic main to call ou native Rust function:

package fr.flob.ffm;

import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.SymbolLookup;
import java.lang.invoke.MethodHandle;

public class App {

    static void main() throws Throwable {
        // Load rust dynamic library
        System.loadLibrary("rust_lib");
        var linker = Linker.nativeLinker();
        var lookup = SymbolLookup.loaderLookup();
        var helloFunction = lookup.find("hello_rust").orElseThrow();

        // Function does not take any arguments and returns nothing
        FunctionDescriptor helloDescriptor = FunctionDescriptor.ofVoid();

        // Create the method handle to call the rust function
        MethodHandle helloHandle = linker.downcallHandle(
                helloFunction,
                helloDescriptor
        );

        // Invoke the native function
        helloHandle.invoke();
    }

}
Enter fullscreen mode Exit fullscreen mode

Once compiled with your favorite tool (maven, Gradle, Ant, javac ....) run the App main method:

java -Djava.library.path=<RUST_PROJECT_PATH>/target/release \
--enable-native-access=ALL-UNNAMED \
-cp <CLASS_PATH> \
fr.flob.ffm.App
Enter fullscreen mode Exit fullscreen mode

Tada πŸŽ‰

πŸ¦€πŸ‘‹ Hello from native rust lib πŸ¦€
Enter fullscreen mode Exit fullscreen mode

πŸ’Ύ Shared memory

Now we know how to call our native Rust library, we can leverage on the Memory Access API to share some data between Java and Rust

Let's add a rust method that takes a pointer to a memory address and the number of elements:

#[unsafe(no_mangle)]
pub extern "C" fn read_data_rust(java_ptr: *const i32, size: i32){
    if java_ptr.is_null() || size <= 0 {
        return;
    }
    let slice = unsafe{std::slice::from_raw_parts(java_ptr, size as usize)};
    println!("πŸ¦€[rust] data received from Java πŸ”Ž πŸ“¦");
    for (i, &value) in slice.iter().enumerate() {
        println!("\t[{}] = {}", i, value);
    }
}
Enter fullscreen mode Exit fullscreen mode

Update the Java main method to prepare Memory and call this new native function:

static void main() throws Throwable {
        // Load rust dynamic library
        System.loadLibrary("rust_lib");
        var linker = Linker.nativeLinker();
        var lookup = SymbolLookup.loaderLookup();

        var dataSize = 10;

        // Get rust function address
        var rustDataFunction = lookup.find("read_data_rust").orElseThrow();
        // Function takes two arguments and returns nothing
        FunctionDescriptor rustDataDescriptor = FunctionDescriptor.ofVoid(
                ValueLayout.ADDRESS,  // pointer to shared memory
                ValueLayout.JAVA_INT // size of elements
        );

        // Create the method handle to call the rust function
        MethodHandle rustDataHandle = linker.downcallHandle(
                rustDataFunction,
                rustDataDescriptor
        );

        // Allocate native memory that both Java and Rust can access
        try (Arena arena = Arena.ofConfined()) {
            // Allocate memory X integers
            MemorySegment sharedMemory = arena.allocate(ValueLayout.JAVA_INT, dataSize);
            // Fill the memory from Java
            for (int i = 0; i < dataSize; i++) {
                sharedMemory.setAtIndex(ValueLayout.JAVA_INT, i, i * 10);
            }
            // Pass the memory address to Rust code
            rustDataHandle.invoke(sharedMemory, dataSize);
        }
Enter fullscreen mode Exit fullscreen mode

Compile both Rust and Java project and run Java app again

Tada (again) πŸŽ‰

πŸ¦€[rust] data received from Java πŸ”Ž πŸ“¦
    [0] = 0
    [1] = 10
    [2] = 20
    [3] = 30
    [4] = 40
    [5] = 50
    [6] = 60
    [7] = 70
    [8] = 80
    [9] = 90
Enter fullscreen mode Exit fullscreen mode

Conclusion

Java FFM Api combined with Rust provided a really easy way to call native function and shared memory efficiently without any complex over head like with JNI (its predecessor).

Full source code and more examples are available on GitHub: https://github.com/fb64/java-ffm-rust

Resources

Top comments (0)