자바 객체의 사용 - 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
: