Java 프로젝트에서 JNI를 통해 C++ DLL 연동하기 [기본/마샬링/콜백함수]

업데이트:

Java JNI <-> C++ DLL 연동

개요

이전 포스팅에서는 C# 프로젝트에서 C++ DLL을 연동하는 방법에 대해 소개하였다면, 이번 포스팅에선 Java 프로젝트에서 C++ DLL을 연동하는 방법을 소개하겠다.

우선, C#에서 C++ 라이브러리를 사용을 쉽게 해주는 런타임 라이브러리가 존재한다.
해당 런타임 라이브러리를 이용하여 DllImport를 선언해주고, 함수와 파라미터만 서로 맞춰주면 연동이 가능하다.

하지만 Java에서는 그렇게 간단하게 연동하지 못한다.
JVM 위에서 동작하는 자바 프로그램돠 OS위에서 바이너리 파일로 바로 동작하는 C++ 프로그램을 연동하기 위해서는 중간작업이 추가로 필요하다.

이 때 사용하는게 JNI이다.

JNI의 정의는 다음과 같다.

자바 네이티브 인터페이스는 자바 가상머신(JVM)위에서 실행되고 있는 자바코드가 네이티브 응용 프로그램(하드웨어와 운영 체제 플랫폼에 종속된 프로그램들) 그리고 C, C++ 그리고 어셈블리 같은 다른 언어들로 작성된 라이브러리들을 호출하거나 반대로 호출되는 것을 가능하게 하는 프로그래밍 프레임워크


JNI를 연동하기 위한 방법으로는 다음과 같은 과정을 거친다.

  1. Java에서 DLL 연동 코드 적용
  2. javah를 이용하여 C 헤더파일 생성
  3. 생성된 헤더파일을 C++ 프로젝트 이동 후 JNI 연동함수 개발
  4. 빌드 후 확인

그럼 JNI를 통해 어떻게 연동할 수 있는지 알아보도록 하자.


JNI 기본 연동

1. Java에서 DLL 연동 코드 적용

우선 Java 코드에서 native 메소드를 선언한다.

package jniTest;
public class JNITest
{
    static
    {
        System.loadLibrary("testdll");
    }

    public native int SendMessage(String message);
    public native String ReceiveMessage(int id);

    public static void main(String[] args)
    {
        JNITest jniTest = new JNITest();
        int id = jniTest.SendMessage("Test123");
        System.out.println("msg: " + jniTest.ReceiveMessage(id));
    }
}


2. javah를 이용하여 C 헤더파일 생성

javah를 명령어로 실행하여 C 헤더파일을 생성한다.
방법은 아래와 같다.

javah.exe [패키지명].[클래스명]

javah.exe -classpath .;[경로] [패키지명].[클래스명]

위 두가지 방법 모두 사용가능하다.
성공되었으면 입력한 폴더 내 헤더파일이 생성된다.


3. 생성된 헤더파일을 C++ 프로젝트 이동 후 JNI 연동함수 개발

헤더파일은 아래와 같다.
Java에서 정의한 메소드가 C++에서 호출되는 다음의 함수로 변환이 되었다.

#include <jni.h>

JNIEXPORT jint JNICALL Java_jniTest_JNITest_SendMessage(JNIEnv *env, jobject obj, jstring message)
JNIEXPORT jstring JNICALL Java_jniTest_JNITest_ReceiveMessage(JNIEnv *env, jobject obj, jint id)


이후 선언된 해당 함수를보고 함수를 구현한다.
여기서 string의 경우 jstring과의 타입 변환이 필요하다.

#include <stdio.h>
#include "jniTest_JNITest.h"

int _id;
char* _message;
JNIEXPORT jint JNICALL Java_jniTest_JNITest_SendMessage(JNIEnv *env, jobject obj, jstring message)
{
    bool result = false;
    _message = (char*)(env)->GetStringUTFChars(message, &result);
	env->DeleteGlobalRef(message);
	return result;
}

JNIEXPORT jstring JNICALL Java_jniTest_JNITest_ReceiveMessage(JNIEnv *env, jobject obj, jint id)
{
    char* resultMsg = "";
    if(id == _id)
        resultMsg = _message;

    return (*env)->NewStringUTF(resultMsg);
}


4. 빌드 후 확인

이제 C++ 프로젝트 DLL으로 빌드한 후, DLL 파일을 자바 경로에 이동하여 자바 프로그램을 실행하면 연동이 잘 되는 것을 확인할 수 있다.


JNI 마샬링

JNI를 통한 연동은 C++에서 Java쪽으로 자바 클래스 인스턴스 구조를 만들어서 전송한다. 클래스 인스턴스의 경우, Java에서 적용 후 헤더를 생성하면 jobject 파라미터로 선언될 것이다.
그 jobject의 타입을 Java에서 찾아서 변환하여 처리한다.

package jniTest;
public class JNITest
{
    static
    {
        System.loadLibrary("testdll");
    }

    public native User SendUser(User user);

    public static void main(String[] args)
    {
        JNITest jniTest = new JNITest();
        User user = new User();
        user.name = "홍길동";
        user.age = 30;
        User changedUser = jniTest.SendUser(user);
    }
}

public class User {
	private string name;
	private int age;

}


우선 위와 같은 자바 코드를 작성한 후 헤더파일을 생성하면 아래와 같이 출력된다.

#include <jni.h>

JNIEXPORT jobject JNICALL Java_jniTest_JNITest_SendUser(JNIEnv *env, jobject obj, jobject userObj)


그러면 이 헤더 함수를 구현해보자.
파라미터로 넘어온 jobject 변수로 클래스와 필드 등의 데이터를 찾아서 jni 제공 함수로 처리할 수 있다.

#include <stdio.h>
#include "jniTest_JNITest.h"

int _id;
char* _message;
JNIEXPORT jobject JNICALL Java_jniTest_JNITest_SendMessage(JNIEnv *env, jobject obj, jobject userObj)
{
    jfieldID fid;	
    jmethodID mid;

    jclass cls = env->GetObjectClass(userObj);	
    fid = env->GetFieldID(cls,"age","Ljava/lang/String");	
    env->SetObjectField(myObj,fid, (*env)->NewStringUTF("홍길남"););	

    fid = env->GetFieldID(cls,"name","I");	
    env->SetIntField(myObj,fid, (jint)25);
    return userObj;
}


JNI 콜백함수

JNI 콜백함수도 바로 위에서 설명한 방식과 유사하게 구현한다.
Java에서 콜백 메소드를 object 변수로 넘겨받아서 Java 내 콜백함수를 찾아서 메소드ID를 생성한 후 호출하는 방식으로 동작한다.

우선 리스너 등록 함수에 인스턴스를 파라미터로 둔다.
인스턴스는 해당 클래스 인스턴스가 입력된다.

package jniTest;
public class JNITest
{
    static
    {
        System.loadLibrary("testdll");
    }

    public native int MulNumber(int num, Object instance);

    public static void main(String[] args)
    {
        JNITest jniTest = new JNITest();
        jniTest.MulNumber(20, this);
    }
}

public void MulCallback(int num) {
    System.out.println("num: " + num);
}


이후 헤더 파일이 아래와 같이 생성이 되고

#include <jni.h>

JNIEXPORT jint JNICALL Java_jniTest_JNITest_MulNumber(JNIEnv *env, jobject obj, jint num, jobject sumObj)


헤더 함수를 구현하면 다음과 같다.

#include <stdio.h>
#include "jniTest_JNITest.h"

int _id;
char* _message;
JNIEXPORT jint JNICALL Java_jniTest_JNITest_MulNumber(JNIEnv *env, jobject obj, jint num, jobject sumObj)
{
    jclass cls = env->GetObjectClass(sumObj);	
	jmethodID callbackMethod = jniEnv->GetMethodID(cls, "MulCallback", "(I)V");
    env->CallVoidMethod(sumObj, callbackMethod, num*num);
    return 1;
}


정리

위에서 소개할 때, 예제에 (I)V, Ljava/lang/String 같은 문자열이 파라미터로 입력되어있다.

이것은 C에서 JAVA의 클래스나 필드등을 찾기위한 JNI 시그니쳐라고 한다.
잠깐 살펴보면 다음과 같다.

Alt text

  • 기본 포맷은 ()V
    • 파라미터가 아무것도 없는 생성자이다.
    • 마지막 V는 void라는 뜻인 듯.
    • () 괄호 안에 생성자의 타입 signature를 구분자 없이 넣어준다.
  • 만약 생성자의 파라미터가 String, int[], float라면 (Ljava/lang/String;[IF)V
  • 만약 생성자의 파라미터가 int, int, int라면 (III)V
  • 연동에 사용되는데 필요한 선언 키워드는 다음과 같다.

이러한 방식으로 Java와 C++ DLL간 연동이 가능하다.


참고자료

태그:

카테고리:

업데이트:

댓글남기기