Rust에 JVM 임베딩
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.toml에 jni 크레이트를 추가하세요. 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
- 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.
요약
- 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 메서드처럼 메서드를 호출합니다. 이는 다음 글의 주제가 될 수도 있습니다.