j4rs v0.12.0: Java to Rust direction

I would like to share a new direction that my j4rs project took after its 0.12.0 release.

Until v0.12.0, j4rs provided to Rust applications the tools to achieve calls to the Java world. This included setting up and manage JVMs, instantiating Java Classes, making calls to Java methods, providing the means for Java to Rust callbacks etc.

The project was following solely a Rust-first approach, giving the Rust applications the ability to take advantage of the huge amount of libraries and tools existing in the Java ecosystem.

This Rust->Java only approach changed in v0.12.0. j4rs can now be used as well in Java projects that want to achieve JNI calls to Rust libraries.

j4rs achieves this with the use of procedural macros.

I have created a Github repository with some examples, but I will try to summarize the most important points here.

Overall, what we want to achieve is to have a Java application that can call Rust functions.

Rust world

We need to create a cdylib Cargo project, in order to have a shared library as output. This output will be loaded and used by the Java code. We also need to define and use the j4rs_derive dependency in the Cargo.toml, since the Rust functions that can be accessible from Java should be annotated with the call_from_java attribute, provided by it.

Having said that, the Cargo.toml should contain the following:

[lib]
crate-type = ["cdylib"]

[dependencies]
j4rs = "0.12"
j4rs_derive = "0.1"

The source code for a java-callable function is simply:

use j4rs::prelude::*;
use j4rs_derive::*;

#[call_from_java("io.github.astonbitecode.j4rs.example.RustSimpleFunctionCall.fnnoargs")]
fn my_function_with_no_args() {
    println!("Hello from the Rust world!");
}

The call_from_java attribute contains the name of the native method as it is defined in Java, along with the package and the Class itself.

The code above, will accept calls from a native method called fnnoargs, defined in the Java class io.github.astonbitecode.j4rs.example.RustSimpleFunctionCall.

Java World

Here we have the actual application that will run and call the Rust code defined above. The following class contains the definition of the native function that maps to the my_function_with_no_args in the Rust code defined above.

package io.github.astonbitecode.j4rs.example;

public class RustSimpleFunctionCall {
    private static native void fnnoargs();

    static {
        System.loadLibrary("rustlib");
    }

    public void doCallNoArgs() {
        fnnoargs();
    }

}

The thing to take attention here, is to load the dynamic library that is created by compiling the Rust code. This is done with the call to loadLibrary, but the actual location of the library should be defined in the java.library.path. For example, you could define it with a VM option:

-Djava.library.path=/home/myuser/my_rust_lib/target/debug

After that, the only thing remaining for a demonstration, is to define a main method that will call the native one. E.g.:

public class Main {
    public static void main(String[] args) {
        System.out.println("Welcome. This is a simple example that demonstrates calls to rust functions using j4rs.\n");

        var rustFnCalls = new RustSimpleFunctionCall();
        rustFnCalls.doCallNoArgs();

        System.out.println("\nBye!");
    }
}

Executing the above should produce the following output:

Welcome. This is a simple example that demonstrates calls to rust functions using j4rs.

Hello from the Rust world!

Bye!

Conventions

j4rs implies some conventions for the arguments and the return types of the native functions.

You may find here some examples of Java code that demonstrate these conventions and here some examples of Rust code.

Native arguments

In the Rust world, the functions that are accessible from Java must be annotated with the call_from_java attribute. They can have any number of arguments, but their type must be j4rs::Instance.

As always, the transformation of Instances to rust values, can be achieved with a call to Jvm.to_rust:

let s: String = jvm.to_rust(string_instance)?;
let s: i32 = jvm.to_rust(integer_instance)?;

In the Java world, the native methods can have any number of arguments again and they must be of type Instance<T>. A Instance object can be created for any Java Object, using the Java2RustUtils.createInstance method:

var integerNativeInvocation = Java2RustUtils.createInstance(1);
var stringNativeInvocation = Java2RustUtils.createInstance("My String");
var anObjectNativeInvocation = Java2RustUtils.createInstance(new AnObject());

In other words the j4rs::Instance type in Rust, maps to the org.astonbitecode.j4rs.api.Instance type in Java.

Return types

In the Rust world, the return type of functions can be either void, or a Result of Instance. The Instances can be created from an InvocationArg, like following:

// Creates an arg of java.lang.String
let ia1 = InvocationArg::try_from("a str")?;
// Creates an Instance for the ia1 InvocationArg
let i1 = Instance::try_from(ia1)?

// Creates an arg of java.lang.Long
let ia2 = InvocationArg::try_from(1_i64)?;
// Creates an Instance for the ia2 InvocationArg
let i2 = Instance::try_from(ia2)?

and be returned to the Java world.

In the Java world, the return type can be either void or Instance<T>.

In the case that the Rust function returns an Err, an InvocationException will be thrown in the Java world.

Again, the j4rs::Instance type in Rust, maps to the org.astonbitecode.j4rs.api.Instance type in Java.

Type-safety thoughts

Even though generics are involved for Instances generation on the Java side, the type-safety during the transition from Java types to Rust types is not enforced during the compilation.

Nothing stops an Instance that is created in the Java code for an Integer to be attempted in the Rust code to get transformed into something else, like for example a String. This will fail during runtime instead of compile time and unfortunately, I am not aware of something that can be done about it…


I hope you find the j4rs crate interesting and useful.

Thanks for reading!

rust  java  j4rs