Cookie Consent by Privacy Policies Generator Vegapit
Export Rust functions to Java using JNI (WebAssembly comparison)
Last updated: 2020-02-04T12:41:52

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_priceand 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.