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 the hello_world function we defined in rust.
  • -l rust_panama_helloworld makes it so the generated bindings will load the rust_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 the jdk.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, so System.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)