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 으로 그 파일을 열면 실제 이름이 나온다.
왜 오류가 안 나는지가 더 궁금했다. javac 와 ndk-build 가 따로 도니 빌드 시점에는 서로를 모른다. 실행할 때 이름으로 찾고 없으면 그때 문제가 된다. 그것도 화면에는 안 나오고 logcat 에만 적힌다.
W/dalvikvm( 880): No implementation found for native
Lcom/example/game/MainActivity;.showAd ()V
그 뒤로는 작업할 때 adb logcat 을 계속 열어 두게 됐다. 안 보면 왜 안 되는지 모르고 보면 대체로 적혀 있다.
문자열은 얻으면 놓아야 한다
문자열을 넘기려니 그냥은 안 됐다. jstring 과 const char* 가 다른 것이라 받은 것을 그대로 쓸 수 없다. 변환 함수가 따로 있었다.
const char* s = env->GetStringUTFChars(msg, 0);
// 사용
env->ReleaseStringUTFChars(msg, s);
처음에는 뒷줄을 안 썼다. 오래 돌리니 메모리가 늘었다. 앞서 화면 객체에서 겪은 것과 같은 모양이다. 얻었으면 놓는 것이 짝이고, 짝이 안 맞으면 쌓인다.
스레드마다 환경이 다르다
C++ 쪽에서 작업 스레드를 돌리다가 Java 를 부르려니 안 됐다. 찾아보니 JNIEnv* 는 그 스레드에서만 쓸 수 있다. 다른 스레드에서 쓰려면 JavaVM 을 들고 있다가 AttachCurrentThread 로 붙여서 새로 얻어야 한다.
AttachCurrentThread 와 DetachCurrentThread 를 짝으로 쓰는 것도 방법인데 더 단순한 쪽을 골랐다. 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 로 직접 짝지어 등록하는 것이다. 그러면 함수 이름을 마음대로 지어도 된다.
여기까지 하면서 든 생각은 경계를 넘는 게 비싸다는 것이다. 같은 언어 안에서는 함수를 부르면 끝인데, 언어 경계에서는 이름과 타입과 문자열과 스레드를 전부 맞춰야 하고 그만큼 틀릴 자리가 늘어난다. 그래서 값 하나씩 여러 번 넘기지 않고 한 번에 묶어 넘기는 쪽으로 짰다.
정리
- 두 언어를 이으려면 이름 규칙을 정확히 맞춰야 하고 한 글자만 달라도 못 찾는다
- 틀려도 빌드는 통과한다. 따로 컴파일되므로 빌드 시점에는 서로를 모른다
- 오류가 화면이 아니라
logcat에만 나온다.adb logcat을 열어 두고 작업한다 extern "C"를 빠뜨려도 같은 증상이다.arm-eabi-nm으로.so심볼을 본다- 문자열은 변환이 필요하고 얻었으면 놓아야 한다. 안 놓으면 쌓인다
- 환경 포인터는 스레드마다 다르다. 메인으로 넘겨 처리하는 편이 단순했다
- 반대 방향은 클래스 찾기와 메서드 찾기와 호출 세 단계다
- 타입 서명은
javap -s로 뽑아 베낀다. 손으로 쓰면 틀린다 - 이름 규칙이 싫으면
RegisterNatives로 직접 등록한다 - 경계를 넘는 게 비싸서 값을 한 번에 묶어 넘기는 쪽으로 짰다