In a previous post, we saw how Rust WebAssembly can be integrated into a JavaFX project using the Asmble tool. Here we look at an integration of the same functionality using the Java Native Interface (JNI). Finally, we compare the two approaches in terms of convenience and performance.
Creating a JNI header file
The process of exporting C/C++ code into Java is nothing new. This very straightforward tutorial from IBM dating back from 2002 shows how the JNI technology was already well developed a decade ago. The process for exporting Rust functions to Java relies on the same technology. The jni-rs crate manages the type conversions which greatly simplifies the process.
The first step will be to define a Java class that will wrap the native code generated by the compiler, and save it in a file called Vegapit.java:
import java.nio.file.Path;
import java.nio.file.Paths;
class Vegapit{
public static native double price(String callput, double F, double K, double T, double V, double R);
static{
Path p = Paths.get("path/to/rust_jni.dylib");
System.load(p.toAbsolutePath().toString());
}
public static void main(String[] args) {
System.out.println("price = " + Vegapit.price("CALL", 100.0, 100.0, 1.0, 0.3, 0.05));
}
}
The signature of Rust function to be imported needs to be declared as public static native
. The static
section is used to declare the path to the library that the Rust compiler will eventually generate. Then we have a main
function to test that our function linkage worked. Now, let’s run the following commands:
javac Vegapit.java
javah Vegapit
The first simply compiles the java file to executable bytecode for the JVM. The second generates a header file Vegapit.h that shows the signature of the JNI-compliant functions the JVM now expects. Here what the header file looks like:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Vegapit */
#ifndef _Included_Vegapit
#define _Included_Vegapit
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Vegapit
* Method: price
* Signature: (Ljava/lang/String;DDDDD)D
*/
JNIEXPORT jdouble JNICALL Java_Vegapit_price
(JNIEnv *, jclass, jstring, jdouble, jdouble, jdouble, jdouble, jdouble);
#ifdef __cplusplus
}
#endif
#endif
The price
function in our Vegapit
class is now called Java_Vegapit_price
and its arguments all have JNI types. The following step is to create the Rust code in order to generate this function exactly.
Compiling JNI-Compliant Rust functions
After creating a new cargo library project, we import this black76.rs file as it contains the code for the pricing function we have been using throughout these post series. In the Cargo.toml file, we set the crate type and the dependencies we will need:
[lib]
crate-type = ["dylib"]
[dependencies]
jni = "0.14"
statrs = "0.11"
Now onto the lib.rs file containing the Java_Vegapit_price
function we are after:
mod black76;
use jni::JNIEnv;
use jni::objects::{JClass, JValue, JString};
use jni::sys::jdouble;
use black76::{CallPut, Black76};
#[no_mangle]
pub extern fn Java_Vegapit_price(env: JNIEnv, _class: JClass, callput: JString, F: jdouble, K: jdouble, T: jdouble, V: jdouble, R: jdouble ) -> jdouble {
let callput_string: String = env.get_string(callput).expect("Couldn't get a Java string!").into();
let cp = match callput_string.as_str() {
"CALL" => CallPut::Call,
"PUT" => CallPut::Put,
_ => panic!("CALL or PUT!")
};
Black76::new(cp, F, K, T, V, R).price()
}
The development team of the jni-rs crate made it very easy for us to find the right types to import by matching the naming conventions of the JNI. Numerical types import seamlessly, while a bit of extra code is required to process the JString
type. All that is left to do now is to build our library with cargo build --release
and put it in the path location we defined in the Vegapit.java
file.
To ensure the linkage is working properly, we run the main
function in the compiled Vegapit
class by invoking java Vegapit
and it returns:
price = 11.342020640681275
Everything works, so how does this approach compare with the WebAssembly solution?
Comparison with the WebAssembly import using Asmble
When it comes to code readability, the JNI solution is more convenient when dealing with String
types. For each String
taken as argument in a Java function, JNI requires one JString
type while Asmble would require a pointer *mut u8
and its size in usize
. This could make the code quite confusing to read especially for functions that take several String
types as argument.
When it came to comparing performance, the test was a loop that runs the pricing function 10,000 times. In both cases, the operation was timed via the time
function on OSX, and here are the results:
JNI implementation
real 0m0.188s
user 0m0.149s
sys 0m0.049s
WebAssembly/Asmble
real 0m0.581s
user 0m1.860s
sys 0m0.168s
It is clear that the loading of the WebAssembly module has a real impact on the performance compared to native. I would assume the overhead cost is only being incurred once at load time, but the numbers clearly tip the boat in one direction. Hopefully, this post has provided you with something useful when tackling your next project.
If you like this post, follow me on Twitter and get notified on the next posts.