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

[Java] JNI Tutorial - Fields and Methods

by 돼지왕 왕돼지 2012. 3. 28.
반응형



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

오늘은 JNI 중 Fields 과 Methods 에 관한 내용을 알아보겠습니다.

http://java.sun.com/docs/books/jni/html/fldmeth.html#11202 글을 번역 및 요약한 내용입니다.


Fields anbd Methods


Accessing Fields


- Java는 instance field 와 static field 를 제공합니다. 그리고 JNI 함수는 calss 의 instance field와 static field 활용을 위한get/set function을 지원합니다.

JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField( JNIEnv *env, jobject obj ){
   jfieldID fid;             /* store the field ID */
   jstring jstr;
   const char *str;

   /* Get a reference to obj's class */
   jclass cls = (*env)->GetObjectClass( env, obj );

   printf( "In C:\m" );

   /* Look for the instance field s in cls */
   fid = (*env)->GetFieldID( env, cls, "s", "Ljava/lang/String;");  
   if ( fid == NULL )
      return;           /* failed to find the field */

   /* Read the instance field s */
   jstr = (*env)->GetObjectFIeld( env, obj, fid );
   str = (*env)->GetStringUTFChars( env, jstr, NULL );
   if ( str == NULL )
      return ;

   printf(" c.s = \"%s\"\n", str );
   (*env)->ReleaseStringUTFChars( env, jstr, str );

   /* Create a new string and overwrite the instance field */
   jstr = (*env)->NewStringUTF( env, "123" );
   if (jstr == NULL )
      return;
   (*env)->SetObjectField( env, obj, fid, jstr );   /* Java code's s value becomes "123" */
}




Procedure for Accessing an Instance Field


native code에서 java의 instance field 접근을 위해서는 two step 이 필요합니다. 먼저 GetFIeldID 를 통해서 fieldID 를 가져옵니다. 이 때 class reference, field name, field descriptor 를 기술해줘야 합니다.

fid = (*env)->GetFieldID( env, cls, "s", "Ljava/lang/String;" );


여기 들어가는 cls 는 GetObjectClass 를 통해서 얻어옵니다. 이렇게 해당 class와 fieldID 를 얻어왔다면 다음과 같이 reference 를 얻어옵니다.

jstr = (*env)->GetObjectField( env, obj, fid );


String이나 Array 의 경우는 이렇게 Get/SetObjectField 로 접근하지만, primitive type의 경우는 GetIntFIeld 나 SetFloatField 와 같은 녀석들을 통해 접근합니다.



Field Descriptors.


"Ljava/lang/String" 이런것들은 JNI field descriptor 라고 불리는데 int 의 경우 "I", float의 경우 "F", boolean의 경우 "Z"로 표기합니다.

reference type에 대한 JNI field descriptor 는 L 로 시작하고, ; 로 끝납니다. 그리고 Java에서 .으로 구분되는 class hierarcy 는 / 로 구분됩니다. array 의 경우는 "["가 들어갑니다. 예를 들어 "[I"는 int[] 와 매핑됩니다. javap 를 이용하면 class file 의 field descriptor 를 얻을 수 있습니다. 

javap -s -p InstanceFieldAccess


<Result>

...

s Ljava/lang/String

...

 


Accessing Static Fields.

 

JNIEXPORT void JNICALL
Java_StaticFieldAccess_accessField( JNIEnv *env, jobject obj ){
   jfieldID fid;
   jint si;

   jclass cls = (*env)->GetObjectClass( env, obj );

   printf( "In C:\n" );
   
   fid = (*env)->GetStaticFieldID( env, cls, "si", "I" );
   if ( fid == NULL )
      return;

   si = (*env)->GetStaticIntField( env, cls, fid );
   printf(" StaticFieldAccess.si = %d\n", si );
   (*env)->SetStaticIntField( env, cls, fid, 200 );
}


static field 와 instance field 의 접근 차이점은 GetStaticFieldID 는 static field에 쓰이고, GetFieldID 는 instance field 에 쓰입니다. 둘의 return 값은 jfieldID 로 같습니다.



Calling Methods


JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod( JNIEnv *env, jobject obj ){
   jclass cls = (*env)->GetObjectClass( env, obj );
   jmethodID mid = (*env)->GetMethodID( env, cls, "callback", "()V" );
   if (mid == NULL )
      return;
   printf( "In C\n" );
   (*env)->CallVoidMethod( env, obj, mid );
}




Calling Instance Methods


먼저 GetMethodID 를 통해 method ID 를 얻어옵니다. 없는 함수를 접근하면 NULL 이 return 되고, NoSuchMethodError 가 throw 됩니다. 그 다음에 Call<Type>Method 를 통해 함수를 실행시킵니다. 

<Thread instance의 Runnable.run 실행시키기>

jobject thd = ...;
jmethodID mid;
jclass runnableIntf = (*env)->FindClass( env, "java/lang/Runnable" );
if ( runnableIntf == NULL )
   .../* error handling */
mid = (*env)->GetMethodID( env, runnableIntf, "run", "()V" );
if (mid == NULL)
   .../*error handling*/
(*env)->CallVoidMethod( env, thd, mid );
.../*check for possible exceptions*/


 findClass 를 통해서 interface 도 얻어올 수 있습니다. 

 

Forming the Method Descriptor


method descriptor 는 argument type 과 return type 을 명시합니다. argument type 이 먼저 오는데 이는 ( ) 안에 들어갑니다. () 안에는 separator를 사용하지 않습니다. return type 은 () 다음에 옵니다. argument 부의 Void type은 V 를 명시하지 않고 빈칸을 사용합니다. 반대로 return type 에는 void 를 V 로 표시해줍니다.

예를 들어 String을 argument 로 받고, String 을 return하는 method 가 있다면 다음과 같은 descriptor 를 사용합니다.

(Ljava/lang/String;)Ljava/lang/String; 



array 관련된 descriptor 예를 들어보겠습니다. String[] 를 argument 로 받고, void return 이라면 다음과 같은 descriptor를 사용합니다.

([java/lang/String;)V

 
 field 와 같이 javap tool 을 사용하면, JNI method descriptor 를 얻을 수 있습니다.



Calling Static Methods


Instance Method 를 호출하듯 Static Method도 호출할 수 있습니다. 다음의 두가지 step 을 통해 호출합니다.

1. GetMethodID 대신 GetStaticMethodID 를 통해 methodID 를 얻어옵니다.
2. class, methodID, 그리고 arguments 를 CallStatic<Type>Method 를 통해서 호출합니다.

Instance Method 와 Static Method 를 호출할 때 주의해야 할 것은, Instance Method Call은 두번째 argument 로 object reference 를 전달하는 반면, Static Method Call 할 떄는 두번째 argument 로 class reference 를 전달합니다. ( object reference 를 통해서도 호출할 수 있다고는 합니다. )

JNIEXPORT void JNICALL
Java_StaticMethodCall_nativeMethod( JNIEnv *env, jobject obj ){
   jclass cls = (*env)->GetObjectClass( env, obj );
   jmethodID mid = (*env)->GetStaticMethodID( env, cls, "callback", "()V" );
   if ( mid == NULL )
      return;   /* method not found */
   printf("In C\n");
   (*env)->CallStaticVoidMethod( env, cls, mid );




Calling Instance Methods of a Superclass


Superclass 에 정의되어 있지만, subclass 에는 override 되지 않은 Instance method 를 호출하는 방법이 있습니다. JNI는 CallNonvirtual<Type>Method 함수를 이 용도로 제공합니다. 다음과 같은 step 을 통해 호출 가능합니다.

1. superclass 의 reference 를 GetMethodID 에 전달하여 methodID 를 얻어옵니다.
2. object, superclass, methodID 를 CallNoncirtual<Type>Method 에 전달하여 호출합니다.



Invoking Constructors.


Constructor 호출은 instance method 호출과 매우 비슷한 방법으로 호출합니다. methodID 를 얻기 위해서 method 이름으로 "<init>" 를 전달하고, method descriptor 의 return type은 "V" 를 사용합니다. NewObject 와 같은 JNI function 에 methodID 를 전달하여 constructor 를 호출할 수 있습니다. 

jstring
MyNewString( JNIEnv *env, jchar *chars, jint len ){
   jclass stringClass;
   jmethodID cid;
   jcharArray elemArr;
   jstring result;

   stringClass = (*env)->FindClass( env, "java/lang/String" );
   if ( stringClass == NULL )
      return NULL;  /* exception thrown */
   
   /* Get the methodID for the String(char[]) constructor */
   cid = (*env)->GetMethodID( env, stringClass, "<init>", "([C)V" );
   if ( cid == NULL )
      return NULL;

   /* Create a char[] that holds the string characters */
   elemArr = (*env)->NewCharArray( env, len );
   if ( elemArr == NULL )
      return NULL;  /* exception thrown */
   (*env)->SetCharArrayRegion( env, elemArr, 0, len, chars );

   /* Construct a java.lang.String object */
   result = (*env)->NewObject( env, stringClass, cid, elemArr );

   /* Free local references */
   (*env)->DeleteLocalRef( env, elemArr );
   (*env)->DeleteLocalRef( env, stringClass );
   return result;


NewObject 함수는 Constructor 를 호출하는 데 사용됩니다. argument 로 class reference 와 methodID, 그리고 constructor 에 필요한 argument 들을 전달합니다.

JNI 는 NewString 과 같은 built-in 함수들을 제공합니다. 이것들은 빈번히 쓰이는 녀석들이며, Java의 String class 를 부르는 것보다 훨씬 효과적이라 좋습니다. 

CallNonvirtual<Type>Method 함수를 이용해서도 constructor 를 호출할 수 있습니다. 이 경우에는 먼저 AllocObject 함수를 통해 uninitialized object 를 먼저 만들고, 그 다음에 CallNonvirtual<Type>Method 를 사용해야 합니다.

result = (*env)->AllocObject( env, stringClass );
if ( result ){
   (*env)->CallNonvirtualVoidMethod( env, result, stringClass, cid, elemArr );
   /* We need to check for possible exceptions */
   if ( (*env)->ExceptionCheck( env ) ){
      (*env)->DeleteLocalRef( env, result );
      result = NULL;
   }


AllocObject 는 uninitialized object 를 만듭니다. 그리고 각 object 에 대해 constructor 는 반드시 한번만 불려야 합니다. 가끔은 AllocObject 를 이용하여 uninitialized object 를 먼저 만들고, Constructor 를 호출해야 더 유리한 경우도 있지만, 대부분은 NewObject 를 통해 Constructor 를 호출하는 것이 에러율도 적고 좋습니다.



Caching Field and Method IDs


fieldID 나 methodID 를 얻어오는 것은 이름과 descriptor 를 이용한 검색을 통합니다. 이런것을 symbolic lookup 이라고 하는데 symbolic lookup 은 매우 무거운 operation 입니다. 이 섹션에서는 어떻게 이 overhead 를 줄일 수 있는지 살펴봅니다.

overhead 를 줄이는 방법은 fieldID 와 methodID 의 cache 를 떠 놓고, 나중에 다시 이용하는 것입니다. cache 를 하는 방법은 2가지가 있습니다. fieldID 나 methodID 가 필요하여 사용하는 순간에 cache 를 하는 것이 있고, class의 static initializer에서 미리 fieldID와 methodID를 cache 하는 것입니다.



Caching at the Point of Use


다음은 fieldID 를 static variable 에 cache 를 해놓는 예제입니다.

JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField( JNIEnv *env, jobject obj ){
   static jfieldID fid_s = NULL;  /* cached field ID for s */

   jclass cls = (*env)->GetObjectClass( env, obj );
   jstring jstr;
   const char *str;

   if ( fid_s == NULL ){
      fid_s = (*env)->GetFieldID( env, cls "s", "Ljava/lang/String;" );
      if ( fid_s == NULL )
         return;  /* exception already thrown */
   }
   printf( "In C:\n");

   jstr = (*env)->GetObjectField( env, obj, fid_s );
   str = (*env) ->GetStringUTFChars( env, jstr, NULL );
   if ( str == NULL )
      return;  /* out of memory */
   printf( " c.s = \"%s\"\n", str );
   (*env)->ReleaseStringUTFChars( env, jstr, str );

   jstr = (*env)->NewStringUTF( env, "123" );
   if ( jstr == NULL )
      return;  /* out of memory */
   (*env)->SetObjectField( env, obj, fid_s, jstr );


다음은 methodID 를 cache 하는 예제입니다.

jstring

MyNewString(JNIEnv *env, jchar *chars, jint len){

   jclass stringClass;

   jcharArray elemArr;

   static jmethodID cid = NULL;

   jstring result;

 

   stringClass = (*env)->FindClass(env, "java/lang/String");

   if (stringClass == NULL) 

       return NULL; /* exception thrown */

 

     /* Note that cid is a static variable */

     if (cid == NULL) {

         /* Get the method ID for the String constructor */

         cid = (*env)->GetMethodID(env, stringClass, "<init>", "([C)V");

         if (cid == NULL) 

             return NULL; /* exception thrown */

     }

 

     /* Create a char[] that holds the string characters */

     elemArr = (*env)->NewCharArray(env, len);

     if (elemArr == NULL)

         return NULL; /* exception thrown */

     (*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);

 

     /* Construct a java.lang.String object */

     result = (*env)->NewObject(env, stringClass, cid, elemArr);

 

     /* Free local references */

     (*env)->DeleteLocalRef(env, elemArr);

     (*env)->DeleteLocalRef(env, stringClass);

     return result;

 }




Caching in the Defining Class's Initializer


사용순간에 cache 를 하는 경우는 사용시점에 cache 가 되어 있는지 항상 확인해야 합니다. 이 경우는 작은 performance 문제를 야기할 수도 있고, multi-thread 에서 사용하는 경우에는 여러 번의 cache 와 checking 이 발생할 수도 있습니다. 

그래서 보통의 경우에 fieldID 와 methodID 를 미리 cache 해 놓는 것이 좋습니다. 미리 cache 를 하는 point는 static initializer 가 좋습니다. 예를 들면 다음과 같이 말이죠.

class InstanceMethodCall{
   private static native void initIDs();
   private native void nativeMethod();
   private void callback(){
      System.out.println("In Java");
   }
   public static void main( String args[] ){
      InstanceMethodCall c = new InstanceMethodCall();
      c.nativeMethod();
   }
   static{
      System.loadLibrary("InstanceMethodCall");
      initIDs();
   }




Comparison between the Two Approaches to Caching IDs


Original Java Code 에 대한 control 을 가지고 있다면, 사용 순간에 cache 하는 것이 좋은 방안입니다. 하지만 그 외의 경우 사용 시점에 cache 를 하게 되면 다음과 같은 단점이 있습니다.

1. fieldID 나 methodID 를 사용하는 순간마다 cache 여부를 확인하고, cache 를 해야 합니다.
2. fieldID 와 methodID 가 class가 unload 되기 전까지만 유효합니다. 사용할 당시에 cache 를 하게 되면, 프로그래머는 "반드시" cachedID 를 사용하는 동안에는 class 가 unload 및 reload 되지 않도록 확실히 해주어야 합니다. 반면 static initializer 를 통해 cache가 된 경우는 cached ID 가 unloaded 및 reload 가 될 때 자동으로 re cache 됩니다.



Performance of JNI Field and Method Operations.


JNI 를 사용함에 있어 performance 는 JVM 이 JNI 를 어떻게 구현했느냐에 따라 확연히 달라집니다. 그래서 절대적인 performance 에 대해서는 말할 수 없지만, 개략적이며 공통적인 performance 에 대해서는 이야기 할 수 있습니다. 일반적으로 Java에서 native 함수를 호출하는 것이 Java에서 Java 함수를 호출하는 것보다 다음의 이유로 더 느립니다.

1. Native method 는 대부분 Java에서 Java 함수를 호출하는 것과는 다른 calling convention 을 사용합니다. 그래서 JVM 은 native method 의 entry point 로 진입하기 전에 argument 를 만들고, stack frame 을 만드는 추가 작업들이 필요합니다.
2. VM 은 inline method call 이 일반적입니다. 하지만 Java에서 native 를 호출하는 것은 inline 화 하는 것이 어렵습니다.

대략적으로 Java/Native call 은 Java/Java call 에 비해 2~3배정도 느립니다. 물론 VM 을 어떻게 구현하느냐에 따라 Java/Native call 과 Java/Java call 의 performance 가 거의 동일하도록 할 수도 있습니다.  

native/Java call 은 Java/native call 과 그 performance 가 매우 비슷합니다. 대부분 native/Java call 은 빈번하지 않기 떄문에 VM 을 구현할 때 이 부분의 optimize 는 많이 배제하는 편입니다. 그래서 많은 native/Java call 은 Java/Java call 에 비해 10배정도 느리곤 합니다.

JNI 에서 field 에 접근하는 것은 JNIEnv 를 통해 call 한다는 데 달려 있습니다. 이 녀석도 사실 조금 느리기는 하겠지만, function call 에 비해서는 매우 사소하기 때문에 무시할만합니다.



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




반응형

댓글