Calling a rust library with the Panama FFI
Steps for setting up a “Hello, World!” example
In this example we will see how to:
- build a simple rust library that exposes a C API (which the Panama FFI can link against).
- use
cbindgen
to generate a C header file for this library. - use
jextract
to generate java bindings from the header file. - create a simple java program that invokes the rust library through the bindings.
Step 1. Setup a project
$ mkdir rust-panama-helloworld
$ cd rust-panama-helloworld
$ cargo init --lib
Step 2. Write a simple rust library
Edit the src/lib.rs
and change the contents to:
#[no_mangle]
pub extern "C" fn hello_world() {
println!("Hello, world!");
}
The #[no_mangle]
attribute is needed to make sure the function will be visible in the library, and extern "C"
is used to make sure the function has the right ABI (the C ABI for the particular platform).
Step 3. Add the needed project config
Go into Cargo.toml
and add the following:
[build-dependencies]
cbindgen = "0.20.0"
[lib]
crate_type = ["cdylib"]
cbindgen
is used to generate a C header file from the rust sources, which will be fed into panama’s jextract
tool to generate java bindings.
Step 4. Create a build script that invokes cbindgen
Create a build.rs
file in the top-level directory and add the following:
extern crate cbindgen;
use std::env;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
cbindgen::Builder::new()
.with_crate(crate_dir)
.with_language(cbindgen::Language::C)
.generate()
.expect("Unable to generate bindings")
.write_to_file("lib.h");
}
This is a simple build script that invokes cbindgen and writes the output to the lib.h
file in the top-level directory.
Step 5. Build the rust library & generate a C header file
$ cargo build
This should create the file (lib)rust_panama_helloworld.(dll/so/dylib)
in the target/debug
folder.
The build should also invoke cbindgen
and generate the file lib.h
in the top-level directory. The contents of this file should look like this:
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
void hello_world(void);
Step 6. Make sure the panama jdk is on the path
$ java --version
openjdk 17-panama 2021-09-14
OpenJDK Runtime Environment (build 17-panama+3-167)
OpenJDK 64-Bit Server VM (build 17-panama+3-167, mixed mode, sharing)
I’m using the latest snapshot available at http://jdk.java.net/panama/ at the time of writing.
Step 7. Generate java bindings using jextract
$ jextract -d classes -t org.openjdk --include-function hello_world -l rust_panama_helloworld -- lib.h
In this command:
-d classes
specifies the output directory for the generated bindings.-t org.openjdk
specifies the java package the generated classes will be in.--include-function hello_world
makes it so we just include thehello_world
function we defined in rust.-l rust_panama_helloworld
makes it so the generated bindings will load therust_panama_helloworld
library automatically.--
is a separator used to indicate the start of the list of header files to process.- and finally
lib.h
is the header file that was generated during step 5.
A classes
directory should be created that contains some files generated by jextract:
./classes
└───org
└───openjdk
constants$0.class
lib_h.class
RuntimeHelper$VarargsInvoker.class
RuntimeHelper.class
To see what jextract is doing in these bindings, the --source
option can also be added to the command above to generate java sources instead of classes, though we need classes for this tutorial (the source files are simply the un-compiled version of the generated class files). The sources could serve as an example of how to use the Panama FFI API directly without using jextract as well, but for this tutorial we’ll keep things simple and use jextract to do the low-level work. See also the State of foreign function support document for a broader overview of the panama foreign function API.
Step 8. Create a java program that calls our library
Create a Main.java
file in the top-level directory and place the following inside:
import static org.openjdk.lib_h.*;
public class Main {
public static void main(String[] args) {
hello_world();
}
}
A simple main class that calls our library function.
Step 9. Run the java program
$ java --add-modules jdk.incubator.foreign --enable-native-access=ALL-UNNAMED -Djava.library.path=./target/debug -cp classes Main.java
In this command:
--add-modules jdk.incubator.foreign
adds thejdk.incubator.foreign
module to the module graph (incubator modules are not resolved by default).--enable-native-access=ALL-UNNAMED
enables native access for the unnamed module, which is needed for the panama API to work.-Djava.library.path=./target/debug
adds the directory with our rust library to the library path, soSystem.loadLibrary
can find it at runtime.-cp classes
specifies the class path with our generated java binding classes (the output of jextract).Main.java
specifies our main class, which will be compiled on the fly, and then run.
Step 10. Observe the ouput
WARNING: Using incubator modules: jdk.incubator.foreign
warning: using incubating module(s): jdk.incubator.foreign
1 warning
Hello, world!
Success! (some warnings are printed because we are using an incubator module)