로딩과 링킹
package pkg;
class Cls {
native double f(int i, String s);
static {
System.loadLibrary("pkg_Cls");
}
}
Unix 계열에서는 libpkg_Cls.so 파일을 찾게 될 것.
Native 메소드 이름 결정 방식
Java_전체클래스이름_메소드이름(오버로딩 된 경우에는 "__매개변수시그니처"가 추가)
class Cls1 {
int g(int i);
native int g(double d);
}
이 경우 native 메소드 이름은 Java_pkgCls_g 가 되겠음. 클래스 이름이나 메소드 이름 등에 사용하는 맹글링 규칙은 간단한데 어차피 이름이나 타입이 숫자로 시작하는 경우가 없으므로 숫자로 시작하는 이스케이프 시퀀스 사용함.
Native 메소드 인자
항상 첫번째 인수로 JNIEnv를 받고, 두번째 인수는 non-static인 경우 객체 레퍼런스를, static인 경우 클래스 레퍼런스를 받게 됨. 그 외의 타입은 아래 표와 같이 매핑됨.
자바 / Native 타입 / 설명
boolean / jboolean / unsigned 8 bits
byte / jbyte / signed 8 bits
char / jchar / unsigned 16 bits
short / jshort / signed 16 bits
int / jint / signed 32 bits
long / jlong / signed 64 bits
float / jfloat / 32 bits
double / jdouble / 64 bits
그 외 인덱스 값이나 크기는 typedef jint jsize; 해놓고 jsize를 사용함.
레퍼런스 타입
모든 JNI 레퍼런스 타입은 jobject로 정의됨. (예를 들면 typedef jobject jclass;)
값 타입
아래와 같이 jvalue라는 공용체로 정의됨.
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;
타입 시그니처
Z (boolean), B (byte), C (char), S (short), I (int), J (long), F (float), D (double), LFQCN; (FQCN), [type (배열). L을 흔하게 쓰겠지. 뒤에 세미콜론 붙는걸 잘 봐야함. 그래서 실제 resolve된 이름은 L로 시작하고 _2로 끝나게 됨. _2가 ;니까.
long f (int n, String s, int[] arr); 이면 (ILjava/lang/String;[I)J
package pkg;
class Cls {
native double f(int i, String s);
...
}
이면 Java_pkg_Cls_f_ILjava_lang_String_2 가 되는 것임. 아래와 같이 되겠음.
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
{
/* Obtain a C-copy of the Java string */
const char *str = (*env)->GetStringUTFChars(env, s, 0);
/* process the string */
...
/* Now we are done with str */
(*env)->ReleaseStringUTFChars(env, s, str);
return ...
}
로컬 레퍼런스와 글로벌 레퍼런스
Native 메소드로 넘어온 객체는 로컬 레퍼런스. JNI 함수에서 반환하는 것도 전부 로컬 레퍼런스. 글로벌 레퍼런스는 로컬 레퍼런스를 가지고 만든다. 대부분의 경우 VM이 Native 메소드 리턴 후 로컬 레퍼런스를 해제하도록 되어있지만, 직접 처리해야 되는 경우가 있는데.. 가령 로컬 레퍼런스를 너무 많이 만들면서 작업하는 경우 자원 고갈 나지 않게 해제해가면서 해야 될 경우가 있고.. 커다란 자바 객체에 로컬 레퍼런스 만들면 GC되지 않기 때문에.. 로컬 레퍼런스는 레퍼런스를 생성한 스레드에서만 유효함.
자바 객체 접근
Native 쪽에서 primitive 잔뜩 가지고 있는 자바 객체에 접근하는건 비효율적이다. 매번 함수 호출해서 들고 와야 되는데 오버헤드가 있고.. 이걸 직접 포인터로 액세스 하려면 VM 쪽에서 Pinning을 지원해야 된다. 단점은.. GC가 지원하는 경우에만 가능하고, 메모리 레이아웃이 다른 경우 (특히 boolean array packing 등) VM 의존적이게 된다는 것. 일부분만 필요하면 API 이용해서 메모리 복사해서 들고 오던가.. Pin 하더라도 메모리 레이아웃이 다르면 Pin 하는 과정에서 메모리 복사가 발생할 수 있다. Pin 해놓은 스레드가 있는 이상 다른 스레드가 Unpin하더라도 메모리 접근 가능하지만 잠그지는 않으니 그건 Native 쪽에서 알아서 처리.
자바 메소드 호출
jmethodID mid = env->GetMethodID(cls, "f", "(ILjava/lang/String;)D");
메소드 시그니처를 위와 같이 써서 메소드ID를 얻어낸 다음,
jdouble result = env->CallDoubleMethod(obj, mid, 10, str);
이런 식으로 호출함..
GetMethodID로 ID를 얻어놓은 시점에서도 클래스가 언로딩 될 수 있음.. 따라서 클래스에 대한 레퍼런스를 확보해 놓거나 매번 메소드/필드ID를 다시 얻어와야 함.
예외 처리
Native에서 자바 메소드 호출한 경우 ExceptionOccurred()를 호출해서 예외 여부를 꼭 확인해야 함. 예외가 확인된 경우 그냥 바로 리턴하면서 예외가 caller에게 넘어가도록 하거나, ExceptionClear를 호출하고 예외 처리를 수행할 수 있음. 다른 JNI 콜 하기 전에 반드시 Clear부터 해야 함. (이 때 예외와 관련된 ExceptionOccurred, ExceptionDescribe, ExceptionClear는 예외)
JNI API
이건 굳이 다시 안 쓰고 JNI 스펙 문서 훑어보면 된다.
JVM 엠베딩 (Invocation API)
이런 것도 되는 줄은 몰랐네.. JNI_CreateJavaVM, DestroyJavaVM으로 JVM을 로딩하고 언로딩 할 수 있음.




덧글