JNI_OnLoad・・・って何やねん〜その1の続きです。
NDKを使って共有ライブラリを使おうとすると、logCatによく出力される「No JNI_OnLoad found in…skipping init」ってメッセージ、一体何やねん、何しに使うねんって疑問を解消・・・出来るかな(^_^;)
どうやって・何しに使うん?その2
重い処理・あるいは既存のC/C++のライブラリを流用する場合などで、native側の処理を待機する場面があると思います。その場合に、大きく分けて2通りの方法が有るんじゃないかと思います。
native側の処理が終了するまでJava側で待機する場合
1つ目はJava側で待機する方法。同期呼び出しってことですね。nativeメソッドからは、処理が終了するまで戻りません。
必要であれば、JavaのThreadやAsyncTaskLoaderなどからnativeメソッドを呼び出して返ってくるのを待つことになります。単発処理のイメージとしてはこんな感じ。
図1.
この場合には、native側はスレッドの事など気にせずに実行するのみです。
native処理中にJava側へ進捗状況などのコールバックを行う場合には、nativeメソッド呼び出し時の第1引数のJNIEnvへのポインタを使います。
native側の処理を非同期で実行して終了時にコールバックを呼んで貰う場合
2つ目は非同期呼び出し。native側へ非同期処理の開始要求を出して、実際の処理の完了を待たずに直ぐに戻ります。native側ではスレッドを生成して処理を行い、処理が終わるとJava側のコールバックメソッドを呼び出して終了を伝えます。こちらのイメージはこんな感じかな?
図2.
見た目はこっちの方が簡単そうですが、実装するには前者の方が簡単です。でも、どちらが良い・悪いではなく、処理の主体がどっちに有るかで分けるのがいいんじゃないかと思います。native側主体でスピード重視なら後者にする方が良いんじゃないかなぁ。
で、後者にする場合にはnative側で生成したスレッド内からJavaのメソッド・フィールドへアクセスする必要が生じます。しかし実はこの場合には、JNI関数が呼び出される時に引き渡されるJNIEnvを使用出来ません。
Android DevelopersのJNI Tipsを見ると、こんな事が書いてあります。
The JNIEnv is used for thread-local storage. For this reason, you cannot share a JNIEnv between threads. If a piece of code has no other way to get its JNIEnv, you should share the JavaVM, and use GetEnv to discover the thread’s JNIEnv. (Assuming it has one; see AttachCurrentThread below.)
意訳すると、JNIEnvはスレッドローカルな領域に保存されるため、スレッド間でJNIEnvを共有できない。もし他にJNIEnvを取得する方法が無いのであれば、(JNIEnvの代わりに)JavaVMを共有してGetEnvを呼び出すことでそのスレッドのJNIEnvを取得しないといけない。((GetEnvでJNIEnvを取得できるのは)そのスレッドがJNIEnvを持っている場合のみ。下記のAttachCurrentThreadを参照)てな感じです。
と言う事で、Javaでスレッドを生成してJNI経由でnative側を呼び出すのをであれば、第1引数がそのスレッドのJNIEnvへのポインタなので特に困りません。しかし、nativeコード内でスレッドを生成する場合には、次のようにしてJNIEnvを割り当ててもらわないといけません。
- スレッドを生成する
- JavaVM#AttachCurrentThread/AttachCurrentThreadAsDaemonを呼び出してスレッドをJavaVMに紐付ける。
この際にJNIEnvへのポインタを取得できます - 1でJNIEnvへのポインタを保存していない場合には、必要に応じてJavaVM#GetEnvを呼び出してJNIEnvへのポインタを取得する
- そのスレッドでの必要な処理が全て終われば、JavaVM#DetachCurrentThreadを呼び出してJavaJMとの紐付けを解除する
- スレッドを終了する
ってことで、最低限JavaVMをどないかして取得しないといけません。現在のAndroidの仕様では1プロセスにJavaVMは1つだけと言う事なので、同じプロセス内であれば、JavaVMを共有できます(1回だけ取得して静的に保持して大丈夫)。
やっとJNI_OnLoadの出番です
自分で別プロセスとしてJavaVMを生成した場合は別として、JavaVMを取得できるタイミングの1つがJNI_OnLoadになります。
というのも、JNI_OnLoadの関数プロトタイプは次の様になっています。
1 |
jint JNI_OnLoad(JavaVM *vm, void *reserved); |
第1引数がそのままJavaVMへのポインタなのでこれを静的に保持しておくのが一番簡単です。
と言う事でこんな感じに書きます。ちょっとお行儀悪いですけど。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#include <jni.h> #include <pthread.h> static JavaVM *savedVm; extern "C" { jint JNI_OnLoad(JavaVM *vm, void */* reserved */); } //******************************************************************************** //******************************************************************************** jint JNI_OnLoad(JavaVM *vm, void */* reserved */) { JNIEnv *env; if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } // save and hold pointer to JavaVM savedVm = vm; // register native functions and/or initialize native codes here if you need return JNI_VERSION_1_6; } // get saved JavaVM JavaVM *getVM() { return savedVm; } // get JNIEnv on current thread // you should call AttachCurrentThread/AttachCurrentThreadAsDaemon on the thread // before this function call JNIEnv *getEnv() { JNIEnv *env = NULL; if (savedVm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) { env = NULL; } return env; } |
注意しないといけないのは、JNIの決まり事で、JNI_OnLoadはCの呼び出し規約にしとかないとダメだということです。
Cとしてコンパイルしても構わないですが、C++としてコンパイルする方がエラー・警告チェックを厳しくチェックしてくれるので、C++でコンパイルして必要なところだけCの呼び出し規約にする方が良いと思います。上のコード例でJNI_OnLoadをCの呼び出し規約にしているのが、5〜7行目となります。
とりあえず共有ライブラリのソースコードのどこかにこれを書いてコンパイルすれば、共有ライブラリを読み込んだ時に、No JNI_OnLoad found in …」なんてことを言われません。自分は、「_onload.cpp」と「_onload.cpp.h」って名前にしてますけどお好きな名前でどうぞ。
もし、registerNatives関数を使ってnativeメソッドを追加するのであれば、19行目に入れます。また、JavaのメソッドIDを取得してキャッシュするなどの初期化処理を行う場合も同様です。
ちなみに、JavaVMを取得するもう1つの方法は、JNI経由でnativeメソッドが呼び出された際の第1引数のJNIEnvへのポインタからJNIEnv#GetJavaVMを呼び出して取得します。
どっちがいいかは好みの問題ですが、JNIを使う≒速度的なパフォーマンスが要求されると思えば、JNI関数の呼び出し毎に初期化するのではなく、最初にまとめて一括で初期化するほうがいいんじゃないかと思います。まさにそのためのJNI_OnLoad関数なわけだし。
native側でスレッドを生成する例〜その1
native側でのスレッド生成はいくつか方法が有りますが、今回は一番ベーシックなpthreadを使います。Java側とJNIから呼び出される関数は省略です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
static pthread_t test_thread; static void *thread_func(void *arg); int nativeStartThread() { return pthread_create(&test_thread, NULL, thread_func, (void *)NULL); } static void *thread_func(void *arg) { JavaVM *vm = getVM(); if (vm) { JNIEnv *env; // attach current thread to JavaVM and get pointer to JNIEnv vm->AttachCurrentThread(&env, NULL); //---------------------------------------------------------------------- // do something here ここで処理を行う //---------------------------------------------------------------------- // detach current thread from JavaVM vm->DetachCurrentThread(); } pthread_exit(NULL); } |
この例では何も処理してませんが、実際の処理は上のコードで「do something here ここで処理を行う」と書いている部分に入れます。JNIEnvも取得できているのでJava側へコールバック呼び出しするなりご自由にどうぞ。
ところで、上の例ではJava側から見ると処理を投げたら放ったらかしなイメージです。でも実際には途中で中断したい時もあるかと思います(例えばアプリが中断する時にnative側の処理を意図せずに動きっぱなしにするのはよくありません)。なので、普通は上のような投げっぱなしの実装にはしません。しかも上のコードではpthread_t変数をstaticに保持しているのでスレッドを1つしか生成することが出来ません。スレッド実行中にもう一度呼び出すとクラッシュします(-_-;)
なのに、何でわざわざこののコードも載せたかと言うと・・・この後の「その2」を見てもらえばわかりますが、それなりにコードの量があります。いきなりそっちだとnativeスレッドをJNI_OnloadとJavaVMへのnativeスレッドの紐付け/解除の部分がよくわからないんじゃないかと思って(^^)v。
と言う事で次は複数処理や中断処理を含めたもう少しまともな実装例を載せます。