An alternative way and the most optimal way to call rust code from java is to use GraalVM. In my opinion, it's the best option if performance is the primary key thing in your case.
GraalVM is a high-performance runtime that provides significant improvements in application performance and efficiency which is ideal for microservices. It is intended for applications created in dynamic languages java and Javascript as well as LLVM-based languages like C C++ and Rust. It breaks down the walls between programming languages and makes shared runtime interoperability possible. It can function independently or in conjunction with OpenJDK, Node.js, or Oracle Database.
GraalVM can be used with OpenJDK to accelerate the performance of Java programs using a new just-in-time compilation technology. Java's bytecode is converted to machine code by GraalVM. This arrangement can be advantageous, especially for other JVM-based languages like Scala, as demonstrated by Twitter running GraalVM in production.
The GraalVM compiler offers performance benefits for highly abstracted applications because it can frequently do away with expensive object allocations. Details can be found in this study article.
Lets code:
Assuming we have an example that we want to call a function from java to rust with one/two arguments and get the result back to java, the code will look like this.
1) Approach-->GraalVM
i want first to warn you that GraalVm it's a bit tricky to handle and in order to become familiar with it needs time thus if you want simplicity go to the second choice.
Assuming you have everything set up correctly in your machine we can move on the llvm installation (you can follow this guide here).
The LLVM toolchain can be added to GraalVM on demand with the GraalVM Updater tool
$GRAALVM_HOME/bin/gu install llvm-toolchain
The above command will install the LLVM toolchain from the GitHub catalog for GraalVM Community users.
export LLVM_TOOLCHAIN=$($JAVA_HOME/bin/lli --print-toolchain-path)
Rust
Let’s look at rustpart.rs, it is a standard Rust function that takes a number finds its cube root and returns it. But we do have to specify #[no_mangle] annotation and we cannot use any crates as well apparently. Simples functions with primitive args/output seem to work but more complex functions do not work when embedded:
#[no_mangle]
fn cube_root(arg: f64) -> f64 {
arg.cbrt()
}
fn main(){}
We compile the Rust source to binary code using rustc compiler with the --emit=llvm-bc flag:
rustc --emit=llvm-bc rustpart.rs
it's important to notice that we will use the .bc generated file from java and not the .rs file
Java
Now let's move to the Java code simply add this dependency in your pom.xml
<dependency>
<groupId>org.graalvm.sdk</groupId>
<artifactId>graal-sdk</artifactId>
<version>22.2.0</version>
</dependency>
And the java code will look like this:
package io.example;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import java.io.File;
import java.io.IOException;
public class App3 {
public static void main(String[] args) throws IOException {
File file=new File("generated.bc");
Context context = Context.newBuilder().allowAllAccess(true).build();
Source source = Source.newBuilder("llvm", file).build();
context.eval(source);
Value ruspart= context.getBindings("llvm").getMember("cube_root");
Double cubeRoot = ruspart.execute(10).asDouble();
System.out.println(cubeRoot);
}
}
Et Voila!!!!
2) Approach-->j4rs
From another point of view if simplicity and flexibility is the primary case you can use as alternative `j4rs`. I will dive into deep details.
Rust
The rust lib.rs:
use std::convert::TryFrom;
use std::result::Result;
use j4rs::InvocationArg;
use j4rs::prelude::*;
use j4rs_derive::*;
use serde::Deserialize;
#[call_from_java("io.example.RustFunctionCalls.addintegers")]
pub extern fn add_integers(integer_instance1: Instance, integer_instance2: Instance) -> Result<Instance, String> {
let jvm: Jvm = Jvm::attach_thread().unwrap();
let i1: i32 = jvm.to_rust(integer_instance1).unwrap();
let i2: i32 = jvm.to_rust(integer_instance2).unwrap();
let sum = i1 + i2;
let ia = InvocationArg::try_from(sum).map_err(|error| format!("{}", error)).unwrap();
Instance::try_from(ia).map_err(|error| format!("{}", error))
}
The cargo file:
[package]
name = "rustlib"
version = "0.1.0"
edition = "2018"
[[bin]]
name = "lib"
path = "src/main.rs"
[build]
rustflags = ["-C", "target-cpu=native"]
[lib]
name = "rustlib"
path = "src/lib.rs"
crate-type = ["cdylib"]
[dependencies]
j4rs = "0.12"
j4rs_derive = "0.1"
serde = { version = "1.0", features = ["derive"] }
We execute the following command to produce the.dll library if we are on a windows system:
cargo build --lib
After that, we can observe the .dll file created under target/debug path
Java
We include the following dependency in the pom.xml
<dependency>
<groupId>io.github.astonbitecode</groupId>
<artifactId>j4rs</artifactId>
<version>0.12.0</version>
</dependency>
The java code will look like this here are the RustFunctionCalls
package io.example;
import org.astonbitecode.j4rs.api.Instance;
import org.astonbitecode.j4rs.api.java2rust.Java2RustUtils;
public class RustFunctionCalls {
private static native Instance addintegers(Instance<Integer> i1, Instance<Integer> i2);
public Integer addInRust(Integer i1, Integer i2) {
Instance instance = addintegers(
Java2RustUtils.createInstance(i1),
Java2RustUtils.createInstance(i2));
return Java2RustUtils.getObjectCasted(instance);
}
}
And here is the main that we call:
package io.example;
import java.io.File;
public class App3 {
public static void main(String[] args) {
File f = new File(App3.class.getResource("/rustlib.dll").getFile());
System.load(f.getAbsolutePath());
RustFunctionCalls rustFnCalls = new RustFunctionCalls();
Integer result=rustFnCalls.addInRust(1,2);
System.out.println(result);
}
}
With this simple example, you can make calls from java to rust
*const Anyis a regular pointer: it is not. It is a pointer to a trait object, which means it is twice the size of a regular pointer. You should not use*const Anyto represent an "arbitrary pointer"; for that, you should either use*const ()or*const c_void(from thelibcpackage). As an aside: you can use::std::mem::size_of<T>()to get the size of a given type.Anyis a trait, and any pointer to a bare trait is a trait object, that is, it is in fact a "fat" pointer. For example, this program prints 16 instead of 8. Hence it is unsuitable for usage as avoid*-like pointer. You need e.g.*mut ()for it.