본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Java] JNI Tutorial - Additional JNI Features

by 돼지왕 왕돼지 2012. 4. 5.
반응형



안녕하세요 돼지왕 왕돼지입니다.

오늘은 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 해주어야 합니다.


RegisterNatives 를 해주는 이유는 다음과 같습니다.
- 여러개의 native method 를 먼저 마구 구현하는 것이 VM 과 링크해서 천천히 하나하나 구현하는 것보다 편할 때가 있습니다.
- 함수 연결을 runtime 에 바꾸고 싶을 때 사용될 수 있습니다.
- native app 이 VM 을 embed 할 때 특히 유용합니다. 왜냐하면 JVM 은 lib 을 통해 보통 search 를 하지, application 자체를 search 하지 않기 때문입니다.





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);




도움이 되셨다면 손가락 꾸욱~

 



반응형

댓글