Embedding JVM in Rust

Published: (January 4, 2026 at 09:40 AM EST)
5 min read
Source: Dev.to

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 String returned by Result.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

  1. Creates a JVM with the compiled JAR on its class path.
  2. Attaches the current OS thread to the JVM, giving us a JNIEnv.
  3. Registers the native implementation of NativeCallback.onResult.
  4. Instantiates NativeCallback (the object whose onResult method is implemented in Rust).
  5. Calls Calculator.add(2, 3, callback). The Java code computes the sum, builds a Result, and invokes the native callback, which prints Result: 5 from Rust.

Recap

  • Embedding a JVM in a Rust binary is straightforward with the jni crate’s invocation feature.
  • 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.

Back to Blog

Related posts

Read more »

Introduction to Java

How does Java code written by humans get executed by machines? It uses a translation system JDK → JRE → JVM → JIT to convert the programming language to machin...