Language/Java

Java native 키워드로 Rust코드와 연동하기

러러 2024. 9. 19. 21:44

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만 공부하면 된다..