Foojay Today

Java Panama Polyglot (Rust) Part 4

May 26, 2022

Hello and welcome to the final article of the Java Panama Polyglot series, where we are presenting quick tutorials or recipes on how to access native libraries written in other languages.

In this article you will be using Project Panama to talk to the Rust language.

If you are new to Java’s Foreign Function Access APIs (Project Panama) check out Panama4Newbies.

In Part 3 you had a chance to learn about how to use Java Project Panama's abilities to access the locally installed Python interpreter.

This capability has the advantage of not only talking to Python but also to any of the installed add-on modules (libraries) such as tensorflow, numpy or pyplot.

As far as native compiled languages go, Rust is the new kid on the block.

Rust is often compared to the Go language where source code can be compiled to a specific operating system.

Unlike C/C++, the source code doesn't contain compile time directives and #defines that handle special cases for different architectures.

By exposing native Rust functions, you can be easily accessed using Project Panama's Foreign Function Access APIs.

Today, let's look at how to access a native library built using the Rust language. This tutorial was derived from Jorn Vernee's article "Calling a rust library with the Panama FFI".

For the impatient the example code is at github.com/carldea/panama-polyglot/rust.

Problem

As a Java developer, you want to invoke my Rust based library function rust_get_pid() that will return an integer value representing an application's process id.

Solution

Below are the high-level steps to access a native Rust library and function.

Step 1: Create a Rust library with a function exported following the C ABI.

Step 2: Generate a lib.h header file .h (extension) for the jextract tool to generate binding code.

Step 3: Generate Java binding code (classes) using jextract

Step 4: Create a Main.java to call the rust_get_pid() generated function

Requirements

Before we can take the steps above let's make sure we install the JDK 19 EA release of OpenJDK containing Panama and Rust correctly.

To install Rust run the following for (MacOS/Linux):

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Assuming you've installed Rust you'll want to add its binaries on the PATH as followings:

source $HOME/.cargo/env

Step 1: Creating a Native Rust Library

Let's initialize the Rust project with the following commands:

(For the demo I created a directory rust. If you are already in a directory named rust you don't need to create it.)

mkdir rust
cd rust
cargo init --lib

Edit and replace the contents of the file src/lib.rs with the following:

use std::process;

#[no_mangle]
pub extern "C" fn rust_get_pid() -> u32 {
  return process::id();
}
Line numberDescription
1Use Rust's standard library module process
3A directive to not mangle (alter) the symbol's function name
4To export a Rust function prefix it with pub extern "C". Here, you'll notice the return type is u32 or an unsigned integer (32 bits).
5Return the process id to the caller
A Rust library exported adheres to the C ABI

After you've edited and saved the lib.rs file let's edit the Cargo.toml file by replacing it with the following contents:

Updating build project file Cargo.toml

The following contents of the Cargo.toml file will compile and build a native Rust library created in the target/debug directory.

[build-dependencies]
cbindgen = "0.20.0"

[lib]
crate_type = ["cdylib"]

[package]
name = "myrustlibrary"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Above you'll notice cbindgen is used to generate a C header (.h) file for jextract to later generate binding code in Java. Also, you want to make sure the name of the library will be named myrustlibrary.

On a MacOS system the library created will reside in the following directory: target/debug/libmyrustlibrary.dylib

On a Linux system the library will be named libmyrustlibrary.so

Step 2: Create a Rust C header generator (generates a lib.h file)

Create a file called build.rs with the following contents:

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");
}

Run the following statement that will call cbindgen to generate a lib.h file.

cargo build

The output should look like the following:

Executing cargo build

This may take awhile. When this is done you can view the contents of lib.h as shown below:

#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

uint32_t rust_get_pid(void);

Step 3: Use jextract against C header file (lib.h)

Run the following using jextract to generate binding code that will be used in the Main.java program created later.

jextract -d classes \
   -t org.rust \
   -l myrustlibrary \
   -- lib.h

Step 4: Creating a Main.java to call the native function

Edit or create a Main.java file with the following contents:

import static org.rust.lib_h.*;

public class Main {
    public static void main(String[] args) {
        System.out.println("Rust getting process id = " + rust_get_pid());
    }
}

Above you'll notice the static import will reference generated Panama binding code. The bindings will do a library lookup and a native symbol lookup of the function rust_get_pid() function based on the C ABI.

Running Main.java

To run the Java program you'll simply do the following:

java --add-modules jdk.incubator.foreign \
   --enable-native-access=ALL-UNNAMED \
   -Djava.library.path=./target/debug \
   -cp classes \
   Main.java

The output will look like the following:

WARNING: Using incubator modules: jdk.incubator.foreign
warning: using incubating module(s): jdk.incubator.foreign
1 warning
Rust getting process id = 36396

If you've gotten this far you deserve a high five! Way to go!

Conclusion

You've now had a chance to build and create a Rust based native library using cargo.

Another notable tool or module was the cbindgen capable of creating a C header file that will be used with the jextract tool.

Once Panama code is generated the code can simply call the Rust function rust_get_pid() to return the application's process id.

Adding more native languages to your skillset is really easy as long as you expose (export) native functions that follow the C ABI.

By doing so will allow your code to easily do symbol lookups.

Thank you for exploring Project Panama with me and I hope to share new and exciting use cases in the future.

As always, feel free to comment or provide feedback so I can improve the articles. The idea is to update the articles as the APIs settle and make it into the GA (final) release.

Topics:

Related Articles

View All

Author(s)

  • Carl Dea

    Carl Dea is a Senior Developer Advocate at Azul. He has authored Java books and has been developing software for 20+ years with many clients, from Fortune 500 companies to ... Learn more

Comments (0)

Your email address will not be published.

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard