본문 바로가기

Java

JNI 실습 (Java에서 C++ 함수 호출)

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

 

Hello JNI | JNI Tutorial

맥에서 Hello JNI!를 출력하는 Java 응용프로그램을 개발해 보자. 단 이 응용프로그램은 문자열을 출력하는 부분을 C++ 로 작성할 것이다. 네이티브 라이브러를 사용하는 Java 클래스 작성하기 java 코

sungcheol-kim.gitbook.io

Java Native Interface 

자바에서 저수준 코드(C, C++) 상호작용(사용하고 사용당하고)할 있게 해주는 자바 인터페이스입니다.

 

자바의 관리 오버헤드를 줄이고 성능을 높일 있고, 하드웨어를 제어할 있지만, 코드가 복잡해지고 관리가 어렵습니다.

 

리액트 네이티브에선 자바와 자바스크립트간 통신을 위한 JSI 위해 사용됩니다.

 

JVM 런타임에 C++ 라이브러리를 로드해 실행해 보겠습니다.

테스트 환경

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 경로 문제일 가능성이 큽니다.

HelloJNI.java

파일 생성

sudo vi HelloJNI.java

 

내용

class HelloJNI {
  private native void print();

  public static void main(String[] args) {
    new HelloJNI().print();
  }

  static {
    System.loadLibrary("HelloJNI"); // HelloJNI라는 C++ 라이브러리 로드 아직 구현 X
  }
}

 

 

이렇게 만들어진 자바 파일을 기반으로 C++ 라이브러리를 만들어 보겠습니다.

HelloJNI.h

javac 명령어로 자바 클래스에 대응하는 cpp 헤더를 생성할 수 있습니다.

javac -h . HelloJNI.java

 

위 명령어를 실행하면 다음과 같은 Hello.JNI.h 파일이 생성됩니다.

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    print
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_print
  (JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif

 

JNIEXPORT와 JNICALL은 JNI를 사용하기 위한 메크로이고, Java_HelloJNI_print는 "Java_패키지이름_클래스이름_메서드이름"  JNI 이름 규칙에 따라 지어진 메서드입니다.

HelloJNI.cpp

이제 Java_HelloJNI_print 함수의 구현체를 작성해 보겠습니다.

 

파일 생성

sudo vi HelloJNI.cpp

 

내용

#include <jni.h>
#include <iostream>
#include "HelloJNI.h"

JNIEXPORT void JNICALL Java_HelloJNI_print
(
  JNIEnv *env,
  jobject thiz
)
{
  std::cout << "Hello JNI!" << std::endl;
}

 

단순히 "Hello JNI!" 문자열을 출력하는 함수입니다.

jni.h

그런데 위에 보면 jni.h을 가져오고 있습니다.

 

그리고 jni.h를 사용하려면 Jni_md.h 파일의 위치도 알아야 합니다.

 

MacOS의 경우 jni.h와 jni_md.h 파일은 JDK를 설치할 때 JDK 경로 기준으로 Contests/Home/include와  Contests/Home/include/darwin 경로에 같이 설치됩니다.

 

저는 ~/.zshrc 파일에 JAVE_HOME을 다음과 같이 정의해 놓고 $JAVA_HOME/include/, $JAVA_HOME/include/darwin으로 사용하겠습니다.

export JAVA_HOME="/Library/Java/JavaVirtualMachines/microsoft-17.jdk/Contents/Home"

 

각자 플랫폼에 맞는 jni.h 파일의 위치를 알고 있어야 합니다.

object

c(pp) 파일을 컴파일하여 생성된 중간 결과물입니다.

 

기계어가 일부 포함되어 있지만, 실행 가능한 상태는 아니며, 일반적으로 링커에 의해 최종 실행 파일이나 라이브러리로 결합됩니다.

g++ -I "$JAVA_HOME/include/" -I "$JAVA_HOME/include/darwin" -c HelloJNI.cpp

 

컴파일러가 jni.h와 jni_md.h 파일을 찾을 수 있도록, 해당 파일의 경로를 추가 지정해 주었습니다.

 

그럼 HelloJNI.o 파일이 생성됩니다.

shared object

런타임에 동적으로  로드할 수 있는 object입니다.

 

다음과 같이 object 파일을 shared object 파일로 변환할 수 있습니다.

g++ -dynamiclib -o libhellojni.jnilib HelloJNI.o

 

shared object 파일 이름은 반드시 lib으로 시작해야 하고, 확장자는 jnilib이어야 합니다.

 

동적 라이브러리의 확장자는 플랫폼마다 다릅니다.

  • MacOS: dylib 또는 jnilib
  • 리눅스: so
  • 윈도우: dll

리액트 네이티브의 안드로이드 코드를 본 적이 있다면, MainApplication에서 SoLoader를 사용하는 것을 보셨을 겁니다.

 

여기서 So가 shared object를 의미합니다.

 

이 클래스의 역할중 하나가 C++ 라이브러리를 로드하는 것입니다.

 

저는 어떤 라이브러리 충돌 이슈로 처음 이 클래스를 봤던 것으로 기억합니다.

 

그리고 처음 만들었던 HelloJNI.java 파일을 컴파일해서 실행해 보겠습니다.

// 컴파일 → 바이트 코드(HelloJNI.class) 생성
javac HelloJNI.java

// 실행
java HelloJNI

 

HelloJNI 클래스의 System.loadLibrary("HelloJNI"); 코드는 실행 파일은 자신의 스코프 안에서 jnihellojni.jnilib 파일을 찾아서 실행합니다.

 

그리고 new HelloJNI().print(); 코드는 객체를 만들고 메서드를 실행시키는 것 같지만, 실제로는 로드된 C++ 라이브러리에서 JAVA_HelloJNI_print 함수를 찾아 실행시킵니다.

 

라이브러리 이름은 대소문자를 무시하고 앞에 jni가 추가된 jnilib 파일을 찾는 것이 규칙이기 때문에, 위에서 shared object 파일을 생성할 때 앞에 jni를 붙여준 것입니다.

'Java' 카테고리의 다른 글

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