Java Native Interface
Java Native Interface는 이름 그대로 Java에서 Native Code를 실행할 수 있는 Interface를 제공한다. Native Code란 Java의 바이트코드처럼 어느 환경에서나 실행할 수 있는 코드가 아닌, 해당 환경에서만 실행할 수 있는 코드를 말한다. 간단히 C/C++ 코드를 컴파일한 어셈블리 코드를 생각하면 된다.
python에서 TensorFlow를 사용해봤다면 익숙할 것이다. python은 인터프리터 언어로 실행 시간이 평균적으로 Java보다도 느리다. 따라서 python으로 모델 학습을 시킨다면 매우 느릴 것이다. 따라서 TensorFlow는 대부분을 C++로 구현해 python에서도 모델을 빠르게 학습시킬 수 있게 만들었다. 이렇게 내부적인 코드를 Native로 구현하면 더 빠르고 효율적인 코드를 작성할 수 있다.
사실, Java에서도 이미 알게 모르게 Native 코드를 사용하고 있다. 예를 들면, 시스템의 시간을 얻는 System.currentTimeMillis 메소드를 보면 native 키워드가 있다. 이 메소드는 JVM이 실행중인 OS에서 제공하는 시간 함수를 호출하도록 구현되어있다.
[참고 - 리눅스 구현 코드] https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/os/linux/vm/os_linux.cpp#l1362
Native Code를 사용하려면 가장 먼저 동적 라이브러리를 만들어야 한다.
동적 라이브러리
라이브러리는 크게 정적 라이브러리(Static Library)와 동적 라이브러리(Dynamic Library)로 나눌 수 있다. 정적 라이브러리는 Object 파일에 링크되어 하나의 실행가능 파일로 만들어지는 것이고, 동적 라이브러리는 실행파일이 라이브러리에 대한 참조만 가지고 있다가 런타임에 사용하는 라이브러리다.
Java는 일반적으로 실행가능 파일로 컴파일해서 사용하지 않기 때문에, 우리는 동적 라이브러리를 만들어야 한다. JVM에서 런타임에 이 라이브러리를 참조해서 사용할 것이다.
헤더 파일 만들기
이제 C++로 동적 라이브러리를 만들어보자. 먼저, 헤더 파일을 만들어야 하지만, 우리는 처음부터 만들 필요가 없다. Java에서 헤더 파일을 만드는 기능을 제공해주기 때문이다. 우리는 native 키워드로 메소드를 선언하기만 하면 된다.
public class NativeLib {
public native void printHello();
public native void print(String str);
public native int sum(int num1, int num2);
}
3가지 다른 메소드를 선언해주었다. 이제 NativeLib 클래스를 컴파일하고 헤더파일을 만들어준다.
javac -h . NativeLib.java
위 코드는 NativeLib.java를 컴파일하고, 헤더파일을 만들어주는 작업을 둘 다 해준다. 만약 따로 하고싶다면 javac로 컴파일한 후, javah 바이너리를 사용하면 된다.
javac NativeLib.java
javah NativeLib
이제 헤더 파일이 생성된 것을 확인할 수 있다.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class NativeLib */
#ifndef _Included_NativeLib
#define _Included_NativeLib
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: NativeLib
* Method: printHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_NativeLib_printHello
(JNIEnv *, jobject);
/*
* Class: NativeLib
* Method: print
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_NativeLib_print
(JNIEnv *, jobject, jstring);
/*
* Class: NativeLib
* Method: sum
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_NativeLib_sum
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
JNIEXPORT와 JNICALL은 JNI에서 우리가 구현할 코드를 사용할 수 있도록 만들어주는 역할을 한다. JNIEnv는 우리의 코드에서 Java 메소드를 사용할 수 있도록 도와주는 역할이다.
C++ 함수 구현
이제 헤더파일에 선언된 3개의 함수를 구현하자. NativeLib.cpp 파일을 아래와 같이 만들어준다.
#include <jni.h>
#include <iostream>
#include "NativeLib.h"
JNIEXPORT void JNICALL Java_NativeLib_printHello(JNIEnv *env, jobject obj)
{
std::cout << "Hello JNI!!" << std::endl;
}
JNIEXPORT void JNICALL Java_NativeLib_print(JNIEnv *env, jobject obj, jstring str)
{
std::cout << str << std::endl;
}
JNIEXPORT jint JNICALL Java_NativeLib_sum(JNIEnv *env, jobject obj, jint num1, jint num2)
{
return num1 + num2;
}
동적 라이브러리 생성
[참고] https://www.baeldung.com/jni#3-compiling-and-linking
이제, C++ 코드를 가지고 동적 라이브러리를 생성하자. 동적 라이브러리를 생성하는 방법은 환경에 따라 다르기 때문에, 위의 링크를 참고하면 된다. Mac OS의 경우, 아래와 같이 컴파일 할 수 있다.
g++ -c -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin NativeLib.cpp -o NativeLib.o
NativeLib.o 파일이 생성되었다. 이 파일을 동적 라이브러리로 만들어 준다.
g++ -dynamiclib -o libNativeLib.dylib NativeLib.o -lc
libNativeLib.dylib라는 이름으로 동적 라이브러리가 만들어졌다. Mac OS에서는 동적 라이브러리 이름 앞에 lib가 붙기 때문에 사용할 때는 NativeLib로 사용할 수 있다.
실행
이제 만들어진 라이브러리를 이용해서 Native Code를 실행해보자.
public static void main(String[] args) {
System.loadLibrary("NativeLib");
NativeLib lib = new NativeLib();
lib.printHello();
lib.print("TEST");
System.out.println(lib.sum(10, 30));
}
먼저, JVM에서 동적 라이브러리를 사용하기 위해서는 NativeLib를 로드해줘야 한다. System.loadLibrary 메소드를 사용하면 된다. 다음으로, native 키워드로 메소드를 선언한 NativeLib 클래스의 인스턴스를 만들어주고, 메소드를 호출해준다. 결과는 아래와 같다.
Hello JNI!!
0x70000e7919f0
40
printHello 메소드와 sum 메소드는 원하는 대로 실행된 것을 확인할 수 있다. 그러나 print 메소드는 파라미터로 넘겨준 "TEST"가 아닌 주소값을 출력한 것을 확인할 수 있다. JNI에서 String을 다룰 때는 JNIEnv를 이용해서 아래와 같이 값을 복사해야 한다.
JNIEXPORT void JNICALL Java_NativeLib_print(JNIEnv *env, jobject obj, jstring str)
{
const char *cString = env -> GetStringUTFChars(str, nullptr);
std::cout << cString << std::endl;
env -> ReleaseStringUTFChars(str, cString);
}
이제 동적 라이브러리를 다시 생성하고 실행해보면 원하는 결과를 얻을 수 있다.
Hello JNI!!
TEST
40
Exception Handling
Java는 Exception이 발생한 경우 try-catch 등으로 예외를 처리하거나 던질 수 있다. C++에서도 비슷한 문법이 존재하지만, 발생할 수 있는 예외들이 1:1로 대응한다는 보장이 없다. 따라서 JNI에서는 다양한 예외 처리 메소드를 지원하고 있다.
JNIEnv를 이용해서 해당 메소드들을 사용할 수 있다. 먼저, ExceptionOccurred와 ExceptionCheck 함수는 예외가 발생했는지를 확인하는 함수이다. ExceptionOccurred는 발생한 Exception 인스턴스의 참조를 반환하고, ExceptionCheck는 JNI_TRUE와 JNI_FALSE로 예외 발생 여부만 반환한다. 다음으로, ExceptionClear는 Exception을 없애고, Native Code에서 처리하기 위해 사용할 수 있다. 마지막으로, ThrowNew는 새로운 Exception을 발생시켜 Java 코드에 전달하는 데 사용할 수 있다.
3줄 요약
- JNI를 사용하면 특정 환경에서만 존재하는 기능을 사용할 수 있다
- Native Code를 사용해서 성능을 개선할 수 있다
- 동적 라이브러리를 만들고, 런타임에 로드하여 사용한다
'Java > 기타' 카테고리의 다른 글
[Java] GC가 일어나면 hashCode의 값이 바뀔까? (0) | 2023.03.29 |
---|---|
[Java] Java Native Image - JVM 없이 Java 어플리케이션 실행하기 (0) | 2023.03.19 |