Skip to content
isdnetworks
Go back

이름 한 글자가 달라서 안 불렸다

Cocos2d-x 로 된 게임 본체는 C++ 이고 기기 기능은 Java 로 써야 한다. 둘을 JNI 로 이어야 광고를 띄우거나 점수를 넘길 수 있다.

Table of contents

Open Table of contents

이름이 하나 틀려서 안 불렸다

Java 쪽에서는 native 로 선언만 한다. 구현은 C++ 에 있다.

public native void showAd();

C++ 쪽 함수 이름은 패키지와 클래스와 함수 이름을 전부 이어 붙인 모양이다.

JNIEXPORT void JNICALL
Java_com_example_game_MainActivity_showAd(JNIEnv* env, jobject obj)

처음에는 아무리 해도 안 불렸다. 오류도 안 나고 그냥 아무 일도 일어나지 않는다. 이름 규칙을 다시 읽고 한 글자씩 대조했다. 패키지 이름의 점을 밑줄로 바꾸는 자리에서 한 군데가 남아 있었다. 딱 한 글자였다.

같은 증상을 내는 것이 하나 더 있었다. extern "C" 를 안 붙이면 C++ 이 이름을 바꿔 버린다. .so 안의 심볼이 _Z... 로 시작하는 모양이 된다. NDK 의 arm-eabi-nm 으로 그 파일을 열면 실제 이름이 나온다.

왜 오류가 안 나는지가 더 궁금했다. javacndk-build 가 따로 도니 빌드 시점에는 서로를 모른다. 실행할 때 이름으로 찾고 없으면 그때 문제가 된다. 그것도 화면에는 안 나오고 logcat 에만 적힌다.

W/dalvikvm(  880): No implementation found for native
  Lcom/example/game/MainActivity;.showAd ()V

그 뒤로는 작업할 때 adb logcat 을 계속 열어 두게 됐다. 안 보면 왜 안 되는지 모르고 보면 대체로 적혀 있다.

문자열은 얻으면 놓아야 한다

문자열을 넘기려니 그냥은 안 됐다. jstringconst char* 가 다른 것이라 받은 것을 그대로 쓸 수 없다. 변환 함수가 따로 있었다.

const char* s = env->GetStringUTFChars(msg, 0);
// 사용
env->ReleaseStringUTFChars(msg, s);

처음에는 뒷줄을 안 썼다. 오래 돌리니 메모리가 늘었다. 앞서 화면 객체에서 겪은 것과 같은 모양이다. 얻었으면 놓는 것이 짝이고, 짝이 안 맞으면 쌓인다.

스레드마다 환경이 다르다

C++ 쪽에서 작업 스레드를 돌리다가 Java 를 부르려니 안 됐다. 찾아보니 JNIEnv* 는 그 스레드에서만 쓸 수 있다. 다른 스레드에서 쓰려면 JavaVM 을 들고 있다가 AttachCurrentThread 로 붙여서 새로 얻어야 한다.

AttachCurrentThreadDetachCurrentThread 를 짝으로 쓰는 것도 방법인데 더 단순한 쪽을 골랐다. Java 호출이 필요하면 큐에 넣어 메인 스레드에서 처리하게 했다. 그러면 JNIEnv* 를 어디서 얻었는지 신경 쓸 자리가 없어진다.

반대 방향과 타입 서명

여기까지가 자바에서 C++를 부르는 쪽이다. 반대도 필요했는데 이쪽이 더 길다. 클래스를 찾고 메서드를 찾고 부르는 세 단계다.

jclass cls = env->FindClass("com/example/game/MainActivity");
jmethodID mid = env->GetMethodID(cls, "onScore", "(I)V");
env->CallVoidMethod(obj, mid, score);

세 번째 인자가 처음에는 암호 같았다. 찾아보니 타입 표기였다. 괄호 안이 인자이고 뒤가 반환이라, (I)V는 정수 하나를 받고 아무것도 안 돌려준다는 뜻이다. 객체가 들어가면 클래스 경로가 통째로 들어가서 (Ljava/lang/String;)V 같은 모양이 된다.

이것도 틀리면 못 찾고 오류는 역시 logcat 에만 나온다. 이름 틀림과 증상이 같다. 손으로 쓰다 몇 번 틀린 뒤에 javap -s 를 알았다. 컴파일된 .class 에서 서명을 그대로 뽑아 준다. 베껴 쓰면 안 틀린다.

이름 규칙에 기대지 않는 길도 있었다. JNI_OnLoad 에서 RegisterNatives 로 직접 짝지어 등록하는 것이다. 그러면 함수 이름을 마음대로 지어도 된다.

여기까지 하면서 든 생각은 경계를 넘는 게 비싸다는 것이다. 같은 언어 안에서는 함수를 부르면 끝인데, 언어 경계에서는 이름과 타입과 문자열과 스레드를 전부 맞춰야 하고 그만큼 틀릴 자리가 늘어난다. 그래서 값 하나씩 여러 번 넘기지 않고 한 번에 묶어 넘기는 쪽으로 짰다.

정리


Share this post on:

Previous Post
다른 기기로 옮겨도 같은 자리에서 죽었다
Next Post
고쳤다고 했는데 확인을 안 했다