2015년 4월 7일 화요일

03. JNI의 이해

* 위 블로그는 스터디를 정리한 것이므로 참고용으로만 사용하시길 바랍니다.

    1. JNI란

    • Java Native Interface의 약어로 자바코드가 자바VM에서 동작할 때, 네이티브 언어로 작성된 코드를 자바에서 사용하거나, 네이티브 언어에서 자바의 클래스와 메서드를 사용할 수 있게 해주는 프로그래밍 인터페이스이다.
    • 여기서 네이티브란 원시 코드라는 의미로 C/C++ 또는 어셈블리어 이다.
    • JNI로 작성된 네이티브 코드는 달빅 VM을 거치지 않고 CPU가 바로 실행한다.
    • JNI는 라이브러리가 아니라 인터페이스 이므로 C/C++과 자바 사이의 함수나 메서드 호출에 문제만 없게한다.
    • 인터페이스 규칙에 따라서 자바의 메서드와 C/C++의 함수를 정의함으로써 자바와 C/C++ 서로 간에 함수를 호출할 수 있게 된다.
    • 자바가 제공하는 플랫폼 이식성을 잃게된다.
    • 자바의 기능 중 하나인 type checking, garbage collection과 같은 안정성을 잃게된다. 
    • 자바에서 네이티브 메서드를 사용하려면 동적 라이브럴리들을 호출해야 한다. 이는 자바 시큐리티 매니저에 반대되는 연산이다.
    • 코드 자체가 방해물이 될 수 있다. (타이핑오류나 컴파일 오류를 종종 겪게 된다.)

    2. JNI 문법 

        A. 함수 이름은 반드시  <반환값>_Java_<패키지명>_<클래스명>_<메서드명> 이어야 한다.
        B. 함수 인수의 첫 번째는 꼭 JNIEnv여야 하며, 두 번째는 jobject여야 한다.
        C. cpp로 작성하였다면 extern "C"를 선언해야한다.
        D. JNIEnv* env와 jobject thiz는 JNI로부터 받은 변수 이므로 함수 인수에 꼭 사용해야 한다.
        E. 포인터 env는 JNI에서 가장 중요한 포인터로, 자바 VM에 대한 인터페이스를 포함하는 구조체의 포인터이다.
        F. 포인터 env에는 JNI의 환경 정보가 포함되어 있고, 자바 VM과의 상호작용 및 자바 객체와의 연계에 필요한 모든 기능을 포함한다.
        G. 변수 thiz에는 포함된 클래스의 정보가들어 있다. 이 예제에서 thiz는 stringFromJNI() 함수를 포함하는 HelloJni의 클래스를 참조한다.
        H. 만약 인수가 추가된다면 env와 변수 thiz 다음에 선언하여 사용한다.
         I. 라이브러리 Symbol은 항상 C 형식으로 있어야한다. C++로 작성했다면 extern "C"로 함수를 감싸서 C 형식으로 변경해야한다.
        J. c++ 형식일 경우env 포인터를 이용하여 멤버 함수를 호출할 때 멤버 함수에 env포인터를 전달 하지 않아야한다.

[네이티브 C코드 hello-jni.c]
#include 
#include 

jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env, jobject thiz )
{
#if defined(__arm__)
  #if defined(__ARM_ARCH_7A__)
    #if defined(__ARM_NEON__)
      #define ABI "armeabi-v7a/NEON"
    #else
      #define ABI "armeabi-v7a"
    #endif
  #else
   #define ABI "armeabi"
  #endif
#elif defined(__i386__)
   #define ABI "x86"
#elif defined(__mips__)
   #define ABI "mips"
#else
   #define ABI "unknown"
#endif

    return (*env)->NewStringUTF(env, "Hello from JNI !  Compiled with ABI " ABI ".");
}

[네이티브 C++코드 hello-jni.cpp]
extern "C"
{
jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env, jobject thiz )
{
#if defined(__arm__)
  #if defined(__ARM_ARCH_7A__)
    #if defined(__ARM_NEON__)
      #define ABI "armeabi-v7a/NEON"
    #else
      #define ABI "armeabi-v7a"
    #endif
  #else
   #define ABI "armeabi"
  #endif
#elif defined(__i386__)
   #define ABI "x86"
#elif defined(__mips__)
   #define ABI "mips"
#else
   #define ABI "unknown"
#endif

    return env->NewStringUTF(env, "Hello from JNI !  Compiled with ABI " ABI ".");
}
}

    3. 자바에서 JNI 호출 

        A. 호출하는 함수 앞에 native를 붙인다
        B. loadLibrary() 함수로 라이브러리를 호출한다.
        C. 라이브러리의 이름에서 Lib와 .so는 붙이지 않는다.
public class HelloJni extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        
        TextView  tv = new TextView(this);
        tv.setText( stringFromJNI() );
        setContentView(tv);
    }
   
    public native String  stringFromJNI();
    
    public native String  unimplementedStringFromJNI();
    
    static {
        System.loadLibrary("hello-jni");
    }
}

    4. JNI 자료형 선언 

        A. 자바에서 선언한 변수 자료형에 따라서 네이티브 코드에서 사용하는 변수와 이름이 달라진다.
        B. 네이티브 코드에서 int나 char와 같은 자료형이 사용될 때 자바에서 건너온 자료형이라는 것을 명시하기 위해서 자료형 앞에 j를 붙인다.
        C. 자바에서 건네받은 int는 jint가 된다. 자바에서 선언한 배열은 변수 자료형 뒤에 Array라는 문자가 붙는다. 예를 들어 자바의 char[]는 네이티브 코드에서 jcahrArray가 된다. jcharArray와 같은 배열을 네이티브코드에서 사용하려면 JNI의 변환 함수를 이용해야한다.
        D. Class -> jClass
        E. Throwable -> jThrowable

jint Java_com_example_hellojni_HelloJni_AddValue(JNIEnv* env, jobject thiz, jint a, jint b) 
{
    return a+b;
}


       

     5. JNI 배열

            A. C언어에서 배열은 연속한 메모리에 값이 설정되지만, 자바의 배열은 메모리가 연속해서 존재하지 않는다.
            B. 자바 배열을 C 배열에 맞게 변환해야 JNI에서 사용이 가능하다.
            C. C/C++에서 자바 배열에 접근하려면  Get<Type>ArrayElements() 함수를 사용하여 자바 배열을 C 형식의 배열로변환해야한다.
            D. 위의 함수는 자바 배열에 접근할 수 있는 메모리를 확보하고, 확보한 메모리의 포인터를 반환한다.
            E. 자바 배열의 사용이 끝나면 Release<Type>ArrayElements() 함수로 확보된 메모리 영역을 해제한다.

jint *carr; 
carr = )*env_->GetIntArrElements(env, arr, NULL);
//arr은 인자로 넘겨받음

//Do something...

(*env)->ReleaseIntArrayElements(env, arr, carr, 0);
 

   6. JNI에서 자바 클래스와 함수

            A. JNI의 리플랙션을 이용하여 C/C++에서 자바 클래스와 함수에 접근한다.
            B. 리플랙션이란 애플리케이션이 실행될 때 자바 클래스에 접근할 수 있는 기능으로, 애플리케이션이 실행될 때 자바의 클래스나 메서드를 지정하여 애플리케이션의 유연성을 높일 수 있다.
            C. 실행과정은 다음과 같다
                (1) Class 찾기 - findClass() 또는 GetObjectClass()함수
                (2) 함수 ID 찾기 - 크래스에서 함수 이름과 인수 지정
                (3) 함수 호출

       6.1 Class 찾기 

            A. C/C++에서 자바의 클래스나 함수를 호출하려면 먼저 사용하고자 하는 Class를 찾아야한다. 이를 위해서 JNIEnv에서 FindClass(), GetObjectClass()함수를 제공한다.

       6.2 함수(Method) ID 또는 변수(Field) ID 찾기

            A. JNIEnv 에서는 GetMethodID()GetStaticMethodID() 함수를 제공한다.
            B. 함수를 찾으려면 시그니처(Signature)의 조합을 입력해야한다.
            C. 시그니처는 JNI에 존재하는 독특한 형태로 함수 이름, 인수, 반환값을 지정하여 유일한 함수로 만들 때 사용된다.



            D. int는 I로 정의되는것을 알 수 있다. 다만 클래스는 조금 특별한 문법을 가진다.
            E. 패키지를 포함한 클래스의 전체이름을 온점(.) 대신 빗금(/) 구분자로 변경한다. 그리고 제일 앞에는 'L'을 붙이고 제일 마지막에는 쌍반점(;)을 붙인다. 예를들어 'java.lang.String' -> 'Ljava/lang/String;'과 같이 변한다.

       6.2 함수 호출

            A. 클래스와 함수 ID를 찾았으면 Call<type>Method() 또는 CallStatic<type>Method()함수를 이용해서 해당 함수를 사용한다.

           [자바 코드]
private void func() {System.out.println("Hello");}


           [C 코드]

jclass cls = (*env)->GetObjectClass(env, obj);
jmethodID funID = (*env)->GetMethodID(env, cls, "func", "()V");
(*env)->CallVoidMethod(env, obj, funID);

           B. 함수이름이 func이므로 GetMethodID의 3번째 인수로 'func'를 설정한다.
           C. 그리고 func()함수의 반환형은 void이고 인수를 사용하지 않으므로 ()이다.
           D. 자바의 멤버 변수도 리플렉션을 이용하면 C/C++에서 접근가능하다.

 7. 대용량 메모리 전달

            A. 자바에서 C/C++ 메모리를 전달할 때 배열을 사용한다.
            B. 이때 단순한 문자열 배열에서 부터 대용량의 이미지 데이터 배열을 전달하게 된다.

            C. 배열을 이용하면 작은 크기의 데이터에서는 속도가 떨어지지않지만, 대용량의 데이터에서는 속도가 떨어지게된다. 이는 자바 배열을 C/C++ 배열로 변환해야 하는데, 변환에 필요한 메모리를 확보하고 메모리를 복사하는 등 메모리를 변환하는 데 많은 시간이 걸리기 때문이다.

            D. 이럴 때에는 java.nio.Buffer 클래스를 이용하여 속도 문제를 해결할 수 있다.
            E. java.nio.Buffer클래스는 C/C++로 데이터를 전달할 때 배열과 같은 변환이 일어나지 않으므로 속도 저하가 거의 없다.



[자바코드]
void Java func(byte [] pixels) {
ByteBuffer buf = ByteBuffer.allocateDirect(10);
buf.order(ByteOrder.LITTLE_ENDIAN); //리틀 엔디안으로 변환
buf.put(pixels);
setBuffer(buf);
buf.get(pixels);
}

            F. 자바에서 allocateDirect() 함수를 사용하여 대용량 메모리를 확보하고, 해당 메모리를 리틀엔디안으로 변환한다.
            G. 자바는 기본적으로 빅 엔디안을 사용하고, 달빅 또는 ART는 리틀엔디안을 사용하므로 엔디안을 변환하지 않고 그대로 네티이트코드에 전달하면 예상하지 못한 겨로가같 나타난다. 따라서 엔디안 변환은 상당히 중요하므로 빼먹지 말아야 한다.

[C 코드]
void setBuffer(JNIEnv * env, jobject thiz, jobject buf)
{
    char* pixel = reinterpret_cast  (env-> GetDirectBufferAddress (buf));
    pixel[0] = 0; //데이터 가공
}


            H. C코드에서는 대용량 메모리를 전달받고, GetDirectBufferAddress() 함수를 이용해서 메모리의 시작 주소를 반환받는다. 그런 다음 개발자가 원하는 상태로 데이터를 가공하면된다.

            I. JNI 문법을 따로 더 익히자.

8. Android.mk 파일

          A. 안드로이드 Makefile은 리눅스에서 사용하는 GNU Makefile과 비슷하지만, 사용법은 훨씬 간단하다.
          B. Makefile은 소스 코드/헤더 파일 지정, 라이브러리 링크 등을 자동으로 해주고, 수정된 코드를 다시 컴파일하는 것을 편리하게 해준다.

          C. 안드로이드의 NDK는 이런 Makefile대신 Android.mk 파일로 컴파일 한다. Android.mk파일은 모듈을 만드는데 필요한 소스 코드와 링크할 라이브러리를 지정하거나 라이브러리의 이름을 지정하는 등 빌드에 필요한 최소한을 정의한다.

[Android.MK 샘플코드]

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES := hello-jni.c

include $(BUILD_SHARED_LIBRARY)
--> Android.mk 파일과 같은 경로에 있는 hello-jni.c를 컴파일해서 libhello-jni.so라는 공유라이브러리를 생성하라

         D. LOCAL_CFALGS, LOCAL_CPPFLAGS :: 이것들은 APP_CFLAGS, APP_CPPFLAGS와 비슷하지만, Application.mk에 정의되는 APP_XXX변수가 모든 모듈에 적용되는 반면, LOCAL_XXX는 현재 모듈에만 적용된다. 실제로 Android.mk에는 최적화 레벨을 설정하지 말고 대신 Application.mk의 APP_OPTIM에 따를것을 권장한다.
       
         E. LOCAL_ARM_MODE는 강제로 ARM모드, 즉 32bit 명령어를 사용하여 바이너리를 생성하는데 사용된다. Thumb 모드(16bit 명령어)에 비해 코드 밀도가 좋지 않을 수 있지만, ARM 코드가 Thumb 코드보다 빠른 경향을 보이므로 성능이 개선될 수 있다. 예를들어 안드로이드의 Skia라이브러리는 ARM 모드로 명시적으로 컴파일 되었다. 만약 ARM모드를 사용하는 특정 파일들만 컴파일하고 싶다면, LOCAL_SRC_FILES에 .arm 접미사를 더해 해당 파일들을 리스트하면된다. 예를들면 file.c대신 file.c.arm으로 등록한다.

         F. LOCAL_ARM_NEON은 코드 내에서 Advanced SIMD 명령어 또는 intrinsic을 사용할 수  있는지, 그리고 컴파일러가 NEON명령어를 네이티브 코드에 생성할 수 있는가를 지정한다. NEON 명령어로 성능이 엄청나게 개선될 수 있지만 NEON은 ARMv7 아키텍처에서만 소개되었고 하나의 선택적인 컴포넌트 이다.

         G. LOCAL_DISABLE_NO_EXECUTE는 그 자체로는 성능에 어떤 영향도 주지 않는다. 하지만 고급 개발자라면 코드가 동적으로 생성될 때 NX bit를 비활성화 시키는 것에 관심을 가질 것이다.

[Android.mk 파일 분석 1]


[Android.mk 파일 분석 2]



8. Application.mk 파일

          A. Application.mk 파일도 애플리케이션에서 사용하는 라이브러리를 정의하는데 사용된다.
          B. 여러 개의 Android.mk파일에서 공통으로 사용하는 정의가 들어 있으며, Application.mk파일은 하나의 프로젝트에 하나만 존재해야한다.

          C. APP_OPTIM :: 옵션이며 release나 debug로 설정된다. 설정하지 않으면 애플리케이션이 디버그 가능핮니에 따라 설정된다.

          D. APP_CFLAGS(C/C++), APP_CPPFLAGS(C++ only) :: 컴파일러에 전달되는 플래그를 정의한다.
이 플래그들은 단순히 인클루드 파일을 찾기 위해 따라갈 경로를 지정해주는 정도로 사용될 수 있기 때문에 (ex: APP_CFLAGS += -I$(LOCAL_PATH)/myincludefiles), 코드를 최적화 하기 위해서는 반드시 필요하지은ㄴ 않다. 모든 플래그 리스트를 보려면 gcc 문서를 참조하도록하자

          E 성능이랑 관련된 플래그들은 -Ox시리즈(x는 전혀 최적화하지 않은 0에서 3까지의 최적화레벨을 지정), 또는 -Os이다. 하지만, 대부분의 경우 단순히 APP_OPTIM을 release로 저으이하거나 APP_OPTIM을 정의하지 않는 것으로 충분히 개발자를 위한 최적화 레벨이 설정되며, 이것만으로도 만족할 만한 결과를 얻을 수 있을것이다.

          F. APP_STL은 애플리케이션이 어떤 표준 라이브러리를 사용할지를 설정하기위해 사용된다.

[Application.mk 파일 분석 1]


[Application.mk 파일 분석 2]




댓글 없음:

댓글 쓰기