Java native 키워드로 Rust코드와 연동하기
native 메서드는 자바가 아닌 C, C++, Rust와 같은 다른 언어들로 구현된 메서드를 뜻한다. 이 메서드를 사용하면 자바에서는 지원하지 않는 기능을 활용할 수 있고 성능상의 이유로 사용하지 못했던 코드를 사용할 수 있다.
native 키워드 사용 방법
- 메서드 선언부에 native 키워드를 사용하고, 메서드의 바디는 작성하지 않는다.
- 일반적으로 JNI(Java Native Interface)를 통해 구현된다.
public class NativeExample {
// 네이티브 메서드 선언
public native int add(int num1, int num2);
static {
// 네이티브 라이브러리를 로드
System.loadLibrary("add");
// 또는
System.load(".dll또는 .so파일의 절대 경로")
}
public static void main(String[] args) {
NativeExample example = new NativeExample();
int sum = example.add(3, 4); // 네이티브 메서드 호출
System.out.println(sum);
}
}
위 코드에서 add 메서드는 자바가 아닌 네이티브 언어로 구현되어 있어 System.loadLibrary("add") 혹은 System.load("절대경로/add.dll")를 통해 네이티브 라이브러리를 로드해야 한다.
위에 작성한 Java 코드와 매칭되는 Rust코드를 작성해보자. 그 전에 주의할 점이 있는데 Rust에서 작성할 함수의 이름에 특별한 규칙이 붙는다는 것이다. Java_<패키지명>_<클래스명>_<메서드명> 이 규칙을 신경 쓰며 Rust 코드를 작성해보자. Rust는 Cargo로 제작했다.
use jni::JNIEnv;
use jni::sys::jint;
#[no_mangle] // 외부에서 Rust코드를 호출할 때 이름이 변경되지 않게 하기 위한 속성
pub extern "C" fn Java_example_NativeExample_add(
mut env: JNIEnv, // 자바 환경 정보 - Rust코드를 Java코드로 바꿀 때 사용하기도 함
_class: jni::sys::jclass,
num1: jint, // jint - Rust에서 사용하는 Java의 인트 타입
num2: jint,
) -> jint {
num1 + num2 // 더한 값 리턴
}
/**
[lib]
crate-type = ["cdylib"] // build시 .dll로 만들기 위한 설정
[dependencies]
jni = "0.21.1" // Java의 int를 쓰기위한 의존성
*/
Cargo build --release
위 Cargo 명령어를 입력하면 target/release/폴더명.dll 파일이 생겼을 것이다.

해당 .dll파일을 사용하기 쉽게 Java코드가 있는 폴더에 옮기고 Java 코드를 실행해보자.
그러면 Exception in thread "main" java.lang.UnsatisfiedLinkError: no native in java.library.path: 와 같은 에러가 발생하면서 환경 변수들이 마구 나올 것이다. 이는 System.loadLibrary를 사용하면서 Djava.library.path='.dll파일 절대경로'에 해당하는 VM option이 필요해서 발생한 문제이다. Intellij에서 VM option을 넣어보자.
1. edit configuration 열기

우측 상단의 application - Edit Configurations를 클릭해서 설정 탭을 연다.
2. Add VM options


-Djava.library.path=D:\java\test\class\src\example
Modify options를 클릭하고 Add VM options를 클릭하면 VM options가 placeholder된 입력창이 나오게 되는데 위와 같이
-Djava.library.path=.dll파일 절대 경로 를 넣어준다. 지금까지 문제 없이 진행 했으면 Java Code를 완성하고 바로 실행해보자.
public class NativeExample {
static {
System.loadLibrary("native");
}
public native int add(int a, int b);
public static void main(String[] args) {
NativeExample nativeExample = new NativeExample();
int add = nativeExample.add(100, 200);
System.out.println("add = " + add);
}
}
/**
output: add = 300
*/
이대로 끝내면 아쉬우니 입력된 문자열을 hash256으로 인코딩하는 코드도 native로 작성해보자.
먼저 Java에 native키워드를 사용해서 메서드를 정의하자.
public native String hash256(String input);
그러고 Rust에서 해당 메서드와 매칭되는 메서드를 작성하자.
use hex;
use jni::JNIEnv;
use jni::objects::JString;
use jni::sys::jstring;
use sha2::{Digest, Sha256};
#[no_mangle]
pub extern "C" fn Java_example_NativeExample_hash256(
mut env: JNIEnv,
_class: jni::sys::jclass,
input: JString,
) -> jstring {
let data: String = env.get_string(&input).expect("Failed to convert jstring to Rust String").into();
// SHA-256 해시 계산
let mut hasher = Sha256::new();
hasher.update(data.as_bytes());
let result = hasher.finalize();
// 해시 결과를 16진수 문자열로 변환
let result_str = hex::encode(result);
// Rust String을 Java String으로 변환하여 반환
let output = env.new_string(result_str).expect("Failed to create Java string");
output.into_raw()
}
간단하게 Rust의 Crates를 사용했다.
이제 이렇게 추가한 Rust파일을 다시 빌드하고, 빌드 해서 생긴 dll파일을 아까 만든 dll과 바꿔준다.
String hash256 = nativeExample.hash256("hello world");
System.out.println(hash256);
/**
ouput : b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
*/
위 코드를 추가해서 실행하면 문제 없이 결과가 잘 나오는 것을 볼 수 있다.
이제 native 키워드를 사용해서 외부 함수 사용 방법을 알았으니 이제 어렵고도 어려운 Rust만 공부하면 된다..