안녕하세요 돼지왕 왕돼지입니다.
오늘은 JNI Tutorial 중 "Additional JNI Features" 에 대해 알아보도록 하겠습니다.
이 글은 http://java.sun.com/docs/books/jni/html/other.html#11202 내용을 요약 정리한 것입니다.
JNI and Threads
Constraints
Multi thread 를 사용할 경우에는 다음과 같은 경우를 주의해야 합니다.
- JNIEnv pointer 는 해당 thread 에서만 valid 합니다. 이 JNIEnv pointer 를 다른 thread 에 전달하거나, cache 하여 다른 thread 에서 사용해서는 안됩니다. JVM 에서 동일한 thread 에서의 연속적인 함수 호출에 대해서는 같은 JNIEnv pointer 를 전달하지만, 다른 thread 의 함수 호출할 경우에는 다른 JNIEnv pointer 를 전달합니다. 따라서 multi thread programming 에서는 JNIEnv 를 cache 하는 것은 실수입니다.
- Local reference 는 만든 thread 에서만 valid 합니다. 그러므로 local reference 를 다른 thread 로 전달해서는 안 됩니다. 다른 thread 에서 이 local reference 를 share 하고 싶은 경우에는 반드시 global reference 로 만들어 share 해야 합니다.
Monitor Entry and Exit.
native code 에서도 JNI function 을 써서 java 의 synchronization 을 쓸 수 있습니다. 그 타겟은 JNI의 reference 입니다. MonitorEnter 함수와 MonitorExit 함수를 사용하면 됩니다.
if ((*env)->MonitorEnter(env, obj) != JNI_OK) {
... /* error handling */
}
... /* synchronized block */
if ((*env)->MonitorExit(env, obj) != JNI_OK) {
... /* error handling */
}
현재 thread 가 어떤것도 monitor 하지 않는데 MonitorExit 함수를 호출하면 Illegal-MonitorStateException 을 던집니다.
MonitorEnter 는 MonitorExit 와 pair 를 이루어야 합니다. 주의해야 할 것은 exception 이 발생했을 때는 바로 MonitorExit 를 불러주는 것이 좋다는 것입니다.
if ((*env)->MonitorEnter(env, obj) != JNI_OK) ...;
...
if ((*env)->ExceptionOccurred(env)) {
... /* exception handling */
/* remember to call MonitorExit here */
if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;
}
... /* Normal execution path.
if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;
MonitorExit 를 제대로 call 해주지 않으면 deadlock 에 걸리기 쉽습니다. java 단에서 synchronize 를 사용하기 위해서는 synchronize 키워드를 native 함수 정의에 바로 사용해 줄 수 있습니다. 그래서 가능하다면, java 단에서 define 해주는 것이 좋습니다.
Monitor Wait and Notify
이에 상응하는 것이 JNI function 으로 제공되지는 않습니다. 대신 이것들은 callback 형태로 불러줄 수 있습니다.
Obtaining a JNIEnv Pointer in Arbitrary Contexts.
평소에는 native function call 에 JNIEnv 가 전달되어 오므로 얻어올 일이 없습니다. 하지만, 가끔 system call 의 callback 등의 형태로 불릴 때에는 JNIEnv 가 함께 전달되지 않아 곤란할 때가 있죠. 그럴 때는 다음과 같이 AttachCurrentThread 를 불러서 얻어옵니다.
JavaVM *jvm; /* already set */
f()
{
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
... /* use env */
}
이미 해당 thread 가 jvm 에 붙어 있다면, AttachCurrentThread 는 JNIEnv 만 return 해줍니다.
JavaVM 을 얻어오는 방법도 여러가지가 있습니다. 처음 VM 이 만들어졌을 때 cache 를 해놓는 방법도 있고, 미리 만들어진 JVM 에 JNI_GetCreatedJavaVMs 함수를 호출해서 얻어오는 경우, native code에서 그냥 GetJavaVM 를 호출하여 얻어오는 경우, 아니면 JNI_OnLoad handler 를 통해서도 얻어올 수 있습니다. JNIEnv와는 다르게 JavaVM 의 경우는 multi thread 에서 공유되어 사용될 수 있습니다.
Matching the Thread Models.
thread 를 다룰 때, native 것을 써야 할지, JVM 의 것을 써야 할지 고민이 되실 것입니다. 어떤 것을 써야 옳을지는 JVM 의 thread model 에 따라 다릅니다. Thread model 은 scheduling, context switching, synchronization, blocking in system call 등에 대한 중요한 구현에 대한 내용들을 담고 있습니다. native thread model 은 OS 가 관리합니다. user thread model 에서는 application code 가 thread operation 을 구현합니다. 많은 현대의 OS 는 native thread model 을 제공합니다.
사실 JVM 이 제공하는 thread model 을 따라야 하는데, JVM 이 user thread 만 제공하는 경우에는 프로그래밍 하기가 어렵습니다. 대부분의 I/O, memory allocation 등의 C library call 은 synchronization 을 기반으로 하고 있기 때문입니다.
일부 VM 은 thread model 은 프로그래머가 제공하여 쓸 수 있는 경우도 있고, 여러개의 thread model 을 제공해주어 선택하여 쓸 수도 있습니다. 따라서 새로운 JVM 을 받으면 그에 대한 document 를 참조하는 것이 중요합니다.
Writing Internationalized Code
JVM 은 string 을 unicode 로 표시합니다. GetStringUTFChars 나 GetSTringUTFRegion 함수를 jstring conversion 용도로 사용하지 않는 것이 좋습니다. platform 이 UTF string 을 native encoding 으로 사용하지 않는 한 말이죠. UTF-8 string 은 JNI 함수의 argument에 들어가는 descriptor 나 이름을 묘사하는데는 좋습니다. 그러나 file name 을 비롯한 local specific string 에는 적합하지 않습니다.
Creating jstrings from Native Strings
native string 을 jstring 으로 만들 때는 String(byte[] bytes) constructor 를 사용합니다. 다음은 native string 을 jstring 으로 바꿔주는 utility 함수입니다.
jstring JNU_NewStringNative(JNIEnv *env, const char *str)
{
jstring result;
jbyteArray bytes = 0;
int len;
if ((*env)->EnsureLocalCapacity(env, 2) < 0) {
return NULL; /* out of memory error */
}
len = strlen(str);
bytes = (*env)->NewByteArray(env, len);
if (bytes != NULL) {
(*env)->SetByteArrayRegion(env, bytes, 0, len, (jbyte *)str);
result = (*env)->NewObject(env, Class_java_lang_String, MID_String_init, bytes);
(*env)->DeleteLocalRef(env, bytes);
return result;
} /* else fall through */
return NULL;
}
Translating jstrings to Native Strings
jstring 을 native encoding 으로 바꾸려면 String.getBytes 를 사용합니다. 다음은 jstring 을 locale-specific native C string 으로 바꾸는 utility 함수입니다.
char *JNU_GetStringNativeChars(JNIEnv *env, jstring jstr)
{
jbyteArray bytes = 0;
jthrowable exc;
char *result = 0;
if ((*env)->EnsureLocalCapacity(env, 2) < 0) {
return 0; /* out of memory error */
}
bytes = (*env)->CallObjectMethod(env, jstr, MID_String_getBytes);
exc = (*env)->ExceptionOccurred(env);
if (!exc) {
jint len = (*env)->GetArrayLength(env, bytes);
result = (char *)malloc(len + 1);
if (result == 0) {
JNU_ThrowByName(env, "java/lang/OutOfMemoryError", 0);
(*env)->DeleteLocalRef(env, bytes);
return 0;
}
(*env)->GetByteArrayRegion(env, bytes, 0, len, (jbyte *)result);
result[len] = 0; /* NULL-terminate */
} else {
(*env)->DeleteLocalRef(env, exc);
}
(*env)->DeleteLocalRef(env, bytes);
return result;
}
Registering Native Methods
기본적으로 Java단에서 loadLibrary 를 call 하면 native 에 정의된 함수가 load 되고, Java단에서 native 함수를 call 하면, 정해진 name convention 을 따라 native 함수를 call 해줍니다. 하지만 이 정해진 name convention 을 따르는 것을 바꿀 수 있습니다. 프로그래머가 강제로 연결해줄 수도 있습니다.
JNINativeMethod nm;
nm.name = "g";
/* method descriptor assigned to signature field */
nm.signature = "()V";
nm.fnPtr = g_impl;
(*env)->RegisterNatives(env, cls, &nm, 1);
이것은 native 에서 호출하는 g 하는 함수를 g_impl 로 연결해주는 역합니다. 이렇게 연결된 g_impl 은 JNIEXPORT 로 declare 할 필요가 없습니다. 하지만 여전히 JNICALL 은 define 해주어야 합니다.
Load and Unload Handlers
Load, Unload handler 가 있어서 System.loadLibrary 를 통한 load 와 VM 의 native libary unload 를 지원합니다. 이 기능은 SDK 1.2 부터 지원됩니다.
The JNI_OnLoad Handler
System.loadLibrary 가 호출되면 JVM 은 다음 함수를 검색합니다.
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved);
이 JNI_Onload 에서는 어떤 JNI 함수든지 호출할 수 있습니다. 보통은 JNI_OnLoad handler 의 목적은 JavaVM pointer, class reference, 또는 method, field ID 를 cache 하는데 사용됩니다.
JavaVM *cached_jvm;
jclass Class_C;
jmethodID MID_C_g;
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *jvm, void *reserved)
{
JNIEnv *env;
jclass cls;
cached_jvm = jvm; /* cache the JavaVM pointer */
if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_2)) {
return JNI_ERR; /* JNI version not supported */
}
cls = (*env)->FindClass(env, "C");
if (cls == NULL) {
return JNI_ERR;
}
/* Use weak global ref to allow C class to be unloaded */
Class_C = (*env)->NewWeakGlobalRef(env, cls);
if (Class_C == NULL) {
return JNI_ERR;
}
/* Compute and cache the method ID */
MID_C_g = (*env)->GetMethodID(env, cls, "g", "()V");
if (MID_C_g == NULL) {
return JNI_ERR;
}
return JNI_VERSION_1_2;
}
The JNI_OnUnload Handler
JNI_OnUnload handler 는 JNI native library 가 unload 될 때 호출됩니다. JNI unload 에 대한 규칙은 다음과 같습니다.
- System.loadLibrary 를 호출한 class C 의 class loader L 이라고 할 때, class loader L 이 더 이상 live object 가 아닐 때 VM 은 JNI_OnUnload handler 를 호출하고, native library 를 unload 합니다. class loader 는 load 한 모든 class 를 참조하고 있기 때문에 이 때 C 도 역시 unload 됩니다.
- JNI_OnUnload handler 는 finalizer 에서 수행됩니다. 그리고 이것은 java.lang.System.runFinalization 을 통해 동기화하여 불릴 수 있고, VM 에 의해 비동기적으로 불릴 수 있습니다.
다음은 JNI_OnUnload handler 가 JNI_OnLoad handler 에서 할당한 resource 를 해지하는 것을 보여줍니다.
JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *jvm, void *reserved)
{
JNIEnv *env;
if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_2)) {
return;
}
(*env)->DeleteWeakGlobalRef(env, Class_C);
return;
}
여기서 MID_C_g 는 delete 하지 않아도 됩니다. C 가 unload 되면서 자동으로 해지되기 때문입니다.
여기서 class C 를 WeakReference 로 잡은 이유가 나오는데, 만약 Strong reference 로 잡고 있었다면, reference counter 가 유지되면서 class loader 를 항상 alive 상태로 유지하기 때문입니다. 그렇다면 unload 되야 할 타이밍에 unload 되지 않고 계속 유지가 되겠죠.
JNI_OnUnload 의 경우는 finalizer 에 의해 실행되므로, 어떤 thread context 에서 수행되는지 알기 어렵습니다. 그래서 복잡한 synchronization 이나 locking operation 은 피하는 것이 좋습니다. 보통 할당했던 resource 들을 해지하는 것이 대부분입니다. 마찬가지로, class loader 가 alive 하지 않으면, class loader 가 불렀던 class 들도 unload 될 가능성이 있기 때문에 cache 해놓았던 class들을 쓰는 것은 좋지 않습니다.
Reflection Support.
Reflection 을 위해 GetSuperclass, IsAssignableFrom, GetObjectClass, IsInstanceOf, FromReflectedField, ToReflectedField, FromReflectedMethod, ToReflectedMethod 등을 제공합니다.
JNI Programming in C++
C의 문법
jclass cls = env->FindClass("java/lang/String");
이 C++에서는 다음과 같이 변경됩니다.
jclass cls = (*env)->FindClass(env, "java/lang/String");
그리고 type check 가 조금 더 강화되어, ( jni.h 가 달라져서 ) jobject 와 jclass 의 구분이 가능합니다.
그 단점으로 다음과 같은 추가적 casting 도 필요합니다.
C에서는
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
C++에서는
jstring jstr = (jstring)env->GetObjectArrayElement(arr, i);
댓글