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
- β Java 25 SDK: https://adoptium.net/
- π¦ Rust : https://rust-lang.org/tools/install/
- β±οΈ 10-15 minutes
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
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]
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 π¦")
}
Build the library:
cargo build --release
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();
}
}
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
Tada π
π¦π Hello from native rust lib π¦
πΎ 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);
}
}
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);
}
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
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
Top comments (0)