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
cbindgento generate a C header file for this library. - use
jextractto 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 classesspecifies the output directory for the generated bindings.-t org.openjdkspecifies the java package the generated classes will be in.--include-function hello_worldmakes it so we just include thehello_worldfunction we defined in rust.-l rust_panama_helloworldmakes it so the generated bindings will load therust_panama_helloworldlibrary automatically.--is a separator used to indicate the start of the list of header files to process.- and finally
lib.his 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.foreignadds thejdk.incubator.foreignmodule to the module graph (incubator modules are not resolved by default).--enable-native-access=ALL-UNNAMEDenables native access for the unnamed module, which is needed for the panama API to work.-Djava.library.path=./target/debugadds the directory with our rust library to the library path, soSystem.loadLibrarycan find it at runtime.-cp classesspecifies the class path with our generated java binding classes (the output of jextract).Main.javaspecifies 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)