Sep 19 2008

JNI 를 통해 사용하는 VC++ 라이브러리를 디버깅하기

분류: Lang.Java 태그: ,, , , , , Heart @ 4:21 오후

Trackback : http://dev.heartsavior.net/archives/205/trackback/

JNI를 사용하는 프로젝트 수행 중에 JNI 단독으로 테스트할 때는 일어나지 않는 오류가 계속 일어나길래, JNI 가 사용하는 VC++ 라이브러리 내부를 디버깅하는 방법을 찾아 보았다.

@ Java Native Interface (JNI) - Debugging C code in a dll

설명이 잘 되어있긴 한데, 정리는 좀 덜 되어 있어서 실제로 디버깅 테스트를 해 보고 절차를 정리해 보았다.

IDE는 Visual C++ 6 를 기준으로 설명한다. 다만, 설정해야 할 부분이 별로 없으므로 아래의 설정은 타 IDE에도 적용이 가능할 것 같다.

1. Project Settings 에서 JNI 구현이 되어 있는 프로젝트를 선택함
2. Settings For: 를 Debug로 맞춘 다음(혹은 Debug 옵션들이 선택된 Custom Configuration도 상관없음) Debug 탭 선택
3. Executable for debug session: JRE 의 java.exe 나 javaw.exe 를 선택
4. Working directory 에 JNI를 사용하는 프로젝트의 root 디렉토리를 전체 경로로 기록
(Eclipse 의 경우 debug / run 시에 해당 디렉토리를 Working directory 로 사용하므로 그에 맞춰주는 것임)
5. Program arguments 에
-Xrunjdwp:transport=dt_socket,server=y,address=<Eclipse 에서 원격 디버깅을 붙을 port>,suspend=n -classpath <jar파일들;class 디렉토리들(Eclipse라면 bin)> <Main 클래스 경로(패키지.클래스명)> <main() 의 arguments> 기록
(classpath 를 적을 때 Working directory 의 상대 경로로 적어도 된다.)

* 중요 포인트 : JNI 라이브러리를 로드하는 부분( System.loadLibrary() 나 System.load() ) 에서 IDE에서 빌드하는 결과 파일을 로드하도록 해 주어야 한다.
참고로, 라이브러리 경로를 절대 경로로 표현하려면 System.loadLibrary() 대신 System.load() 를 사용하여야 하고, 이 때는 경로에 확장자도 붙여 준다.

디버그할 때는 VC++ 에서 Configuration 을 위에서 설정한 대상(Debug) 으로 맞추고 F5를 눌러 실행하면 JVM이 실행된 Console 이 뜬다.  VC++ 에서 break 걸리면 VC++ 디버거 사용 방법대로 디버깅 하면 된다.

VC++ 6 에서 실행할 때 native method 를 호출하는 위치까지는 진행되어야 breakpoint라도 걸린다. (F11로 실행해 봐야 JVM Disassembly 가 뜨기 때문에 별 도움이 안됨.)

위와 같은 방법으로 JNI 를 통해 호출되는 라이브러리를 디버깅할 수 있다.

—-

다음으로, 양 쪽으로 모두 디버그 하는 방법을 알아보자. (Eclipse 3.3 기준)

VC++ 6 은 위와 설정이 같다.

Eclipse 에서는 아래와 같이 설정한다. (다른 IDE를 쓴다면 해당 IDE 가 원격 디버깅하는 방법대로 하면 됨)

1. Open Debug Dialog 를 실행한다.
2. Remote Java Application 을 선택하고 New 를 선택한다.
3. Main 탭에서 Project 를 선택하고, Connection Properties 에서 Port 를 VC++ 에서 설정한 port 를 적는다.

해당 Debug Configuration 을 실행하면 VC++ 가 실행한 JVM 에 원격 디버거로 붙게 된다.

주의해야 할 점은, 프로그램 로직이 짧으면 Eclipse 가 연결하려는 시도 중에 프로그램이 끝나버린다.
또한, VC++ 에서 break 가 걸린 상태에 Eclipse에서 연결을 시도하면, VC++ 에서 break 풀릴 때까지 Eclipse 는 연결이 안되는 것 같다. 즉, 연결에 대한 처리가 delay 되는 것 같다. (확실하지는 않음. 혹시 아시는 분은 리플로 알려주시면 감사하겠습니다.)

양 쪽으로 디버깅할 때에는, Java 프로그램이 시작할 때 native 의 초기 진입점(native method) 을 바로 실행하도록 하면 양쪽으로 디버그를 걸기가 좀 편하다.
초기 진입점에 breakpoint 걸고, VC++에서 멈춰져 있는 상태가 되면 Eclipse 에서 원격 디버거로 연결하고, VC++ 에서 바로 F5를 눌러 초기 진입점을 빠져나가면 (뭐 이리 복잡하냐 -_-) 이후로는 양쪽으로 breakpoint 에서 멈추게 된다.

어쨌든 절차도 복잡하고… 가장 좋은 방법은 JNI 로 사용할 라이브러리를 ‘디버깅할 일이 없도록’ 하는 것이겠다.

ps. 특정 IDE 에서 컴파일해야 하는 게 아니라면 Eclipse 의 CDE를 통해 Eclipse 내부에서 북치고 장구치는 것도 가능하다.

@ Integrated Debugger for Java*/JNI Environments


Dec 10 2006

JNI를 사용하여 JAVA에서 C/C++의 라이브러리를 사용

분류: Lang.Java 태그: ,, , Heart @ 3:39 오후

Trackback : http://dev.heartsavior.net/archives/122/trackback/

연구실에 잠깐 몸담았을(?) 때 교수님 요청으로 교수님의 색인어 추출기(C 버전)를 Java에서 호출할 수 있도록 만들어 보았다.
그 이후로 소스 코드를 잃어버려서 정리해두지 못했는데, 다행히도 1년 만에 소스 코드를 받아서 정리해보려 한다.

작업 환경은 J2SE 1.5(5.0) & Visual C++ 6.0 이었고, 따로 필요한 건 없다.

필요한 이론적 정보들은 검색엔진에서 쉽게 찾을 수 있을 것이므로 여기서는 간단하게 순서와 소스 코드를 나열하는 것으로 설명을 대신한다.(교수님 라이브러리가 직접적으로 나타나는 부분은 라이센스 문제로 삭제한다.)
1. C/C++과의 인터페이스를 자바 소스 코드에 삽입하고, 필요한 부분에서 이를 호출하도록 프로그래밍 한다.

package nlp.ham;
 
public class INDEXCaller {
	/**
	 * KLT2000 라이브러리로부터 색인어 추출 기능을 이용하여 자바에서 문장을 넘겼을 때
	 * 추출된 색인어와 그 빈도수를 리턴하는 함수
	 *
	 * @param s            색인어를 추출할 문장
	 * @param resLimit    결과값의 최대 갯수(메모리 할당 문제)
	 * @param resWord     추출된 워드 배열 - resLimit보다 커야 함!!
	 * @param resFreq     추출된 워드의 빈도수 배열 - resWord와 같아야 함!!
	 */
	public native void getIndexWord(String s, int resLimit, String [] resWord);
 
	static {
		System.loadLibrary("KLT2000");
		System.loadLibrary("INDEXCaller");
	}
}

JNI의 함수 프로토타입은 간단하다. public native void 까지 맞추어 주면 되며, 라이브러리에서의 리턴 값은 OUT 파라미터 형식으로 받으면 된다.public native 까지 맞추어 주면 되며, 리턴 값을 명시할 수 있다.(물론, OUT 파라미터 형식으로 받아도 된다)

인터페이스가 되는 부분은 INDEXCaller.dll이고, 그 안에서 KLT2000.dll을 사용하여야 하기 때문에 두 라이브러리를 모두 읽어들였다.

라이브러리 명을 알고 있어야 하므로, 1번을 수행한 다음 라이브러리를 작성하려 한다면 미리 정해 두는게 좋다.

2. 만든 Java 소스 코드에서 C/C++ 헤더 파일을 추출한다.

javac -d . INDEXCaller.java;

javah -jni nlp.ham.INDEXCaller

1. 에서 만든 Java 소스 코드를 컴파일 한 다음, javah 를 이용하여 헤더 파일을 만든다.
nlp_ham_INDEXCaller.h 라는 헤더 파일이 만들어진다. (파일명 규칙이 간단하다는 것을 알 수 있다.)

만들어진 헤더 파일은 이렇게 되어 있다.

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni .h>
/* Header for class nlp_ham_INDEXCaller */
 
#ifndef _Included_nlp_ham_INDEXCaller
#define _Included_nlp_ham_INDEXCaller
 
#ifdef __cplusplus
extern "C" {
#endif
 
/*
 * Class:     nlp_ham_INDEXCaller
 * Method:    getIndexWord
 * Signature: (Ljava/lang/String;I[Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_nlp_ham_INDEXCaller_getIndexWord
	(JNIEnv *, jobject, jstring, jint, jobjectArray);
 
#ifdef __cplusplus
}
 
#endif
#endif
 
</jni>

3. C/C++ 라이브러리 프로젝트에 헤더 파일을 추가하고 이에 맞는 구현을 추가한다.

구현 부분은 소스의 맨 아래 부분에 위치해 있다.
앞에서도 밝혔듯이 교수님 라이브러리 부분을 모두 삭제하여 코드 내용의 의미는 전혀 없으므로 틀만 참고하자.

// INDEXCaller.cpp : Defines the entry point for the DLL application.
 
#include "stdafx.h"
#include <stdlib .h>
#include "nlp_ham_INDEXCaller.h"
 
////////////////////////////////////////////////////////////////////////////////////////
// 자바< ->C++의 한글 문제 해결 메소드들
 
char *jbyteArray2cstr( JNIEnv *env, jbyteArray javaBytes );
jbyteArray cstr2jbyteArray( JNIEnv *env, const char *nativeStr);
jbyteArray javaGetBytes( JNIEnv *env, jstring str );
jbyteArray javaGetBytesEncoding( JNIEnv *env, jstring str, const char *encoding );
jstring javaNewString( JNIEnv *env, jbyteArray javaBytes );
jstring javaNewStringEncoding(JNIEnv *env, jbyteArray javaBytes, const char *encoding );
 
static jclass class_String;
static jmethodID mid_getBytes, mid_getBytesEncoding;
static jmethodID mid_newString, mid_newStringEncoding;
 
char *jbyteArray2cstr( JNIEnv *env, jbyteArray javaBytes )
{
	size_t len = (*env).GetArrayLength(javaBytes);
	jbyte *nativeBytes = (*env).GetByteArrayElements(javaBytes, 0);
	char *nativeStr = (char *)malloc(len+1);
	strncpy( nativeStr, (char *)nativeBytes, len );
	nativeStr[len] = ‘\0;
	(*env).ReleaseByteArrayElements(javaBytes, nativeBytes, JNI_ABORT);
	return nativeStr;
}
 
/* C 문자열로부터 자바 바이트 배열을 생성하여 반환 */
jbyteArray cstr2jbyteArray( JNIEnv *env, const char *nativeStr)
{
	jbyteArray javaBytes;
	int len = strlen( nativeStr );
	javaBytes = (*env).NewByteArray(len);
	(*env).SetByteArrayRegion(javaBytes, 0, len, (jbyte *) nativeStr);
	return javaBytes;
}
 
/* 자바 스트링을 디폴트 인코딩의 자바 바이트 배열로 변환.
* String 클래스의 getBytes() 메쏘드를 호출한다. */
jbyteArray javaGetBytes( JNIEnv *env, jstring str )
{
	if ( mid_getBytes == 0 )
	{
		if ( class_String == 0 )
		{
			jclass cls = (*env).FindClass("java/lang/String");
			if ( cls == 0 ) return 0;  /* 오류 */
			class_String = (_jclass *)((*env).NewGlobalRef(cls));
			if ( class_String == 0 ) return 0;  /* 오류 */
		}
		mid_getBytes = (*env).GetMethodID(class_String, "getBytes", "()[B");
			if (mid_getBytes == 0) return 0;
	}
	/* str.getBytes(); */
 
 
	return (_jbyteArray *)((*env).CallObjectMethod(str, mid_getBytes));
}
 
/* 자바 스트링을 지정된 인코딩 `encoding'의 자바 바이트 배열로 변환.
* String 클래스의 getBytes(String encoding) 메쏘드를 호출한다. */
jbyteArray javaGetBytesEncoding( JNIEnv *env, jstring str, const char *encoding )
{
	if ( mid_getBytesEncoding == 0 )
	{
		if ( class_String == 0 )
		{
			jclass cls = (*env).FindClass("java/lang/String");
			if ( cls == 0 ) return 0;  /* 오류 */
			class_String = (_jclass *)((*env).NewGlobalRef(cls));
			if ( class_String == 0 ) return 0;  /* 오류 */
		}
		mid_getBytesEncoding = (*env).GetMethodID(class_String, "getBytes", "(Ljava/lang/String;)[B");
		if (mid_getBytesEncoding == 0) return 0;
	}
	/* str.getBytes( encoding ); */
	return (_jbyteArray *)((*env).CallObjectMethod(str, mid_getBytesEncoding, (*env).NewStringUTF(encoding)));
}
 
/* 디폴트 인코딩의 자바 바이트 배열을 자바 스트링으로 변환.
* String 클래스의 new String(byte[] bytes) 메쏘드를 호출한다. */
jstring javaNewString( JNIEnv *env, jbyteArray javaBytes )
{
	if ( mid_newString == 0 )
	{
		if ( class_String == 0 )
		{
			jclass cls = (*env).FindClass("java/lang/String");
			if ( cls == 0 ) return 0;  /* 오류 */
			class_String = (_jclass *)((*env).NewGlobalRef(cls));
			if ( class_String == 0 ) return 0;  /* 오류 */
		}
		mid_newString = (*env).GetMethodID(class_String, "<init>", "([B)V");
			if ( mid_newString == 0 ) return 0;
	}
	/* new String( javaBytes ); */
	return (_jstring *)((*env).NewObject(class_String, mid_newString, javaBytes ));
}
 
/* 지정된 인코딩 `encoding'의 자바 바이트 배열을 자바 스트링으로 변환.
* String 클래스의 new String(byte[] bytes, String encoding)
* 메쏘드를 호출한다. */
jstring javaNewStringEncoding(JNIEnv *env, jbyteArray javaBytes, const char *encoding )
{
	jstring str;
	if ( mid_newString == 0 )
	{
		if ( class_String == 0 )
		{
			jclass cls = (*env).FindClass("java/lang/String");
			if ( cls == 0 ) return 0;  /* 오류 */
			class_String = (_jclass *)((*env).NewGlobalRef(cls));
			if ( class_String == 0 ) return 0;  /* 오류 */
		}
		mid_newString = (*env).GetMethodID(class_String, "</init><init>", "([BLjava/lang/String;)V");
			if ( mid_newString == 0 ) return 0;
	}
	/* new String( javaBytes, encoding ); */
	str = (_jstring *)((*env).NewObject(class_String, mid_newString, javaBytes, (*env).NewStringUTF(encoding) ));
	return str;
}
 
// 여기까지
////////////////////////////////////////////////////////////////////////////////////
 
BOOL APIENTRY DllMain( HANDLE hModule,
		DWORD  ul_reason_for_call,
		LPVOID lpReserved  )
{
	return TRUE;
}
 
/*
* Java_nlp_ham_HAMCaller_getIndexWord
* Java로부터 문장을 받아 색인어 추출기를 이용하여 색인어를 추출하고 이를 Java로 다시 리턴하는 함수
*
* <parameters>
* str - Java로부터 전달되는 문장
* limitRes - 한계 결과값
* resWord - 추출된 색인어 배열
*/
JNIEXPORT void JNICALL Java_nlp_ham_INDEXCaller_getIndexWord(JNIEnv * env, jobject obj,
		jstring str, jint limitRes, jobjectArray resWord) {
 
	char * str_to_cpp = NULL;
 
	// String을 cpp 형식의 char * 로 변환
	str_to_cpp = jbyteArray2cstr(env, javaGetBytes(env, str));
 
	unsigned char * Term[MAXKEYWORDS];
 
	unsigned char * temp = new unsigned char [strlen(str_to_cpp) + 1];
	strcpy((char*)temp, str_to_cpp);
 
	for( unsigned int i = 0; i < nKeyword; i++)
	{
		// resWord[i]에 Term 저장
		jstring iArray = javaNewString(env, cstr2jbyteArray(env, (char *)Term[i]));
		(*env).SetObjectArrayElement(resWord, i, iArray);
	}
 
	delete [] temp;    // memory를 반환
 
	//delete [] Term;
	//delete TM;
 
	delete [] str_to_cpp;
}

안타깝게도 그 당시 Java Collection을 읽고 쓰는 방법은 찾아내지 못하였다. 아마 안되는 게 아닌가 싶다.
(방법을 알고 계시면, 혹은 찾으셨다면 덧글로 남겨 주시기 바랍니다. ^^;;)

4. 완성하였다면 빌드하여 DLL 라이브러리 파일을 만든다.
과정은 컴파일러에 따라 다르므로 생략…

5. 만들어진 DLL 파일을 classpath로 가지고 온다.
예제의 경우에는 JNI로 연결되는 DLL 파일이 내부에서 다른 DLL 파일을 참조하므로 2 개의 DLL 파일을
classpath로 복사하여야 한다. 복잡하지 않게 Java 프로젝트 빌드 결과의 루트로 위치시키는 것을 추천한다.

6. 끝. (기존 Java 어플리케이션 프로그램과 동일하게 빌드하고 사용하면 된다.)

———————————————————————————————–
주의할 점
1. 함수의 형식을 정확히 일치시키지 않으면 호출 시에 java.lang.UnsatisfiedLinkError 가 발생한다.
Exception이 아니라 Error라는 것에 유의하자.
2. native method 내에서 에러가 발생하면 JVM까지 죽어버리므로, 라이브러리를 신경써서 구현하자.