이번 포스트 내용은 JNI Tutorial의 Chapter2 예제를 따라하면서 제 나름대로의 설명이 약간 추가된 것입니다.
그리고 원활한 컴파일을 위해 이전 포스트를 먼저 테스트하는 것을 추천드립니다.
이번 포스트에선 C++에서 특정 자바 클래스에 메서드를 추가해 보겠습니다.
JNI 코드에서 자바 클래스 파일을 찾아 해당 클래스에 있는 네이티브 메서드 선언에 메서드 정의를 바인딩할 수 있습니다.
메서드를 정의를 바인딩하려면 JNI_OnLoad 함수에서 JavaVM을 사용해야 합니다.
다른 위치에서도 바인딩을 할 수 있지만, 모든 JNI 라이브러리를 로드할 때 JNI_Onload라는 함수를 찾아 가장 먼저 실행시키기 때문에 여기서 바인딩하는 것이 좋습니다.
테스트 환경
OS: MacOS
Java: OpenJDK 17.0.11+9 LTS (Microsoft)
g++:Apple clang version 15.0.0 (clang-1500.3.9.4)
테스트 도중 문제가 발생한다면, JavaJDK나 g++ 버전, 아래 설명할 g++ include 경로 문제일 가능성이 큽니다.
Calculator.java
생성
sudo vi Calculator.java
내용
public class Calculator {
private native int add(int a, int b);
private native int mul(int a, int b);
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println("1 + 1 =" + calc.add(1, 1));
System.out.println("10 * 10 =" + calc.mul(10, 10));
}
static {
System.loadLibrary("calculator");
}
}
접근제한자와 타입 사이에 native 키워드가 붙어 있습니다.
이 메서드는 JNI를 통해 구현된다고 컴파일러에게 알려주는 것입니다.
Calculator.cpp
이번엔 자동으로 실행되는 JNI_Onload 함수만 구현하면 되기 때문에, 이전 포스트에서 처럼 javac JNI 규칙에 맞는 함수를 정의할 필요가 없습니다.
생성
sudo vi Calculator.cpp
내용
#include <jni.h>
#include <iostream>
jint add
(
JNIEnv *env,
jobject thiz,
jint a,
jint b
)
{
return a + b;
}
jint mul
(
JNIEnv *env,
jobject thiz,
jint a,
jint b
)
{
return a * b;
}
JNIEXPORT jint JNICALL JNI_OnLoad
(
JavaVM *pVM,
void *reserved
)
{
JNIEnv *env;
if (pVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6)) {
return -1;
}
// 메서드 시그니처를 생성한다.
JNINativeMethod nm[2];
nm[0].name = const_cast<char *>("add");
nm[0].signature = const_cast<char *>("(II)I");
nm[0].fnPtr = reinterpret_cast<void *>(add);
nm[1].name = const_cast<char *>("mul");
nm[1].signature = const_cast<char *>("(II)I");
nm[1].fnPtr = reinterpret_cast<void *>(mul);
// 클래스를 찾고, 해당 클레스에 네이티브 메서드로 등록한다.
jclass cls = env->FindClass("Calculator");
env->RegisterNatives(cls, nm, 2);
return JNI_VERSION_1_6;
}
JavaVM 객체를 통해 JNIEnv 객체를 얻어옵니다.
JavaVM 객체의 GetEnv 메서드는 첫 번째 매개변수에 JNIEnv 객체의 포인터를 입력해 줍니다.
jni.h는 파일 상단에 사용할 수 있는 버전을 정의하고 있습니다.
#define JNI_VERSION_1_1 0x00010001
#define JNI_VERSION_1_2 0x00010002
#define JNI_VERSION_1_4 0x00010004
#define JNI_VERSION_1_6 0x00010006
#define JNI_VERSION_1_8 0x00010008
#define JNI_VERSION_9 0x00090000
#define JNI_VERSION_10 0x000a0000
JNI_VERSION_1_6을 정의하고 있지 않다면 작동을 중지합니다.
if (pVM->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6)) {
return -1;
}
JNINativeMethod 구조체는 name, signature, fnPtr 속성을 가지고 있습니다.
- name: 바인딩할 메서드 이름
- signature: (입력)출력 → (II)I는 정수 2개를 받아 정수를 반환
- fnPtr: 함수 포인터(함수명)
다음과 같이 작성하는 것도 가능합니다.
JNINativeMethod nm[2] = {
{
const_cast<char *>("add"),
const_cast<char *>("(II)I"),
reinterpret_cast<void *>(add)
},
{
const_cast<char *>("mul"),
const_cast<char *>("(II)I"),
reinterpret_cast<void *>(mul)
}
};
JNIEnv 객체의 FindClass 메서드로 특정 클래스에 대한 포인터를 얻을 수 있습니다.
jclass cls = env->FindClass("Calculator");
JNIEnv 객체의 RegisterNatives 메서드로 특정 클래스에 네이티브 정의를 바인딩합니다.
env->RegisterNatives(cls, nm, 2);
세 번째 매개변수는 두 번째 매개변수에 입력된 배열의 크기입니다.
이제 컴파일해서 실행할 수 있습니다.
// 컴파일 Calculator.cpp → Calculator.o
g++ -I "$JAVA_HOME/include/" -I "$JAVA_HOME/include/darwin" -c Calculator.cpp
// 컴파일 Calculator.o → libcalculator.jnilib
g++ -dynamiclib -o libcalculator.jnilib Calculator.o
// 컴파일 → Calculator.class 파일 생성
javac Calculator.java
// 실행
java Calculator
이전 포스트에서 언급한 것처럼 cpp 파일을 컴파일할 때, jni.h와 jni_md.h의 위치를 지정해 주어야 합니다.
'Java' 카테고리의 다른 글
JNI 실습 (Java에서 C++ 함수 호출) (0) | 2024.08.29 |
---|