1. 소개

우리가 알고 있듯이 Java의 주요 장점 중 하나는 이식성입니다. 즉, 코드를 작성하고 컴파일하면 이 프로세스의 결과가 플랫폼 독립적인 바이트코드가 됩니다.

간단히 말해서 이것은 Java 가상 머신을 실행할 수 있는 모든 기계 또는 장치에서 실행할 수 있으며 예상한 대로 원활하게 작동합니다.

그러나 때로는 특정 아키텍처에 대해 고유하게 컴파일된 코드를 실제로 사용해야 하는 경우가 있습니다 .

네이티브 코드를 사용해야 하는 몇 가지 이유가 있을 수 있습니다.

  • 일부 하드웨어를 처리해야 하는 필요성
  • 매우 까다로운 프로세스의 성능 향상
  • Java로 다시 작성하는 대신 재사용하려는 기존 라이브러리입니다.

이를 달성하기 위해 JDK는 JVM에서 실행되는 바이트코드와 기본 코드 (일반적으로 C 또는 C++로 작성됨) 사이에 브리지를 도입합니다 .

이 도구를 Java 네이티브 인터페이스라고 합니다. 이 기사에서는 이를 사용하여 코드를 작성하는 방법을 살펴보겠습니다.

2. 작동 원리

2.1. 네이티브 메소드: JVM이 컴파일된 코드를 만나다

Java는 메서드 구현이 네이티브 코드에서 제공될 것임을 나타내는 데 사용되는 네이티브 키워드를 제공합니다.

일반적으로 네이티브 실행 프로그램을 만들 때 정적 또는 공유 라이브러리를 사용할 수 있습니다.

  • 정적 라이브러리 – 모든 라이브러리 바이너리는 연결 프로세스 동안 실행 파일의 일부로 포함됩니다. 따라서 libs는 더 이상 필요하지 않지만 실행 파일의 크기가 증가합니다.
  • 공유 라이브러리 – 최종 실행 파일에는 코드 자체가 아닌 라이브러리에 대한 참조만 있습니다. 실행 파일을 실행하는 환경이 프로그램에서 사용하는 libs의 모든 파일에 액세스할 수 있어야 합니다.

후자는 바이트 코드와 고유하게 컴파일된 코드를 동일한 바이너리 파일에 혼합할 수 없기 때문에 JNI에 대해 의미가 있습니다.

따라서 공유 라이브러리는 클래스의 일부가 아닌 .so/.dll/.dylib 파일(사용 중인 운영 체제에 따라 다름) 내에 네이티브 코드를 별도로 보관합니다 .

기본 키워드는 추상적 인 방법의 일종으로 우리의 방법을 변환 :

private native void aNativeMethod();

다른 Java 클래스에 의해 구현되는 대신 분리된 기본 공유 라이브러리에서 구현 된다는 주요 차이점 이 있습니다 .

메모리에 포인터가 있는 테이블은 모든 네이티브 메서드의 구현에 대해 구성되어 Java 코드에서 호출할 수 있습니다.

2.2. 필요한 구성 요소

다음은 고려해야 할 주요 구성 요소에 대한 간략한 설명입니다. 이 기사의 뒷부분에서 더 자세히 설명하겠습니다.

  • 자바 코드 – 우리의 클래스. 여기에는 최소한 하나의 기본 메서드 가 포함됩니다 .
  • 네이티브 코드 – 일반적으로 C 또는 C++로 코딩된 네이티브 메서드의 실제 논리입니다.
  • JNI 헤더 파일 – C/C++용 이 헤더 파일( JDK 디렉토리에 /jni.h 포함)에는 기본 프로그램에 사용할 수 있는 JNI 요소의 모든 정의가 포함되어 있습니다.
  • C/C++ 컴파일러 – GCC, Clang, Visual Studio 또는 플랫폼에 대한 기본 공유 라이브러리를 생성할 수 있는 한 우리가 원하는 모든 것 중에서 선택할 수 있습니다.

2.3. 코드의 JNI 요소(Java 및 C/C++)

자바 요소:

  • "네이티브" 키워드 – 이미 다루었듯이 네이티브로 표시된 모든 메서드는 네이티브 공유 라이브러리에서 구현되어야 합니다.
  • System.loadLibrary(String libname) – 공유 라이브러리를 파일 시스템에서 메모리로 로드하고 내보낸 함수를 Java 코드에서 사용할 수 있도록 하는 정적 메서드입니다.

C/C++ 요소(많은 요소가 jni.h 내에 정의 )

  • JNIEXPORT- 함수를 공유 라이브러리에 내보내기 가능으로 표시하여 함수 테이블에 포함되므로 JNI가 찾을 수 있습니다.
  • JNICALL – JNIEXPORT 와 결합 하여 JNI 프레임워크에서 메서드를 사용할 수 있도록 합니다.
  • JNIEnv – 네이티브 코드를 사용하여 Java 요소에 액세스할 수 있는 메서드가 포함된 구조
  • JavaVM – 실행 중인 JVM을 조작할 수 있는 구조(또는 새 JVM 시작)에 스레드를 추가하고 파괴하는 등…

3. 헬로월드 JNI

다음으로 JNI가 실제로 어떻게 작동하는지 살펴보겠습니다.

이 사용방법(예제)에서는 C++를 기본 언어로 사용하고 G++를 컴파일러 및 링커로 사용합니다.

선호하는 다른 컴파일러를 사용할 수 있지만 Ubuntu, Windows 및 MacOS에 G++를 설치하는 방법은 다음과 같습니다.

  • Ubuntu Linux – 터미널에서 "sudo apt-get install build-essential" 명령 실행
  • Windows – MinGW 설치
  • MacOS – 터미널에서 "g++" 명령을 실행 하고 아직 없으면 설치합니다.

3.1. 자바 클래스 생성

고전적인 "Hello World"를 구현하여 첫 번째 JNI 프로그램 만들기를 시작해 보겠습니다.

시작하려면 작업을 수행할 기본 메서드가 포함된 다음 Java 클래스를 만듭니다.

package com.baeldung.jni;

public class HelloWorldJNI {

    static {
        System.loadLibrary("native");
    }
    
    public static void main(String[] args) {
        new HelloWorldJNI().sayHello();
    }

    // Declare a native method sayHello() that receives no arguments and returns void
    private native void sayHello();
}

보시다시피 정적 블록에 공유 라이브러리를 로드합니다 . 이렇게 하면 필요할 때 언제 어디서나 필요할 때 사용할 수 있습니다.

또는 이 간단한 프로그램에서 다른 곳에서는 네이티브 라이브러리를 사용하지 않기 때문에 네이티브 메서드를 호출하기 직전에 라이브러리를 로드할 수 있습니다.

3.2. C++에서 메서드 구현

이제 C++에서 네이티브 메서드의 구현을 만들어야 합니다.

C++ 내에서 정의와 구현은 일반적으로 각각 .h.cpp 파일에 저장됩니다.

먼저 메소드의 정의를 작성 하려면 Java 컴파일러 -h 플래그 를 사용해야 합니다 .

javac -h . HelloWorldJNI.java

이렇게 하면 클래스에 포함된 모든 네이티브 메서드가 매개변수로 전달된 com_baeldung_jni_HelloWorldJNI.h 파일 이 생성 됩니다. 이 경우에는 하나만 전달됩니다.

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
  (JNIEnv *, jobject);

보시다시피 함수 이름은 정규화된 패키지, 클래스 및 메서드 이름을 사용하여 자동으로 생성됩니다.

또한 우리가 알아차릴 수 있는 흥미로운 점은 두 개의 매개변수가 함수에 전달된다는 것입니다. 현재 JNIEnv에 대한 포인터 . 또한 메서드가 연결된 Java 객체인 HelloWorldJNI 클래스 의 인스턴스입니다 .

이제 sayHello 함수를 구현하기 위해 .cpp 파일 을 만들어야 합니다. 여기에서 "Hello World"를 콘솔에 출력하는 작업을 수행합니다.

헤더를 포함하는 .h 파일과 동일한 이름으로 .cpp 파일의 이름을 지정하고 다음 코드를 추가하여 기본 기능을 구현합니다.

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
  (JNIEnv* env, jobject thisObject) {
    std::cout << "Hello from C++ !!" << std::endl;
}

3.3. 컴파일 및 링크

이 시점에서 필요한 모든 부품이 제자리에 있고 부품 간에 연결되어 있습니다.

C++ 코드에서 공유 라이브러리를 빌드하고 실행해야 합니다!

그렇게 하려면 Java JDK 설치에서 JNI 헤더를 포함하는 것을 잊지 않고 G++ 컴파일러를 사용해야 합니다 .

우분투 버전:

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

윈도우 버전:

g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

MacOS 버전;

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

플랫폼용 코드를 com_baeldung_jni_HelloWorldJNI.o 파일로 컴파일 하면 새 공유 라이브러리에 포함해야 합니다. 이름을 지정하기로 결정한 것은 System.loadLibrary 메소드에 전달된 인수 입니다.

우리는 "네이티브"라는 이름을 지정했으며 Java 코드를 실행할 때 로드합니다.

그런 다음 G++ 링커는 C++ 개체 파일을 브리지 라이브러리에 연결합니다.

우분투 버전:

g++ -shared -fPIC -o libnative.so com_baeldung_jni_HelloWorldJNI.o -lc

윈도우 버전:

g++ -shared -o native.dll com_baeldung_jni_HelloWorldJNI.o -Wl,--add-stdcall-alias

MacOS 버전:

g++ -dynamiclib -o libnative.dylib com_baeldung_jni_HelloWorldJNI.o -lc

그리고 그게 다야!

이제 명령줄에서 프로그램을 실행할 수 있습니다.

그러나 방금 생성한 라이브러리가 포함된 디렉토리의 전체 경로를 추가해야 합니다. 이렇게 하면 Java는 네이티브 라이브러리를 찾을 위치를 알 수 있습니다.

java -cp . -Djava.library.path=/NATIVE_SHARED_LIB_FOLDER com.baeldung.jni.HelloWorldJNI

콘솔 출력:

Hello from C++ !!

4. 고급 JNI 기능 사용하기

인사하는 것은 좋지만 별로 유용하지 않습니다. 일반적으로 Java와 C++ 코드 간에 데이터를 교환하고 프로그램에서 이 데이터를 관리하려고 합니다.

4.1. 네이티브 메소드에 매개변수 추가하기

네이티브 메서드에 몇 가지 매개변수를 추가합니다. 매개변수와 다른 유형의 리턴을 사용하는 두 개의 기본 메소드로 ExampleParametersJNI 라는 새 클래스를 생성해 보겠습니다 .

private native long sumIntegers(int first, int second);
    
private native String sayHelloToMe(String name, boolean isFemale);

그런 다음 이전과 같이 "javac -h"를 사용하여 새 .h 파일을 만드는 절차를 반복합니다.

이제 새 C++ 메서드를 구현하여 해당 .cpp 파일을 만듭니다.

...
JNIEXPORT jlong JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sumIntegers 
  (JNIEnv* env, jobject thisObject, jint first, jint second) {
    std::cout << "C++: The numbers received are : " << first << " and " << second << std::endl;
    return (long)first + (long)second;
}
JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sayHelloToMe 
  (JNIEnv* env, jobject thisObject, jstring name, jboolean isFemale) {
    const char* nameCharPointer = env->GetStringUTFChars(name, NULL);
    std::string title;
    if(isFemale) {
        title = "Ms. ";
    }
    else {
        title = "Mr. ";
    }

    std::string fullName = title + nameCharPointer;
    return env->NewStringUTF(fullName.c_str());
}
...

JNIEnv 유형 의 포인터 *env사용 하여 JNI 환경 인스턴스에서 제공하는 메소드에 액세스했습니다.

이 경우 JNIEnv를 사용하면 Java 문자열 을 C++ 코드에 전달하고 구현에 대해 걱정하지 않고 뒤로 물러날 수 있습니다.

Oracle 공식 문서 에서 Java 유형과 C JNI 유형의 동등성을 확인할 수 있습니다 .

코드를 테스트하려면 이전 HelloWorld 예제 의 모든 컴파일 단계를 반복해야 합니다 .

4.2. 개체 사용 및 네이티브 코드에서 Java 메서드 호출

이 마지막 예제에서는 Java 개체를 기본 C++ 코드로 조작하는 방법을 볼 것입니다.

일부 사용자 정보를 저장하는 데 사용할 새 클래스 UserData 만들기를 시작합니다 .

package com.baeldung.jni;

public class UserData {
    
    public String name;
    public double balance;
    
    public String getUserInfo() {
        return "[name]=" + name + ", [balance]=" + balance;
    }
}

그런 다음 UserData 유형의 개체를 관리하는 데 사용할 몇 가지 기본 메서드를 사용하여 ExampleObjectsJNI 라는 또 다른 Java 클래스를 만듭니다 .

...
public native UserData createUser(String name, double balance);
    
public native String printUserData(UserData user);

한 번 더 .h 헤더를 만든 다음 새 .cpp 파일 에 네이티브 메서드의 C++ 구현을 만들어 보겠습니다 .

JNIEXPORT jobject JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser
  (JNIEnv *env, jobject thisObject, jstring name, jdouble balance) {
  
    // Create the object of the class UserData
    jclass userDataClass = env->FindClass("com/baeldung/jni/UserData");
    jobject newUserData = env->AllocObject(userDataClass);
	
    // Get the UserData fields to be set
    jfieldID nameField = env->GetFieldID(userDataClass , "name", "Ljava/lang/String;");
    jfieldID balanceField = env->GetFieldID(userDataClass , "balance", "D");
	
    env->SetObjectField(newUserData, nameField, name);
    env->SetDoubleField(newUserData, balanceField, balance);
    
    return newUserData;
}

JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_printUserData
  (JNIEnv *env, jobject thisObject, jobject userData) {
  	
    // Find the id of the Java method to be called
    jclass userDataClass=env->GetObjectClass(userData);
    jmethodID methodId=env->GetMethodID(userDataClass, "getUserInfo", "()Ljava/lang/String;");

    jstring result = (jstring)env->CallObjectMethod(userData, methodId);
    return result;
}

다시 말하지만 JNIEnv *env 포인터를 사용하여 실행 중인 JVM에서 필요한 클래스, 개체, 필드 및 메서드에 액세스합니다.

일반적으로 Java 클래스에 액세스하려면 전체 클래스 이름을 제공하거나 객체 메서드에 액세스하려면 올바른 메서드 이름과 서명을 제공하면 됩니다.

네이티브 코드에서 com.baeldung.jni.UserData 클래스의 인스턴스도 생성하고 있습니다. 인스턴스가 있으면 Java 리플렉션과 유사한 방식으로 모든 속성과 메서드를 조작할 수 있습니다.

우리는 다른 모든 방법을 확인할 수 있습니다 JNIEnv의를오라클 공식 문서 .

4. JNI 사용의 단점

JNI 브리징에는 함정이 있습니다.

주요 단점은 기본 플랫폼에 대한 의존성입니다. 우리는 본질적 으로 Java의 "한 번 쓰고 어디에서나 실행" 기능을 잃게 됩니다 . 이것은 우리가 지원하고자 하는 플랫폼과 아키텍처의 새로운 조합마다 새로운 라이브러리를 구축해야 한다는 것을 의미합니다. Windows, Linux, Android, MacOS를 지원하는 경우 이것이 빌드 프로세스에 미칠 수 있는 영향을 상상해 보십시오.

JNI는 프로그램에 복잡성을 추가할 뿐만 아니라 또한 JVM으로 실행되는 코드와 기본 코드 사이에 값비싼 통신 계층을 추가합니다 . 마샬링/비정렬화 프로세스에서 Java와 C++ 간에 양방향으로 교환되는 데이터를 변환해야 합니다.

때로는 유형 간의 직접적인 변환이 없기 때문에 동등한 것을 작성해야 합니다.

5. 결론

특정 플랫폼(일반적으로)에 대한 코드를 컴파일하면 바이트코드를 실행하는 것보다 빠릅니다.

이는 까다로운 프로세스의 속도를 높여야 할 때 유용합니다. 또한 장치를 관리하는 라이브러리를 사용해야 하는 경우와 같이 다른 대안이 없는 경우에도 마찬가지입니다.

그러나 우리가 지원하는 각각의 다른 플랫폼에 대해 추가 코드를 유지해야 하기 때문에 이것은 대가를 치르게 됩니다.

그렇기 때문에 일반적으로 Java 대안이 없는 경우에만 JNI를 사용 하는 것이 좋습니다 .

항상 그렇듯이 이 기사의 코드는 GitHub에서 사용할 수 있습니다 .

Generic footer banner