Embedding JVM in Rust
Source: Dev.to
In this post
- Embedding a JVM into a Rust application.
- Calling Java code from Rust and Rust code from Java.
The post assumes you’re familiar with both Java and Rust. The complete source code is available in the GitHub repository for this article.
Java side – a tiny library
We’ll use a small, deliberately artificial example to showcase a variety of Java‑Rust interaction features. The library consists of three classes:
package me.ivanyu.java_library;
public class Calculator {
public static void add(int a, int b, ResultCallback callback) {
int sum = a + b;
String message = "Result: " + sum;
Result result = new Result(message);
callback.onResult(result);
}
}
@FunctionalInterface
public interface ResultCallback {
void onResult(Result result);
}
package me.ivanyu.java_library;
public class Result {
private final String message;
public Result(final String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
Calculator.add computes the sum of two integers, builds a message string, wraps it in a Result object, and finally invokes the supplied callback.
Native callback class
To let Rust handle the callback, we declare a native method in a class that implements ResultCallback:
package me.ivanyu.java_library;
public class NativeCallback implements ResultCallback {
@Override
// The `native` keyword tells the JVM that the implementation lives in native code.
public native void onResult(Result result);
}
Build the JAR
./gradlew clean jar
The resulting JAR will be located at java/lib/build/libs/lib.jar.
Rust side – dependencies
Add the jni crate to your Cargo.toml. The invocation feature is required to launch a JVM from Rust.
[dependencies]
jni = { version = "0.21.1", features = ["invocation"] }
Implementing the native method in Rust
// Prevent symbol mangling.
// https://doc.rust-lang.org/rustc/symbol-mangling/index.html
#[unsafe(no_mangle)]
// `extern "system"` ensures the correct calling convention for JNI.
// The name follows the convention described in
// https://docs.oracle.com/en/java/javase/21/docs/specs/jni/design.html#resolving-native-method-names
pub extern "system" fn Java_me_ivanyu_java_1library_NativeCallback_onResult(
mut env: JNIEnv,
_obj: JObject,
result_obj: JObject,
) {
// Call `Result.getMessage()` – signature: ()Ljava/lang/String;
let message_jstring = env
.call_method(result_obj, "getMessage", "()Ljava/lang/String;", &[])
.expect("Failed to call getMessage")
.l()
.expect("getMessage returned null");
// Convert the Java string to a Rust `String`.
let message_jstring = JString::from(message_jstring);
let message: String = env
.get_string(&message_jstring)
.expect("Couldn't get Java string")
.into();
println!("{}", message);
}
Key points
#[unsafe(no_mangle)]disables name mangling so the JVM can locate the function.extern "system"selects the platform‑specific JNI calling convention.- The method name follows the JNI naming scheme (
Java___). - We retrieve the
Stringreturned byResult.getMessage()and print it in Rust.
Launching the JVM from Rust and invoking Java code
use jni::{
objects::{JObject, JString},
InitArgsBuilder, JNIEnv, JavaVM, NativeMethod, JNIVersion,
};
use std::path::Path;
fn main() {
// -------------------------------------------------------------------------
// 1️⃣ Initialise the JVM with the library JAR on the class path.
// -------------------------------------------------------------------------
let jar_path = Path::new("java/lib/build/libs/lib.jar");
let classpath_option = format!("-Djava.class.path={}", jar_path.display());
let jvm_args = InitArgsBuilder::new()
.version(JNIVersion::V8) // Java 8+ bytecode version
.option(&classpath_option) // Add our JAR to the class path
.build()
.expect("Failed to create JVM args");
let jvm = JavaVM::new(jvm_args).expect("Failed to create JVM");
// -------------------------------------------------------------------------
// 2️⃣ Attach the current thread to obtain a JNIEnv.
// -------------------------------------------------------------------------
let mut env = jvm
.attach_current_thread()
.expect("Failed to attach current thread");
// -------------------------------------------------------------------------
// 3️⃣ Register the native method for `NativeCallback`.
// -------------------------------------------------------------------------
let native_callback_class = env
.find_class("me/ivanyu/java_library/NativeCallback")
.expect("Failed to find NativeCallback class");
// Signature: (Lme/ivanyu/java_library/Result;)V
let native_methods = [NativeMethod {
name: jni::strings::JNIString::from("onResult"),
sig: jni::strings::JNIString::from("(Lme/ivanyu/java_library/Result;)V"),
fn_ptr: Java_me_ivanyu_java_1library_NativeCallback_onResult as *mut _,
}];
env.register_native_methods(native_callback_class, &native_methods)
.expect("Failed to register native method");
// -------------------------------------------------------------------------
// 4️⃣ Obtain the `Calculator` class and call its static `add` method.
// -------------------------------------------------------------------------
let calculator_class = env
.find_class("me/ivanyu/java_library/Calculator")
.expect("Failed to find Calculator class");
// Create an instance of `NativeCallback` (the Rust‑implemented callback).
let native_callback_obj = env
.new_object("me/ivanyu/java_library/NativeCallback", "()V", &[])
.expect("Failed to instantiate NativeCallback");
// Call: Calculator.add(int a, int b, ResultCallback callback)
env.call_static_method(
calculator_class,
"add",
"(IILme/ivanyu/java_library/ResultCallback;)V",
&[
jni::objects::JValue::from(2i32),
jni::objects::JValue::from(3i32),
jni::objects::JValue::from(native_callback_obj),
],
)
.expect("Failed to invoke Calculator.add");
}
What the program does
- Creates a JVM with the compiled JAR on its class path.
- Attaches the current OS thread to the JVM, giving us a
JNIEnv. - Registers the native implementation of
NativeCallback.onResult. - Instantiates
NativeCallback(the object whoseonResultmethod is implemented in Rust). - Calls
Calculator.add(2, 3, callback). The Java code computes the sum, builds aResult, and invokes the native callback, which printsResult: 5from Rust.
Recap
- Embedding a JVM in a Rust binary is straightforward with the
jnicrate’sinvocationfeature. - Native methods can be linked either by following the JNI naming convention or by registering them explicitly with
RegisterNatives. - The example demonstrates a full Java → Rust → Java round‑trip: Java calls a Rust‑implemented callback, which in turn interacts with Java objects.
Feel free to clone the repository, experiment with different signatures, or extend the example to pass more complex data structures between the two runtimes. Happy coding!
Alternative native‑method registration snippet
::from("onResult"),
sig: jni::strings::JNIString::from("(Lme/ivanyu/java_library/Result;)V"),
fn_ptr: Java_me_ivanyu_java_1library_NativeCallback_onResult as *mut c_void,
}];
env.register_native_methods(&native_callback_class, &native_methods)
.expect("Failed to register native methods");
// Create an instance of NativeCallback
let callback_obj = env
// Our native callback code.
.alloc_object(&native_callback_class)
.expect("Failed to allocate NativeCallback object");
// Find the Calculator class and call the `add` method (static).
let calculator_class = env
.find_class("me/ivanyu/java_library/Calculator")
.expect("Failed to find Calculator class");
env.call_static_method(
&calculator_class,
"add",
"(IILme/ivanyu/java_library/ResultCallback;)V",
&[
jni::objects::JValue::Int(3),
jni::objects::JValue::Int(5),
jni::objects::JValue::Object(&callback_obj),
],
)
.expect("Failed to call add");
Running the program:
cargo run
Result: 8
Further reading
There is another similar crate, Duchess, which is more high‑level and focused on ergonomics. It uses macros to reflect Java classes into Rust types and call methods as if they were native Rust methods. This may be a topic for another post.