자바 객체의 사용 - NDK

C++ 2012. 12. 27. 22:39 |

네이티브 코드 내에서 자바 객체를 사용할 수도 있다. 즉 콜백과 비슷한 형식을 지원한다. private영역까지 접근할 수 있다.

자바 객체의 클래스 정보 얻기

일단, 인자로 넘겨지는 자바 객체에 대한 클래스 정보를 획득하는 함수가 제공된다.

    <p>jclass cls = (*env)->GetObjectClass(env, jobj);</p>

여기서 주의할 것은 반환값으로 돌아오는 cls의 레퍼런스값은 오직 이 네이티브 메쏘드가 수행이 끝날 동안에만 유효하다는 것이다.

자바 객체 및 클래스 타입의 래퍼런스는 네이티브 메쏘드 실행시마다 결정되므로, 한 번 실행해 얻은 레퍼런스를 다음번 메쏘드에서 다시 사용하려는 것은 잘못된 것이다.

따라서, 원칙적으로 객체 레퍼런스 변수를 전역 변수로 할당하는 것은 위험한 일이다. 하지만, 꼭 허용되지 않는 것은 아니다. 전역 레퍼런스로 지정해 줄 수 있는 함수가 있다.

    <p> jobject NewGlobalRef(JNI_Env* env, jobject lobj) </p>

네이티브 코드를 호출한 객체의 메쏘드에 대한 ID얻기

객체의 메쏘드를 호출하기 위해 그 메쏘드의 ID를 획득해야 한다. 이것은 메쏘드의 이름과 시그너쳐를 이용하는데, 이름은 별 무리가 없겠으나, 시그너쳐는 까다롭다.

시그너쳐는 메쏘드의 인자 리스트와 반환 타입 정보를 스트링화한 것으로, 다음과 같은 규칙이 있다.

  • 시그너쳐 : "(인자 리스트)반환값"으로 이루어진 스트링이다.
  • 인자리스트의 인자들은 아래의 타입 기호에 따라 표기되며, 서로 ';'으로 구분된다.
  • 반환값 역시 타입 기호에 따라 표시된다.

타입 기호는 다음과 같다. 이것은 자바 가상 머신 스펙에서 클래스 파일 포맷 부분에 등장하는 기호와 동일하다.

    <p>Z : boolean
    B : byte
    C : char
    S : short
    I : int
    J : long
    F : float
    D : double </p>
  • 이들의 배열은 이들의 타입 기호 앞에 '['를 붙인다. 즉 int 배열은 [I가 된다.
  • 클래스 타입의 경우, L뒤에 클래스명을 패키지명까지 포함해 나열하면 된다. 즉, String클래스인 경우, Ljava.lang.String이다.

이것은 수작업으로 할 경우, 실수가 많을 수 있으나, javap를 이용해, 쉽게 기호화된 시그너쳐를 확인할 수 있다.

    <p>javap -s -p Hello</p><p>Compiled from Hello.java
    class Hello extends java.lang.Object {
    static {};
    /* ()V */
    Hello();
    /* ()V */
    public static void coo(java.lang.String, java.lang.String);
    /* (Ljava/lang/String;Ljava/lang/String;)V */
    private void foo(java.lang.String, java.lang.String);
    /* (Ljava/lang/String;Ljava/lang/String;)V */
    public native boolean getBoolean(boolean);
    /* (Z)Z */
    public native byte getByte(byte);
    /* (B)B */
    public native char getChar(char);
    /* (C)C */
    public native double getDouble(double);
    /* (D)D */
    public native float getFloat(float);
    /* (F)F */
    public native java.lang.String getHello(java.lang.String);
    /* (Ljava/lang/String;)Ljava/lang/String; */
    public native int getInt(int);
    /* (I)I */
    public native int getIntArray(int[])[];
    /* ([I)[I */
    public native long getLong(long);
    /* (J)J */
    public native short getShort(short);
    /* (S)S */
    public static void main(java.lang.String[]);
    /* ([Ljava/lang/String;)V */
    }</p>

이것은 다음과 같이 Hello클래스에 두 메쏘드를 추가한 후의 javap결과이다.

    <p> private void foo(String str,String str1) {
    System.out.println("Hello.foo() = "+str+str1);
    }</p><p> public static void coo(String str,String str1) {
    System.out.println("Hello.coo() = "+str+str1);
    }</p>

이제 foo메쏘드의 ID를 획득해 보자.

// 클래스 정보 획득
jclass cls = (*env)->GetObjectClass(env, obj);

// 메쏘드 ID
jmethodID fooM = (*env)->GetMethodID(env, cls, "foo", "(Ljava/lang/String;Ljava/lang/String;)V");

// 그런 메쏘드가 없으면, 0반환
if(fooM == 0) {
printf("Method foo isn't found\n");
}
else {
......
}

    <p> </p>

인자 생성 및 메쏘드 호출

이제 메쏘드를 호출하는 일이 다음 순서가 되겠다. 물론 여기에 해당되는 함수들이 제공되는데, 이 함수들을 통해, 클래스 내의 메쏘드를 호출하므로, 메쏘드 ID와 더불어 메쏘드의 인자들을 리스트화해서 보내야 한다. 두 가지 방법이 있다.

  • C의 va_list구조체를 이용한다.
  • jvalue 유니온를 이용한다.

여기서는 jvalue 유니온을 이용해 보자. 그 구조는 다음과 같다.

    <p>typedef union jvalue {
    jboolean z;
    jbyte b;
    jchar c;
    jshort s;
    jint i;
    jlong j;
    jfloat f;
    jdouble d;
    jobject l;
    } jvalue;</p>

그러면, 스트링 두 개를 인자로 받는 foo메쏘드를 호출해 보자. String은 객체이므로, l에 스트링 객체의 레퍼런스를 할당하면 된다.

    <p>jvalue* args;
    .......

    args = (jvalue*)malloc(sizeof(jvalue)*2);
    args[0].l = (*env)->NewStringUTF(env,"oops");
    args[1].l = (*env)->NewStringUTF(env,"la");
    (*env)->CallVoidMethodA(env, obj, fooM,args ); // JNIEnv, jobject, Method ID, jvalue</p>

여기에서는 foo가 void 타입이므로, CallVoidMethodA가 쓰였지만, 이 외에에도 반환값에 따라 여러 함수가 제공된다. 이들의 인자는 위와 동일하다.

  • jboolean CallBooleanMethodA
  • jbyte CallByteMethodA
  • jchar CallCharMethodA
  • jshort CallShortMethodA
  • jint CallIntMethodA
  • jfloat CallFloatMethodA
  • jdouble CallDoubleMethodA

참고로, va_list를 이용하여 인자를 전달하는 함수들은 A로 끝나지 않고, V로 끝나는데, 역시 마지막 인자만 va_list타입으로 수정해 주면 된다.

정적 메쏘드 호출

정적 메쏘드 또한 호출할 수 있는 함수가 따로 있다.

메쏘드 ID : GetStaticMethodID함수 이용

    <p>jMethodID cooM = (*env)->GetStaticMethodID(env, cls, "coo", "(Ljava/lang/String;Ljava/lang/String;)V");
    if(cooM == 0) {
    printf("Method foo isn't found\n");
    }
    else { .....
    } </p>
인자 형성 방법은 동일하다.

메쏘드 호출 : CallStaticVoidMethodA 이용

    <p>cooM = (*env)->GetStaticMethodID(env, cls, "coo", "(Ljava/lang/String;Ljava/lang/String;)V");
    if(cooM == 0) {
    printf("Method foo isn't found\n");
    }
    else {
    args = (jvalue*)malloc(sizeof(jvalue)*2);
    args[0].l = (*env)->NewStringUTF(env,"papa");
    args[1].l = (*env)->NewStringUTF(env,"ya");
    <b> (*env)->CallStaticVoidMethodA(env, cls, cooM,args );</b>
    }</p>

여기서 주의할 것은 CallStaticVoidMethodA의 두 번째 인자는 jobject타입이 아닌, jclass타입이란 것이다. 클래스 메쏘드이니 당연하기도 하다.

상위 클래스의 메쏘드 호출

한 걸음 더 나아가 상위 클래스의 메쏘드도 호출할 수 있다. 여기에 해당되는 메쏘드는 모두 NonVirtual이라는 이름을 포함하고 있는데, c++을 배운 이라면, NonVirtual의 의미를 알고 있을 것이다.

상위 클래스 타입의 jclass 객체 획득

    <p>jclass sClass = (*env)->GetSuperclass(env,cls);</p>

메쏘드 ID : GetMethodID함수 이용

    <p>superFooM = (*env)->GetMethodID(env, sClass, "foo", "(Ljava/lang/String;Ljava/lang/String;)V");
    if(superFooM == 0) {
    printf("Method foo isn't found\n");
    }
    else {
    .....
    }</p>

메쏘드를 호출: CallNonvirtualVoidMethodA

    <p>args = (jvalue*)malloc(sizeof(jvalue)*2);
    args[0].l = (*env)->NewStringUTF(env,"can");
    args[1].l = (*env)->NewStringUTF(env,"dy");
    (*env)->CallNonvirtualVoidMethodA(env,obj,sClass ,superFooM,args );
    </p>

멤버 필드 접근

필드를 접근하는 방법 또한 메쏘드를 접근하는 방법과 크게 다르지 않다. 필드가 Private일지라도 접근할 수 있다. 이 또한 몇 가지 함수에 의존하고 있는데, 그 함수의 형식이 메쏘드를 호출하는 함수와 거의 유사하기 때문이다. 일단 다음의 기호를 잊지 말자.

    <p>Z : boolean
    B : byte
    C : char
    S : short
    I : int
    J : long
    F : float
    D : double </p>
  • 이들의 배열은 이들의 타입 기호 앞에 '['를 붙인다. 즉 int 배열은 [I가 된다.
  • 클래스 타입의 경우, L뒤에 클래스명을 패키지명까지 포함해 나열하면 된다. 즉, String클래스인 경우, Ljava.lang.String이다.

이제 Hello.java에 몇 가지 필드를 추가해 보자.

private int intVal;
private static int iStaticIntVal;
private String strVal = "Hello";

이를 재컴파일 후, javah를 실행하고, 새로이 생긴 함수의 선언부를 HelloImpl.c로 복사해 이것에 대해 추가적으로 구현해 보기로 하자.

필드를 접근하는 것은 두 단계로 이루어진다.

  • 클래스와 필드의 이름과 타입을 이용해 필드의 ID를 얻어낸다.
  • 필드의 ID와 객체를 이용해 실제 필드의 값을 얻어낸다.

인스턴스 필드 접근

먼저 필드의 ID를 얻어내기 위한 함수는 다음과 같다.

    <p> jfieldID GetFieldID(JNI_Env* env,jclass clazz, const char *name, const char *sig)
    </p>

그리고, 인스턴스 필드를 접근하는 함수들은 다음과 같다.

1) Read 함수
  • jobject GetObjectField(JNI_Env* env,jobject obj, jfieldID fieldID)
  • jboolean GetBooleanField(JNI_Env* env,jobject obj, jfieldID fieldID)
  • jbyte GetByteField(JNI_Env* env,jobject obj, jfieldID fieldID)
  • jchar GetCharField(JNI_Env* env,jobject obj, jfieldID fieldID)
  • jshort GetShortField(JNI_Env* env,jobject obj, jfieldID fieldID)
  • jint GetIntField(JNI_Env* env,jobject obj, jfieldID fieldID)
  • jlong GetLongField(JNI_Env* env,jobject obj, jfieldID fieldID)
  • jfloat GetFloatField(JNI_Env* env,jobject obj, jfieldID fieldID)
  • jdouble GetDoubleField(JNI_Env* env,jobject obj, jfieldID fieldID)
2) Write 함수
  • void SetObjectField(JNI_Env* env,jobject obj, jfieldID fieldID, jobject val)
  • void SetBooleanField(JNI_Env* env,jobject obj, jfieldID fieldID, jboolean val)
  • void SetByteField(JNI_Env* env,jobject obj, jfieldID fieldID,jbyte val)
  • void SetCharField(JNI_Env* env,jobject obj, jfieldID fieldID, jchar val)
  • void SetShortField(JNI_Env* env,jobject obj, jfieldID fieldID,jshort val)
  • void SetIntField(JNI_Env* env,jobject obj, jfieldID fieldID,jint val)
  • void SetLongField(JNI_Env* env,jobject obj, jfieldID fieldID, jlong val)
  • void SetFloatField(JNI_Env* env,jobject obj, jfieldID fieldID, jfloat val)
  • void SetDoubleField(JNI_Env* env,jobject obj, jfieldID fieldID, jdouble val)

클래스 필드 접근

Static 필드를 접근하기 위해 먼저 ID를 얻어내야 한다.

    <p>jfieldID GetStaticFieldID(JNI_Env* env,jclass clazz, const char *name, const char *sig)</p>

그리고, 필드를 접근하는 함수들은 다음과 같다.

Gettor 함수
  • jobject GetStaticObjectField(JNI_Env* env,jclass clazz, jfieldID fieldID)
  • jboolean GetStaticBooleanField(JNI_Env* env,jclass clazz, jfieldID fieldID)
  • jbyte GetStaticByteField(JNI_Env* env,jclass clazz, jfieldID fieldID)
  • jchar GetStaticCharField(JNI_Env* env,jclass clazz, jfieldID fieldID)
  • jshort GetStaticShortField(JNI_Env* env,jclass clazz, jfieldID fieldID)
  • jint GetStaticIntField(JNI_Env* env,jclass clazz, jfieldID fieldID)
  • jlong GetStaticLongField(JNI_Env* env,jclass clazz, jfieldID fieldID)
  • jfloat GetStaticFloatField(JNI_Env* env,jclass clazz, jfieldID fieldID)
  • jdouble GetStaticDoubleField(JNI_Env* env,jclass clazz, jfieldID fieldID)
Settor 함수
  • void SetStaticObjectField(JNI_Env* env,jclass clazz, jfieldID fieldID, jobject value)
  • void SetStaticBooleanField(JNI_Env* env,jclass clazz, jfieldID fieldID, jint value)
  • void SetStaticByteField(JNI_Env* env,jclass clazz, jfieldID fieldID, jbyte value)
  • void SetStaticCharField(JNI_Env* env,jclass clazz, jfieldID fieldID, jchar value)
  • void SetStaticShortField(JNI_Env* env,jclass clazz, jfieldID fieldID, jshort value)
  • void SetStaticIntField(JNI_Env* env,jclass clazz, jfieldID fieldID, jint value)
  • void SetStaticLongField(JNI_Env* env,jclass clazz, jfieldID fieldID, jlong value)
  • void SetStaticFloatField(JNI_Env* env,jclass clazz, jfieldID fieldID, jfloat value)
  • void SetStaticDoubleField(JNI_Env* env,jclass clazz, jfieldID fieldID, jdouble value)

예제

이제 이들을 사용해 Hello 클래스의 필드들을 접근해 보기로 하자. Hello.java에 native메쏘드인 다음을 추가하고 컴파일 후, HelloImpl.c에서, 다음을 중심으로 코딩해 보자.

    <p>public native void testFieldAccess()</p>
정적 변수 접근

정적 필드인 iStaticIntVal 필드를 접근하여, 값을 읽어들인 후, 다시 2를 더해 값을 써 보자.

    <p>jfieldID jFieldId;
    jint iStaticIntVal;
    jclass cls = (*env)->GetObjectClass(env, jobj);

    jFieldId = (*env)->GetStaticFieldID(env,cls,"iStaticIntVal","I");
    if(jFieldId == 0) {
    printf("Field iStaticIntVal not Found in Hello class\n");
    return;
    }

    // 값을 읽어들인 후,
    iStaticIntVal = (*env)->GetStaticIntField(env,cls,jFieldId);

    // 2를 더해, 다시 그 값을 쓴다.
    (*env)->SetStaticIntField(env,cls,jFieldId,(iStaticIntVal+2)); </p>
인스턴스 변수 접근
    <p>jint iVal;
    jfieldID jFieldId;
    jclass cls = (*env)->GetObjectClass(env, jobj);
    jFieldId = (*env)->GetFieldID(env,cls,"intVal","I");
    if(jFieldId == 0) {
    printf("Field intVal not Found in Hello class\n");
    return;
    }
    iVal = (*env)->GetIntField(env,jobj,jFieldId);
    printf("iVal in C = %d\n",iVal);
    (*env)->SetIntField(env,jobj,jFieldId,iVal);</p>

그런데, 실제로 int를 가지고 실험해 본 결과, C에서 값을 잘 넘겨받는 것까지는 좋은데, 다시 자바로 값을 써 줄 때, 인자가 잘 전달되지 않는 현상을 보였다. 좀더 생각해 볼 문제이다.

스트링 접근

스트링은 객체이므로 객체를 접근하는 방식을 통해 접근해야 한다. 우선 GetFieldID 함수의 인자로 "Ljava/lang/String;"을 주목하면, 객체의 접근 방식과 동일함을 알 수 있다. 여기에서 세미콜론(;)을 빠뜨리면, 인식하지 못하므로 주의하기 바란다.

    <p>jstring jstr;
    jfieldID jFieldId;
    jclass cls = (*env)->GetObjectClass(env, jobj);

    jFieldId = (*env)->GetFieldID(env,cls,"strVal","Ljava/lang/String;");
    if(jFieldId == 0) {
    printf("Field strVal not Found in Hello class\n");
    return;
    }</p><p>jstr = (*env)->GetObjectField(env,jobj,jFieldId);
    (*env)->SetStaticIntField(env,cls,jFieldId,(iStaticIntVal+2));
    str = (*env)->GetStringUTFChars(env, jstr, 0);
    printf("Get String : %s\n",str);
    </p><p>새로운 스트링을 생성하여, 이것의 레퍼런스값을 str 필드에 할당하려면, 다음과 같이 한다.
    jstr = (*env)->NewStringUTF(env, "123");
    (*env)->SetObjectField(env, jobj, jFieldId, jstr);</p>

기타 객체를 호출하는 방법도 스트링과 거의 동일하다. (*env)->GetObjectField(env,jobj,jFieldId);를 통해, jobject타입의 객체를 얻어낸 후, 필요에 따라 이 객체의 메쏘드를 호출하면 된다.

[출처] native 코드내에서 자바 객체의 사용|작성자 츠바샤

===================================================================================================================
퍼온 글이긴 합니다만 ndk를 사용함에 있어서 도움이 될까 싶어서 올립니다.
여기에 몇가지를 더 추가하자면 ndk에서는 보통 이런식으로 사용하시는 분들이 많습니다.
Java에서 값을 -> c,c++로 전달 -> c,c++연산 과정 수행 ->Java로 수행 결과를 가져옴 -> 출력
허나 위의 방법을 이용해서..c,c++에서 java 메소드나 필드를 가져올 수 있습니다. 이에 대한 예는 보통 쉽게 구할 수 있는 예제 파일의 경우 클래스를 임의로 만든 후 c/c++에서 해당 메소드를 GetObjectClass를 이용하여 obj를 가져온 다음 이를 통해 해당 클래스의 메소드를 가져오는데 findclass를 이용하여 FindClass ("android/os/Process"); 같은 형태로 불러와 쓸 수 있습니다.
즉, java에서 우리가 import를 하여 쓰는 많은 클래스를 c/c++에서도 FindClass를 통해 가져온 후 클래스 내 메소드나 필드를 불러와 쓸 수 있는 것이지요.
하지만 이렇게 쓰는 방법은 많은 번거로움을 가지고 있는 것은 물론 복잡도도 더 커집니다만 이렇게 NDK를 통하여 구현할 경우 이점이 2가지가 있습니다.
첫번째는 모든 분들이 알다시피 c/c++이 지니고 있는 이점을 그대로 활용할 수 있다는 점입니다. 이점에 대해서는 많은 분들이 공감하실 것 같아 이상의 설명은 생략하겠습니다.
두번째 이점으로는 역컴파일에 의해 소스코드가 공개되기 어렵다는 것입니다. 대부분의 apk 파일의 경우 확장자만 zip로 바꾼 후 압축을 풀면 풀립니다. 이후 보이는 클래스 파일을 java 파일로 만드는 것은 dex2jar라는 도구와 디컴파일러 도구만 있으면 apk를 구성하고 있는 대부분의 자바 소스를 구경하실 수 있습니다.
헌데 ndk를 통해 c/c++를 빌드한 .so 라이브러리 파일의 경우 안의 소스를 보기가 어렵습니다. 코드 변환기를 써도 함수명까지는 확인이 가능하나 함수내 소스에 대해서는 글쎄요. 지금까지 여러 방법을 써봤으나 함수명 파악 말고는 소스파악이 안되더군요.
...때문에 보안을 요하는 도구를 개발할 시 이 같은 방법을 활용하는 것이 더 나을 것 같습니다.
실제로 몇몇 어플 보안업체에서는 이같은 방법을 활용하고 있습니다.
마지막으로 c와 c++에 해당하는 기본지식과 위와 같이 java 클래스의 활용방법만 알게 되어도 상당히 유리한 상태에서의 개발 작업을 진행하실 수 있을 것입니다.

'C++' 카테고리의 다른 글

소스 가독성 높이는 방법  (0) 2013.10.11
문자열 비교는 == 비교연산자 와 strcmp 어떤걸 써야 하는가..  (0) 2013.10.10
JNI Basic for NDK -4  (0) 2012.12.27
JNI Basic for NDK -3  (0) 2012.12.27
JNI Basic for NDK -1  (0) 2012.12.27
Posted by maysent
:

JNI Basic for NDK -4

C++ 2012. 12. 27. 22:38 |

JNI Basic for NDK - 4

필드와 메써드

글을 쓰다보니 처음 의도와는 달리 JNI Programmer's guide and Specification을 거의 베끼는 형식이 되어가고 있습니다. 단지 예제를 그냥 안드로이드 환경에 맞도록 조금 수정하는 정도의 차이 밖에 없지만 나름 의미가 있다고 생각하고 계속 진행하도록 하겠습니다.

원본이 보고 싶으신 분은 “Java Native Interface: Programmer's Guide and Specification”
http://java.sun.com/docs/books/jni/ 를 참고하세요.

지금까지 JNI가 네이티브 메써드에서 어떻게 기본형 데이터와 참조형 데이터를 다루는지를 봤습니다. 이제는 오브젝트의 필드와 메써드를 접근하는 방법을 알아보도록 하겠습니다.

1. 필드에 접근하기

Java에서는 두가지 종류의 필드가 있습니다. 클래스의 인스턴스는 클래스의 복사본 형태로 자신만의 '인스턴스 필드'를 가지고 있습니다. 하지만 '스태틱 필드'는 모든 인스턴스가 동일한 필드를 공유를 합니다.

JNI에서는 이 두가지 필드를 읽거나 수정을 할 수 있는 방법을 모두 제공하고 있습니다. 우선 인스턴스 필드를 접근하는 예제를 보도록 하겠습니다. 이클립스에서 InstanceFieldAccess라고 하는 프로젝트를 생성하고 InstanceFieldAccessActivity를 아래와 같이 수정합니다.

public class InstanceFieldAccessActivity extends Activity {
private String s;

private native void accessField();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
s = "abc";
String orgString = accessField();
String str = new String("In Java, it was ");
str = str + "s = \"" + orgString + "\". \n";
str = str + "Now it is s = \"" + s + "\".";

TextView tv = new TextView(this);
tv.setText(str);
setContentView(tv);
}

static {
System.loadLibrary("InstanceFieldAccess");
}
}

InstanceFieldAccessActivity 클래스에는 s라고 하는 String형의 인스턴스 필드가 있습니다. 이 필드는 onCreate에서 'abc'로 값이 세팅이 됩니다. 다음에 accessField라고하는 네이티브 메써드를 호출합니다. 이 메써드는 인스턴스 필드인 s에 저장된 문자열을 다른 것으로 변경을 할 것입니다. 그 변경된 내용은 TextView에 표시가 됩니다.

accessField의 네이티브 메써드 구현은 아래와 같습니다.

#include <stdio.h>
#include "InstanceFieldAccess.h"

JNIEXPORT jstring JNICALL Java_ndk_instancefieldaccess_InstanceFieldAccessActivity_accessField
(JNIEnv *env, jobject obj)
{
jfieldID fid;
jstring jstr, jstrOrg;
const char *str;

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

fid = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");
if (fid == NULL) {
return NULL;
}

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

jstr = (*env)->NewStringUTF(env, "123");
if (jstr == NULL) {
return NULL;
}
(*env)->SetObjectField(env, obj, fid, jstr);

return jstrOrg;
}

ndk-build로 컴파일하고 이클립스에서 실행을 하면 아래와 같은 내용이 디스플레이 됩니다.

1.1 인스턴스 필드에 접근하는 방법

인스턴스 필드에 접근하기 위해서는 세 단계의 과정을 거쳐야합니다.

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

GetObjectClass를 호출하여 클래스 레퍼런스를 얻어옵니다.

fid = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");
필드 아이디를 얻기 위해서 GetFieldID를 호출합니다. 이때 앞에서 얻은 클래스 레퍼런스인 cls와 필드 이름과 필드 디스크립터가 인자로 사용이 됩니다.

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

필드 아이디를 구한 후에는 GetObjectField를 호출하여 원하던 인스턴스 필드를 얻어옵니다.

스트링은 기본형이 아닌 오브젝트라서 Get/SetObjectField라는 함수를 사용했습니다. 기본형 데이터에 대해서는 GetIntField나 SetFloatField 같은 함수를 JNI에서 별도로 제공합니다.

1.2 필드 디스크립터

앞에서 본 "Ljava/lang/String;"를 JNI 필드 디스크립터라고 합니다. 이는 Java의 필드 타입을 표현한 C 스트링입니다.

기본형에 대해서는 int는 "I", float는 "F", double은 "D", boolean은 "Z" 같은 식으로 표현이 됩니다.

java.lang.String과 같은 레퍼런스 타입의 경우는 L로 시작을 해서 JNI 클래스 디스크립터가 나오고 마지막으로 세미콜론(;)으로 끝납니다. JNI 클래스 디스크립터는 클래스의 전체 이름에서 '.'를 '/'로 치환한 것입니다. java.lang.String은 아래와 같이 됩니다.

"Ljava/lang/String;"

배열에 대한 디스크립터는 "["와 배열의 요소의 데이터 타입으로 구성이 됩니다. int[]의 경우는 "[I"가 됩니다.

필드 디스크립터를 알아내기 위해서 javap라고 하는 tool을 사용할 수 있습니다. JNI 프로그래밍 중에서 불편한 것 중의 하나가 필드 디스크립터를 사용하는 것입니다. 그래도 javap를 사용하면 일을 좀 더 편하게 실수 없이 할 수 있습니다.

사용법은 현재 폴더가 jni 폴더라고 가정했을 때 아래와 같습니다.

javap -classpath ../bin -s -private ndk.instancefieldaccess.InstanceFieldAccessActivity

 

'C++' 카테고리의 다른 글

문자열 비교는 == 비교연산자 와 strcmp 어떤걸 써야 하는가..  (0) 2013.10.10
자바 객체의 사용 - NDK  (0) 2012.12.27
JNI Basic for NDK -3  (0) 2012.12.27
JNI Basic for NDK -1  (0) 2012.12.27
NDK C++ 자료  (0) 2012.12.27
Posted by maysent
:

JNI Basic for NDK -3

C++ 2012. 12. 27. 22:37 |

JNI Basic for NDK - 3

배열 다루기

JNI에서는 기본형 배열과 오브젝트 배열을 다르게 취급합니다. 기본형 배열이란 int나 boolean 같은 기본형으로 이루어진 배열을 말합니다. 오브젝트 배열이란 클래스의 인스턴스나 다른 배열을 가지고 있는 배열을 말합니다. 아래 코드를 볼까요.

int[] iarr;

float[] farr;

Object[] oarr;

int[][] arr2;

여기서 iarr과 farr은 기본형 배열입니다. oarr과 arr2는 오브젝트 배열이죠.

1. 기본형 배열

네이티브 메써드에서 기본형 배열을 다루는 것은 앞서 본 문자열을 다루는 것과 비슷합니다.

이클립스에서 새 Android Project를 생성합니다. New Android Project 창에 아래와 같이 입력하고 Finish를 클릭합니다.

Project Name : IntArray
Build Target : Android 2.2
Application name : IntArray
Package name : ndk.intarray

자동 생성된 IntArrayActivity.java의 IntArrayActivity class의 내용을 위의 코드와 같이 수정합니다.

public class IntArrayActivity extends Activity {
private native int sumArray(int[] arr);

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

int arr[] = new int[10];
for(int i = 0; i < 10; i++) {
arr[i] = i;
}
int sum = sumArray(arr);
String str = new String("sum = " + sum);

TextView tv = new TextView(this);
tv.setText(str);
setContentView(tv);
}

static {
System.loadLibrary("IntArray");
}
}

arr이라고 하는 배열에 담겨 있는 숫자를 네이티브 메써드인 sumArray를 사용해서 더하는 간단한 프로그램입니다.

1.1 C에서 배열에 접근하기

javah를 사용해서 네이티브 메써드인 sumArray의 C 함수 원형을 알아보면 아래와 같습니다.

JNIEXPORT jint JNICALL Java_ndk_intarray_IntArrayActivity_sumArray
(JNIEnv *, jobject, jintArray);

여기서 보듯이 int 형의 배열은 JNI에서는 jintArray 형으로 표현이 됩니다. 앞에서 본 jstring과 마찬가지로 jintArray도 C 언어에서의 배열과는 다릅니다. 잘 못 생각하고 아래와 같이 구현을 하면 이상한 결과가 나올 겁니다.

JNIEXPORT jint JNICALL Java_ndk_intarray_IntArrayActivity_sumArray
(JNIEnv *env, jobject obj, jintArray arr)

{

jint i, sum = 0;

for (i=0; i < 10; i++) {

sum += arr[i];

}

return sum;

}

기본형 배열을 다루기 위해서는 반드시 JNI 함수를 사용해야합니다. 아래의 코드는 적당한 JNI 함수를 사용해서 새로 구현한 네이티브 메써드입니다.

JNIEXPORT jint JNICALL Java_ndk_intarray_IntArrayActivity_sumArray
(JNIEnv *env, jobject obj, jintArray arr)
{
jint buf[10];
jint i, sum = 0;
(*env)->GetIntArrayRegion(env, arr, 0, 10, buf);
for (i = 0; i < 10; i++) {
sum += buf[i];
}
return sum;
}

1.2 기본형 배열 접근하기

위의 예에서는 배열에 접근하기 위해서 GetIntArrayRegion이라고 하는 JNI 함수를 사용했습니다. 이 함수를 사용해서 arr에 있는 모든 배열 요소를 buf에 복사를 했습니다. 세번째 인자인 0은 복사할 배열의 시작 위치를 네번째 인자인 10은 복사할 갯수를 나타냅니다. 이 함수에 대응하는 JNI 함수인 SetIntArrayRegion은 C 배열에 있는 요소를 JNI 배열에 복사하는 역할을 합니다.

JNI는 Get/Release<Type>ArrayElements 함수들도 제공을 합니다. 여기서 <Type> 자리에 기본형 이름이 들어갑니다. 이 함수는 기본형 배열에 접근할 수 있는 포인터를 리턴합니다. 위의 예를 GetIntArrayElement를 사용해서 구현하면 아래와 같습니다.

JNIEXPORT jint JNICALL Java_ndk_intarray_IntArrayActivity_sumArray
(JNIEnv *env, jobject obj, jintArray arr)
{
jint *carr;
jint i, sum=0;
carr = (*env)->GetIntArrayElements(env, arr, NULL);
if (carr == NULL) {
return 0;
}
for (i = 0; i < 10; i++) {
sum += carr[i];
}
(*env)->ReleaseIntArrayElements(env, arr, carr, 0);
return sum;

}

2. 오브젝트 배열

JNI는 오브젝트 배열에 접근하는 함수를 기본형 배열에 접근하는 함수와 별도로 제공을 합니다. GetObjectArrayElement는 지정한 인덱스의 오브젝트를 리턴합니다. SetObjectElement는 지정한 인덱스의 오브젝트 내용을 변경합니다. 기본형 배열을 다루는 JNI 함수와는 달리 전체 배열을 한번에 얻어오는 함수는 제공되지 않습니다.

아래의 예를 보도록 하겠습니다.

public class ObjectArrayTestActivity extends Activity {
private static native int[][] initInt2DArray(int size);

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

String str = new String("");

int[][] i2arr = initInt2DArray(3);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
str = str + " " + i2arr[i][j];
}
str = str + "\n";
}

TextView tv = new TextView(this);
tv.setText(str);
setContentView(tv);
}

static {
System.loadLibrary("ObjectArrayTest");
}
}

위 예에서 네이티브 메써드인 initInt2DArray는 int 형의 2차원 배열을 생성합니다. Java 코드에서는 이 생성된 배열을 받아와서 디스플레이를 해 줍니다.

initInt2DArray 의 네이티브 구현은 아래와 같습니다.

JNIEXPORT jobjectArray JNICALL Java_ndk_objectarraytest_ObjectArrayTestActivity_initInt2DArray
(JNIEnv *env , jclass cls, jint size)
{
jobjectArray result;
int i;
jclass intArrCls = (*env)->FindClass(env, "[I");
if (intArrCls == NULL) {
return NULL;
}
result = (*env)->NewObjectArray(env, size, intArrCls, NULL);
if ( result == NULL) {
return NULL;
}
for (i = 0; i < size ; i++) {
jint tmp[256];
int j;
jintArray iarr = (*env)->NewIntArray(env, size);
if (iarr == NULL) {
return NULL;
}
for (j=0; j < size; j++) {
tmp[j] = i+j;
}
(*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
(*env)->SetObjectArrayElement(env, result, i, iarr);
(*env)->DeleteLocalRef(env, iarr);
}
return result;
}

(*env)->FindClass(env, "[I");

FindClass JNI 함수는 int의 이차원 배열의 원소의 클래스를 찾습니다. 인자로 사용된 "[I"는 Java에서 ini[]와 같습니다. 이를 JNI 클래스 디스크립터라고 합니다.

(*env)->NewObjectArray(env, size, intArrCls, NULL);

NewObjectArray는 intArrCls형의 오브젝트를 배열의 요소로 갖는 배열을 생성합니다. 우리의 예에서는 FindClass로 얻어온 int[] 형의 데이터를 배열의 요소로 갖게 됩니다. int형의 배열은 NewIntArray를 사용해서 만들어야합니다.

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

DeleteLocalRef는 가상 머신이 메모리를 모두 소진해 버리는 일이 발생하지 않도록 하기 위해서 실행합니다. NewIntArray의 리턴으로 전달된 iarr은 더이상 필요가 없으므로 이를 가상 머신 상에서도 제거하여 메모리 낭비는 줄입니다.

'C++' 카테고리의 다른 글

문자열 비교는 == 비교연산자 와 strcmp 어떤걸 써야 하는가..  (0) 2013.10.10
자바 객체의 사용 - NDK  (0) 2012.12.27
JNI Basic for NDK -4  (0) 2012.12.27
JNI Basic for NDK -1  (0) 2012.12.27
NDK C++ 자료  (0) 2012.12.27
Posted by maysent
: