본문 바로가기

Java

JNI 실습 (C++에서 자바 클래스에 메서드 구현)

이번 포스트 내용은 JNI Tutorial의 Chapter2 예제를 따라하면서 제 나름대로의 설명이 약간 추가된 것입니다.

 

메서드 등록 | JNI Tutorial

void f5(int, String [], char)

sungcheol-kim.gitbook.io

그리고 원활한 컴파일을 위해 이전 포스트를 먼저 테스트하는 것을 추천드립니다.

이번 포스트에선 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++ 함수 호출)  (1) 2024.08.29