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

[Java] JNI Design Overview. ( JNI 의 전체 구조 )

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



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

이 글은 Oracle 에서 제공하는 Tutorial 문서를 번역한 것입니다.

출처 :  http://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/design.html#wp9502 

오늘은 먼저 JNI 의 전체적 구조에 대해 한번 알아볼까요?


Design Overview 

이 장에서는 JNI 의 주된 design을 집중적으로 알아보려 합니다. 대부분의 design issue 는 Native methods 와 관련이 있습니다. Invocation API 들은 5장에서 다뤄질 예정입니다.

JNI Interface Functions and Pointers

 
Native code 는 JNI function call 을 통해 Java VM 의 기능에 접근합니다. JNI function 들은 interface pointer 를 통해서 접근 가능합니다. Interface pointer 는 pointer 를 가르키는 pointer 입니다. 이 pointer 는 pointer table ( 혹은 array ) 를 pointing 하고 있고, 이 table 의 element 가 되는 pointer 들은 interface function 들을 가르키고 있죠. 모든 interface function 은 array 에 offset 을 접근자로 predefined 되어 있습니다. 다음 그림은 interface pointer 의 구조에 대해 묘사하고 있습니다.


JNI interface 는 C++ 의 virtual function table 이나 COM interface 와 비슷합니다. interface table을 사용함으로서 얻을 수 있는 이점은 JNI name space 가 native code 와 분리 될 수 있다는 것입니다. hard-wired 의 경우는 불가능한데 말이죠. Interface table을 사용하면, VM 은 여러개 버전의 JNI function table 을 쉽게 준비할 수 있습니다. 예를 들어 VM 은 다음과 같은 2개의 JNI function table을 제공할 수 있습니다.
 - 하나는 debugging에 적합한, illegal argument check table.
 - 다른 하나는 JNI spec 에 따라 요구되는 최소한의 check table.

JNI interface pointer 는 현재의 thread 에서만 유효합니다. 따라서 native method 는 다른 thread 에 interface pointer 를 "절대" 넘겨서는 안됩니다. JNI 를 사용하는 VM 은 JNI interface pointer 가 pointing 하고 있는 곳에 thread-local data 를 할당하고, 저장할 수 있습니다. 
 
Native 함수들은 JNI interface pointer 를 argument 로 받습니다. VM 은 같은 Java thread 에서 호출을 할 때 호출 횟수에 상관없이 native method 에 같은 interface pointer 를 전달합니다. 하지만, 다른 Java thread에서 native method 가 호출될 때에는 다른 JNI interface pointer 가 전달 될 수도 있습니다.



Loading and Linking Native Methods


Native 함수들은 System.loadLibrary 함수를 통해 load 됩니다. 다음 예제에서는 class 초기화 함수에서 paltform-specific native library 를 load 하는 방법에 대해 알려줍니다. 해당 library 는 native method f 가 정의되어 있다고 칩니다.

// Java
package pkg;


class Cls{
   native double f( int i, String s );
   static{
        System.loadLibrary( "pkg_cls" );
   }


System.loadLibrary 에 전달되는 argument 는 프로그래머에 의해 임의로 선택된 library 이름입니다. System 은 platform-specific 하지만, standard 한 방법으로 전달받은 library 이름을 native library 이름으로 전환해줍니다. 예를 들어 Solaris system 에서는 pkg_Cls 를 libpkg_Cls.so 로 전환하고, Win32 system 에서는 pkg_Cls 를 pkg_Cls.dll 로 변환합니다.

프로그래머는 여래 클래스에서 사용하는, 모든 native 함수들을 하나의 library 에 저장하여 사용할 수 있습니다. 그 클래스들은 물론 하나의 class loader 에 의해 로드되야 합니다. VM 은 내부적으로 load 된 native library 를 class loader 기준으로 유지합니다. Vendor 는 name crash 를 최소화하는 방향으로 native library 이름을 정해야만 합니다.

만약 사용하시는 OS 가 dynamic linking 을 지원하지 않는다면, 모든 native 함수들은 VM 에 미리 link 되어 있어야 합니다. 이 경우에는 VM이 System.loadLibrary 를 호출해도, 실제로 library load 를 하지 않고 마치게 됩니다.

프로그래머는 JNI 함수 RegisterNatives() 를 호출하여 class 와 관련된 native 함수들을 등록할 수 있습니다. RegisterNatives() 함수는 static link function 과 함께 사용하기에 매우 유용한 함수입니다.
 


Resolving Native Method Names

 
Dynamic linker 는 그 이름을 기준으로 entry 들을 연결합니다. Native 함수 이름은 다음과 함께 연결됩니다.
 - the prefix Java_
 - a mangled fully-qualified class name
 - an underscore ("_") separator
 - a mangled method name
 - overloaded native 함수는, two underscores ("__") 가 mangled argument signature 다음에 옵니다.

VM 은 연결하려고 하는 함수 이름이 native library 에도 존재하는지를 확인합니다. VM 은 처음에 short name 을 찾아봅니다. short name 은 argument signature 가 없는 함수 이름을 말합니다. 그 다음에 long name을 찾습니다. long name 은 argument signature 가 함께 있는 함수 이름이지요. Long name 은 어느 native 함수가 다른 native 함수를 overload 했을 경우에만 사용합니다. 하지만, native method 가 Java 함수와 같은 이름을 가졌다고 해도 문제가 되지 않습니다. Nonactive ( Java method ) 는 native library 에 들어 있는 녀석이 아닙니다. (??)

다음 예는 native 함수 g 가 long name 을 사용할 필요가 없다는 것을 보여줍니다. 왜냐하면 다른 g 함수는 native method 가 아니고, 이 말인즉슨 native library 에 있는 녀석이 아니기 때문입니다.

class Cls1{
   int g( int i );
   native int g( double d );


유효한 C 함수 이름으로 모든 Unicode 문자를 번역하기 위해서 name-mangling scheme 을 도입해야 합니다. "/" 대신 underscore( "_" )이 fully qualified class name 을 묘사할 때 쓰입니다. 이름이나 타입 정의자는 절대 숫자와 함께 시작되지 못합니다. 따라서 escape character 로서 _0, ~ _9 를 사용해야 하며 이 escape table 의 예는 사이트를 참조하세요.

Native 함수들과 Interface APIs 는 해당 platform 의 표준 library 호출방식을 따릅니다. 예를 들어 UNIX 시스템에서는 C calling convention 을 사용하고, Win32 시스템에서는 __stdcall 을 사용하죠.



Native Method Arguments


JNI interface pointer 는 native 함수들의 첫번째 argument 입니다. JNI interface pointer 는 JNIEnv 타입으로 되어 있죠. 두번째 argument 는 native 함수가 static 인지 nonstatic 인지에 따라 달라집니다. nonstatic native 함수의 두번째 argument 는 object 의 reference 입니다. static native 함수의 두번째 argument 는 해당 Java class 입니다.

나머지 argument 들은 일반적인 Java 함수들의 argument 와 같은 형태입니다. Native 함수를 호출하면 return value 를 통해 일반적인 call routine 으로 값을 반환합니다. 3장에서는 Java 와 C type 간의 mapping 에 대해 다룹니다.

다음 코드는 C function 을 이용하여 native 함수 f 를 구현하는 것을 보여줍니다.

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 함수 f 는 다음과 같이 정의됩니다.

package pkg;

class Cls{
   native double f( int i, String s );
   ...


우리는 항상 Java objects 를 interface pointer env 를 통해서 조작한다는 사실에 유념해야 합니다. C++ 을 이용하면 C 보다는 조금 더 깔끔하게 native code 작성할 수 있습니다.

extern "c" /* specify the C calling convention */

jdouble Java_pkg_Cls_f__ILjava_lang_String_2( JNIEnv *env, jobject obj, jint i, jstring s ){
   const char *str = env->GetStringUTFChars( s, 0 );
   ...
   env->ReleaseStringUTFChars( s, str );
   return ...


C++ 사용하면, 추가적인 indirection 과 argument 로서 전달하는 interface pointer 를 없앨 수 있습니다. 하지만 mechanism 은 C 로 하나 C++ 로 하나 동일합니다. JNI 함수들은 inline member 함수로서 정의됩니다.
 

 

Referencing Java Objects


integer, character와 같은 Primitive type들은 Java 와 native code 사이를 오갈 때 copy 가 됩니다. 반면 Java object 들은 reference 형태로 전달됩니다. VM 은 native code 로 전달된 모든 모든 오브젝트들이 GC 되지 않도록  잘 관리해야 합니다. 또한 native code 에서는 VM 에게 object 가 필요없어진 시점을 확실히 알려야 합니다. 그리고 GC 는 native code 에 의해 참조되는 녀석들을 collecting 하면 안 됩니다.



Global and Local References


JNI 는 native code 에서 사용되는 object reference 를 두개의 category 로 나눕니다. local 과 global reference 가 그것입니다. Local reference 는 native method call 동안에만 유용한 녀석으로, native method 가 return 하는 순간 자동으로 free 됩니다. Global references 들은 명시적으로 free 해주지 않는한은 계속 유지됩니다.

Native 함수들에 전달되는 Object 들은 local references 의 형태로 전달됩니다. 모든 JNI 함수들을 통해 return 되는 Java objects 들도 모두 local references 입니다. JNI 는 프로그래머들이 local reference 를 기반으로 global reference 를 만들 수 있게 해줍니다. Object 를 argument 로 받는 JNI 함수들은 local 과 global reference 모두를 받을 수 있습니다. Native 함수들은 local 혹은 global reference 를 VM 에게 결과로서 return 할 수 있습니다.

대부분의 경우, 프로그래머들은 native 함수가 return 을 하면, VM 에서 모든 local reference 를 free 하는 방향으로 만들어야 합니다. 하지만 가끔은 프로그래머가 명시적으로 local reference 를 free 하는 경우도 있지요. 다음과 같은 경우를 생각해보세요.
 - native 함수가 큰 Java object 를 이용하는 경우, Java object 를 local reference 로 만들 수 있습니다. native 함수는 caller 에게 return 하기전에 그 object 를 조작하지요. 큰 Java object 를 참조, 유지하는 local reference는 큰 Java object가 GC 되는 것을 방해합니다. 그것이 더 이상 계산에 쓰이지 않더라도 말이죠.
 - native 함수가 많은 local reference 를 만든경우, 비록 그것들이 한꺼번에 쓰이는 것이 아닌데 말이죠. VM이 일정한 양의 local reference 를 필요로 하긴 하겠지만, 너무 많은 local reference 사용은 out of memory 를 초래할 수 있습니다. 예를 들어, native 함수가 array 의 많은 object 들을 loop 를 통해 도는 경우, 그리고 그 object 들이 local reference 로 저장되는 경우, 그리고 iteration 마다 그 element 가 operation 에 쓰이는 경우를 생각해보세요. 매 iteration 후 프로그래머는 더 이상 array element 인 local reference 가 필요하지 않겠죠.

JNI 는 프로그래머들에게 손수 local reference 를 언제든 지울 수 있는 기능을 제공합니다. native 함수 안에서 언제든지 말이죠. 프로그래머가 언제든 local reference 를 free 할 수 있도록 하기 위해서, JNI 함수는 결과로서 return 되는 녀석을 제외하곤 추가적인 local reference 를 만들 수 없게 만듭니다. (??)

Local reference 는 그것들이 만들어진 thread 에서만 유효합니다. 그래서 native code 는 local reference 를 다른 thread 에 절대 넘겨서는 안 됩니다.



Implementing Local References


Local reference 를 만들기 위해서 Java VM 은 Java 에서 native 함수로 전달되는 부분에 registry 를 만듭니다. Registry 는 nonmovable local reference 를 Java object 에 mapping 합니다. 그리고 그 object 를 GC 당하지 않게 유지합니다. Native 함수로 전달되는 모든 Java object 는 ( JNI function call 을 통해 return 되는 녀석들을 비롯하여 ) 자동적으로 registry 에 등록됩니다. registry 는 native 함수가 return 을 하면 지워지며, 그 안에 등록된 entries 를 GC 당할 수 있는 상태로 만듭니다.

registry 를 만들 수 있는 다른 방법이 있습니다. table, linked list, 또는  hash table 을 이용한 방법이죠. 비록 reference counting 이 registry 에 registry 의 element들의 2중 등록이 안 되도록 하는 데 사용되곤 있지만, JNI 구현은 duplicate entries 에 대해 탐색과 삭제에 대한 책임이 없습니다.

local references 가 native stack 을 지속적으로 scan 하여 믿을만하게 구현된 것이 아닐 수도 있다는 사실에 유념해야 합니다. native code 는 local reference 를 global 또는 heap data structure 에 저장할 수도 있습니다. (??)



Accessing Java Objects


JNI 는 global 과 local reference 에 대한 풍부한 accessor 함수들을 제공합니다. 이 말인 즉슨, native 함수를 구현한 것이 VM이 내부에서 어떻게 Java objects 를 표현하는지에 상관없이 잘 작동한다는 것입니다. JNI 가 여러 종류의 VM 구현에서 널리 쓰일 수 있는 결정적 이유가 이것입니다.

opaque references 를 통한 accessor 함수들을 사용하는 것은 C data structure 를 직접적으로 사용하는 것에 비해 overhead가 큽니다.



Accessing Primitive Arrays


이 overhead 는 많인 primitive data type 들을 가지고 있는 큰 Java object 사용에 있어서는 납득할 수 없을 정도로 큽니다. 예를 들면 integer array 나  string 말이죠. ( native 함수가 vector 나 matrix 계산에 쓰인다는 것을 생각해보세요. ) function call 로 element 들을 가져오며, Java array 를 순회하는 것은 엄청나게 비효율적입니다. 

"pinning" 이라는 방법을 사용하면 native 함수가 VM 에게 array 의 내용물을 고정시키도록 할 수 있습니다. 이렇게 하면 native 함수는 element 들에 대한 직접적인 pointer 를 얻어 올 수 있는 것이지요. 하지만 이 방법은 다음의 두가지 사항을 가정하고 있습니다.
 - GC 가 pinning 을 지원해야만 한다.
 - VM 은 primitive array 를 반드시 memory 에 유지하고 있어야 합니다. 비록 이것이 대부분의 primitive array 에 대해 자연스러운 구현이지만, boolean array 는 좀 예외적입니다. 이녀석은 packed 혹은unpacked 두가지 방식으로 구현될 수 있습니다. 따라서 boolean array를 사용하는 native code 는 portable 하지 않습니다.

위의 두가지 사항을 cover 할 수 있는 협상 포인트를 우리는 얻어냈습니다.

첫째로, 우리는 primitive array elements 를 Java array 와 native memory buffer 사이에 copy 하는 함수들을 제공합니다.

두번째로, 프로그래머들은 pinned-down 된 array elements 들을 얻어올 수 있는 함수 set을 사용할 수 있습니다. 주의해야 할 것은 이 함수들이 Java VM 에게 storage allocation 과 copy를 시킨다는 것이지요. 실제로 array 를 copy 하는지는 다음 조건에 따른 VM 의 구현에 따라 다릅니다.
- 만약 GC 가 pinning 을 지원한다면, array 의 layout 은 native 함수가 예상하는 것과 같습니다. 따라서 copy 가 필요 없습니다.
- GC가 pinning 을 지원하지 않는다면, array 는 nonmovable memory block 에 복사됩니다. ( 예를 들면 C heap ). 그리고 format conversion 이 수행되지요. copy 된 녀석의 pointer 가 return 될 것입니다.

마지막으로 interface 는 VM 에게 native code 가 더 이상 array elements 를 필요로 하지 않는 다는 사실을 알리는 함수들을 제공합니다. 당신이 이 함수들을 호출하면 system 은 array 를 unpin 하거나, non-movable copy 가 되었던 것을 원래의 array 에 옮기고, copy 한 녀석은 free 시킵니다.

우리의 이런 접근법은 유연함을 제공합니다. GC algorithm은 copying 과 pinning array에 대한 각기 다른 결정을 할 수 있습니다. 예를 들어 GC 가 작은 object 는 copy 하고, 큰 object 는 pin 할 수 있다는 것이죠.

JNI 구현은 반드시 native 함수들이 multiple thread 에서 동시에 같은 array 를 접속할 수 있다는 사실을 염두하여 구현되어야 합니다. 예를 들면 JNI 는 pinned array 에 대한 내부적인 counter 를 유지하여 다른 thead 에서 pinned 된 녀석을 unpin 하지 못하도록 할 수 있습니다. 한 native 함수가 독점적으로 primitive array 에 대한 접근한다면 이에 대해 lock 을 걸 필요는 없다는 사실을 인지해야 합니다. 동시적으로 다른 thread에서 Java array 를 update 하는 행위는 예상할 수 없는 결과를 초래하곤 합니다.



Accessing Fields and Methods

 
JNI 는 native code 가 Java object 의 field 를 접근할 수 있고 method 도 호출할 수 있게 합니다. JNI는 함수와 필드를 그들의 symbolic 이름과 type signature 를 통해 인식할 수 있습니다. process 가 field 와 method iD를 그들의 name & signature 를 통해 찾아내는 작업은 2단계로 수행됩니다. 예를 들어 cls class 의 f 함수를 호출하는 것을 native code 에서 하는 것은 다음과 같습니다. 먼저 다음과 같이 method ID 를 얻어냅니다.

jmethodID mid = env->GetMethodID( cls, "f", "(ILjava/lang/String;)D");


native code 는 methodID 를 사용하여 method lookup 을 다시 할 필요없이 지속적으로 method 를 다음과 같이 사용할 수 있습니다.

jdouble result = env->CallDoubleMethod( obj, mid, 10, str );


field 나 method ID이 조회하여 variable로 유지한다 해도 VM은 그 class 를 unloading 할 수 있습니다. class가 unload 되면, method 나 field ID 는 유효하지 않습니다. 그래서 native code 에서는 반드시
- live reference 를 유지해야 하거나
- method 나 field ID 를 다시 얻어와야 합니다.
만약 method 나 field ID 를 오랜 시간동안 사용하고 싶다면 말이죠. 

JNI 는 field 나 method ID 가 내부적으로 어떻게 구현되었는지 신경쓰지 않습니다.

 
 

Reporting Programming Errors. 

JNI 는 NULL pointer 를 보낸다던지 유효하지 않은 argument type 을 보낸다던지 하는 argument 관련 error를 체크하지 않습니다. Illegal argument type 은 Java class object 대신 normal Java object를 사용하는 경우가 그 예입니다. JNI 는 다음의 이유로 이런 에러들을 체크하지 않습니다.

- JNI 함수들에게 모든 error 상황을 확인하도록 하는것은 performance 측면에서 좋지 않습니다.
- 많은 경우에, 이런 정보를 확인하기 위한 충분한 runtime type 정보가 제공되지 않습니다.

대부분의 C library 함수들은 프로그래밍 에러로부터 보호되지 않습니다. printf() 함수의 경우, 잘못된 주소를 전달 받았을 때 에러 코드를 return 하기보다는 runtime error 를 return 합니다. C library 함수들에게 모든 에러를 체크하게 하는 것은 user code에서도, library 에서도 체크하는 이중 체크를 불러올 수 있습니다.

프로그래머는 반드시 유효나 pointer 나 argument 를 JNI 함수에게 전달해야 합니다. 만약 그렇지 않다면, 특정 경우에 system state 를 꼬이게 하거나, VM 을 crash 시킵니다.



Java Exceptions


JNI 는 native 함수들이 Java exception mechanism 을 사용할 수 있도록 해줍니다. 대부분의 경우 JNI 함수들은 error code 를 return 하거나 Java exception 을 던져서 에러를 알려줍니다. Error code 는 대부분 리턴 가능하지 않은 value( out of range ) 또는 NULL 을 return 하는 방식이지요. 이 error code를 통해 프로그래머는
- error 가 발생했을 때 return 값을 확인함으로써 빨리 에러유무를 확인할 수 있습니다.
ExceptionOccurred() 함수를 호출하여, error 에 대한 자세한 정보를 담을 수 있는 exception object 를 얻을 수 있습니다.

다음 두 경우에는 프로그래머가 error code 를 확인하지 않고 exception 부터 확인해야 할 필요가 있습니다.
- Java 함수를 호출하는 JNI function이 Java method 의 결과값을 return 합니다. 프로그래머는 반드시 ExceptionOccurred() 를 호출해서 Java 함수를 수행하는 동안 exception 이 발생하지는 않았는지 확인해야 합니다.
몇몇의 JNI array access 함수들은 error code 를 반환하지 않습니다. 대신에 ArrayIndexOutOfBoundsException 이나 ArrayStroeException 을 반환합니다.

대부분의 경우, error code 가 없다고, exception 이 던져지지 않았다고 보장하는 것은 아닙니다.



Asynchronous Exceptions


여러개의 thread 를 사용하는 경우, main thread 가 아닌 경우에 asynchronous exception 을 던질 가능성이 있습니다. Asynchronous exception 은 현재 thread의 native code 의 수행에 즉각적으로 반응하지는 않습니다. 다음의 경우를 제외하곤 말이죠.
- native code 가 synchronous exception 을 던지는 JNI function 중 하나를 호출하는 경우가 아니라면
- native code 가 Exception Occurred() 를 명시적으로 불러주어 synchronous 나 asynchronous exception 을 발견하는 경우가 아니라면.

synchronous exception 을 발생시킬 수 있는 JNI function 들만이 asynchronous exception 을 체크하는 방식으로 해야 한다는 사실에 명심해야 합니다.

Native 함수들은 적당한 장소에 ( 예를 들면 tight 하게 exception check 없이 도는 loop ) ExceptionOccurred() 코드를 추가해서 현재 thread 가 asynchronous exception 에 충분한 시간동아반응하도록 해야 합니다.



Exception Handling


native code 에서 exception 을 처리하는 2가지 방법을 소개합니다.
- native 함수들은 바로 return 할지를 결정 할 수 있습니다. 그래서 native 코드를 호출한 Java part에서 바로 던져진 exception 을 확인할 수 있습니다.
- native code 는 ExceptionClear() 함수를 통해서 발생한 exception 들을 clear 할 수 있습니다. 그리고선 자신의 exception-handling code 를 수행할 수 있습니다.

exception 이 발생하면, native code 는 반드시 다른 JNI call 을 하기 전에 exception 을 clear 해줘야 합니다. pending 된 exception 이 있을 때, JNI 함수가 안전하게 호출할 수 있는 함수들은 다음과 같습니다.

- ExceptionOccurred()

- ExceptionDescribe()

- ExceptionClear()

- ExceptionCheck()

- ReleaseStringChars()

- ReleaseStringUTFChars()

- ReleaseStringCritical()
- Release<Type>ArrayElements()
- ReleasePrimitiveArrayCritical() 
- DeleteLocalRef()
- DeleteGlobalRef()
- DeleteWeakGlobalRef()
- MonitorExit()
- PushLocalFrame()
- PopLocalFrame() 



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




반응형

댓글