Rust에 JVM 임베딩

발행: (2026년 1월 4일 오후 11:40 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

위의 링크에 포함된 텍스트를 번역하려면, 번역하고 싶은 전체 내용을 제공해 주세요.
코드 블록, URL 및 마크다운 형식은 그대로 유지하면서 본문만 한국어로 번역해 드리겠습니다.

이 글에서

  • Rust 애플리케이션에 JVM을 임베드하기.
  • Rust에서 Java 코드를 호출하고 또한 Java에서 Rust 코드를 호출하기.

이 글은 Java와 Rust 모두에 익숙하다고 가정합니다. 전체 소스 코드는 GitHub repository for this article에서 확인할 수 있습니다.

Source:

Java 측 – 작은 라이브러리

다양한 Java‑Rust 상호작용 기능을 보여주기 위해 의도적으로 인공적인 작은 예제를 사용할 것입니다. 이 라이브러리는 세 개의 클래스로 구성됩니다:

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는 두 정수의 합을 계산하고, 메시지 문자열을 만든 뒤, 이를 Result 객체에 감싸고, 마지막으로 전달된 콜백을 호출합니다.

네이티브 콜백 클래스

Rust가 콜백을 처리하도록 하기 위해, ResultCallback을 구현하는 클래스에 네이티브 메서드를 선언합니다:

package me.ivanyu.java_library;

public class NativeCallback implements ResultCallback {
    @Override
    // `native` 키워드는 구현이 네이티브 코드에 있음을 JVM에 알립니다.
    public native void onResult(Result result);
}

JAR 빌드

./gradlew clean jar

생성된 JAR 파일은 java/lib/build/libs/lib.jar에 위치합니다.

Rust 측 – 의존성

Cargo.tomljni 크레이트를 추가하세요. invocation 기능은 Rust에서 JVM을 실행하기 위해 필요합니다.

[dependencies]
jni = { version = "0.21.1", features = ["invocation"] }

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);
}

핵심 포인트

  • #[unsafe(no_mangle)]은 이름 맹글링을 비활성화하여 JVM이 함수 위치를 찾을 수 있게 합니다.
  • extern "system"은 플랫폼에 맞는 JNI 호출 규약을 선택합니다.
  • 메서드 이름은 JNI 명명 규칙(Java___)을 따릅니다.
  • Result.getMessage()가 반환하는 String을 가져와 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.

요약

  • Embedding a JVM(JVM 삽입)은 jni 크레이트의 invocation 기능을 사용하면 Rust 바이너리에서 간단하게 할 수 있습니다.
  • Native methods(네이티브 메서드)는 JNI 명명 규칙을 따르거나 RegisterNatives를 통해 명시적으로 등록함으로써 연결할 수 있습니다.
  • 예제는 Java → Rust → Java 전체 라운드‑트립을 보여줍니다: Java가 Rust에서 구현한 콜백을 호출하고, 그 콜백이 다시 Java 객체와 상호작용합니다.

레포지토리를 클론하고, 다양한 시그니처를 실험하거나 두 런타임 간에 더 복잡한 데이터 구조를 전달하도록 예제를 확장해 보세요. 즐거운 코딩 되세요!

대체 네이티브 메서드 등록 스니펫

::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");

프로그램 실행:

cargo run
Result: 8

Further reading

비슷한 다른 크레이트인 Duchess 가 있는데, 이것은 더 높은 수준이며 사용성에 중점을 둡니다. 이 크레이트는 매크로를 사용해 Java 클래스를 Rust 타입으로 반영하고, 마치 네이티브 Rust 메서드처럼 메서드를 호출합니다. 이는 다음 글의 주제가 될 수도 있습니다.

Back to Blog

관련 글

더 보기 »

Java 소개

인간이 작성한 Java 코드는 어떻게 기계에서 실행될까요? 프로그래밍 언어를 기계어로 변환하기 위해 JDK → JRE → JVM → JIT 라는 번역 시스템을 사용합니다.