Android開発者のためのJNI入門


今回はJNIについて解説します。
本記事はAndroid Developersの開発ガイドラインを意訳(一部わかりやすい表現に変更しています)しています。Android NDKを使いたいなど、より高速化を求める、効率的な処理を知りたい開発者にはおすすめの内容です。
興味を持ったら是非、元記事もご確認ください。

JNIはJava Native Interfaceの略称で、Javaとネイティブコード(C/C++)を連携するための仕組みです。共有ライブラリからの動的読み込みをサポートしています。今から説明する利用方法は、ややこしいですが理解して使いこなせば、Javaとネイティブコードをつないだ非常に効率的なコード生成が可能です。

さらに詳細な仕様については JNI spec for J2SE 6 を参照して下さい、より実践的なJNIプログラミングを習得したければ、JNI Programmer’s Guide and Specificationが便利です。

1. JNIを理解する上で最も大事なオブジェクト:JavaVMとJNIEnv

(JavaVM and JNIEnv)

JNIには2つ(JavaVMとJNIEnv)、とても重要なオブジェクト(構造体)があります。これらの役割はオブジェクトへの関数テーブルを保持することで、ダブルポインタ(関数テーブルへのポインタのポインタ)で表現されています。

JavaVMオブジェクト

JavaVMオブジェクトはバーチャルマシン(VM)の生成/破棄を行う呼び出しインターフェイス(invocation interface)を提供します。
Java/JNIのセオリーに従うなら複数のVMを保持できますが、Android上では1つのVMしか保持できません(これはAndroidでの制限です)。

JNIEnvオブジェクト

JNIEnvオブジェクトはJNI関数のほとんどで利用されます。作成した(もしくは作りたいネイティブコードの)関数の最初の引数がJNIEnvオブジェクトとなります。
※マルチスレッドプログラミングでの注意として、多くのVMではスレッドローカルな領域にJNIEnvが保存されます。つまり、スレッド間でJNIEnvを共有することができません

JNIEnvを取得する方法がないケース(使いたいのに引数を保持していないなど利用できないケース)は、
JavaVMオブジェクトを共有しておくことをお勧めします。JavaVMオブジェクトのGetEnvメソッドをつかって、現在のスレッドのJNIEnvを見つけられるからです。

CとC++でJavaVM/JNIEnvオブジェクトの構造が異なる

JNIEnvとJavaVMはC言語とC++言語で異なったものが提供されます。提供元のインクルードファイル、”jni.h”ファイルではCとC++どちらのファイルに含まれているかを判断してJNIEnvとJavaVMを定義しています。
cファイルとcppファイル両方でjni.hをインクルードしてJNIEnvを使うと、混乱のもとになるので、どちらか片方でのみ利用する方が良いでしょう。

もっと多くのJNI Tipsは続きから。

2. ネイティブコードのスレッド処理:アタッチ・デタッチ

(Threads)

全てのバーチャルマシン(VM)のスレッドはLinuxのスレッドと同等で、Linux Kernelによって管理されています。
スレッドはJava言語から、とりわけThread.startなどで生成されます。しかし、VM上のJavaからでなくても(=ネイティブで生成されたスレッドを)VMに紐づける(アタッチする)ことが可能です。

たとえばネイティブ上でpthread_create関数を使ってスレッドを生成した場合、JNIではAttachCurrentThread関数、もしくはAttachCurrentThreadAsDaemon関数でVMに関連付けておきます。
関連づけを行わないと、JNIEnvを取得できず、JNIから呼び出すことも出来なくなるからです(=操作できないスレッドとなってしまう)。

ネイティブで生成したスレッドを関連付ける(アタッチする)理由は、VMがスレッドの資産を初期化、確保するためです。スレッドをmainスレッドグループに追加するとデバッガでもネイティブで作ったスレッドが見えるようになります。
すでにアタッチ済みのスレッドをAttachCurrentThreadした場合は、なにもしません(no operation)。

Dalvikはネイティブコードのスレッドをサスペンド出来ない

Dalvikバーチャルマシンはネイティブコードで実行中のスレッドをサスペンドできません(サポートしていません)。
ガベージコレクションの実施中、もしくはデバッガーがサスペンドを要求した場合は、VMの動きとしては、次回JNI呼び出しされたタイミングで、スレッドを一時停止することになります。

スレッドのデタッチを忘れない

アタッチしたスレッドは終了時に必ず、DetachCurrentThread関数をつかって紐付けを解除(デタッチ)してください。

Android 2.0 (Eclair)以降であれば、pthread_key_create関数をつかって、デストラクタ(削除用関数)を登録し、
そのデストラクタの中で、DetachCurrentThreadを呼び出してもかまいません。
(※事前にpthread_setspecific関数を使ってスレッドを特定するkey情報を作り、JNIEnvに登録しておく必要があります)

3. JNIからJavaオブジェクトへアクセスする

(jclass, jmethodID, and jfieldID)

ネイティブコードからJavaオブジェクトにアクセスしたいときの手順は一般的に以下の通りです。

  1. FindClassをつかってクラスオブジェクトへの参照を取得
  2. GetFieldIDをつかってフィールド(変数)を特定するIDを取得
  3. GetIntFiledのように決まった手法で、フィールド(変数)にアクセス

Javaのメソッド呼び出ししたいときも同様に、クラスオブジェクトへの参照を取得した後、メソッドIDを取得します。
良く出てくるオブジェクト名は以下の通りです。

  • jclass: Javaのクラスへの参照
  • jmethodID: Javaのクラスのメソッドの識別子
  • jfieldID: Javaのクラスのフィールド(メンバ)の識別子
//FindClassでMyClassオブジェクトを見つけ、jclassとして扱う
jclass* localClass = env->FindClass("MyClass");

パフォーマンス向上のために

メソッドなどのIDはIDといいながらも、VM内部の該当クラスオブジェクトのデータ構造体へのポインタであることがほとんどです。
クラスオブジェクトを見つけるには文字列比較を必要とします(文字列操作なので、ちょっと遅い)
しかし、一度、特定してしまえば(=ポインタを持ってることに等しいので)、非常に高速に関数呼び出し、フィールドアクセスが可能です。

パフォーマンスを優先するならネイティブコード上でIDをキャッシュするとよいでしょう。
現在のAndroidではVM1つに対して、1プロセスが上限です。静的なローカル構造体にデータを保持しておくことが妥当な解でしょう。

ネイティブコードで利用するクラスへの参照、IDの生存期間について

クラスへの参照、フィールドID、メソッドIDはクラスのオブジェクトがアンロード(削除)されるまで有効です。
クラスのアンロードはクラスのロードを担当するオブジェクトであるClassLoaderと関連付けられているすべてのクラスがガベージコレクション(GCが実行できる)するときにのみ実行されます。
一般的なPCなどではGCは、まれな現象ですが、Androidでは起こりえるため、十分注意してください。

Java側のクラスを取り扱うjclassについては以下のことを忘れないようにしてください。

  • jclassはクラスへの参照であること
  • jclassはネイティブコード側でJavaインスタンスへの参照を保持することが目的。
    NewGlobalRef関数をつかってGCから保護する必要がある

(詳細は次のセクションで)

またフィールドID、メソッドIDをキャッシュする場合、クラスの中で以下のようなJNIコードを用意すると便利です。

    /*
     * A native function that looks up and caches interesting
     * class/field/method IDs for this class.  Returns false on failure.
     */
    private static native boolean nativeClassInit();

    /*
     * Invoke the native initializer when the class is loaded.
     */
    static {
        if (!nativeClassInit())
            throw new RuntimeException("native init failed");
    }

クラスの内部にnativeClassInit関数をネイティブコードで用意しておき、この関数内でIDを検索(look-up)してキャッシュしてください。
GCによるクラスの解放や再生成が行われたときに自動的に実行されるため、取りこぼしが発生しません。

4. JNI経由ででもらうオブジェクトのスコープ:ローカル参照とグローバル参照

(Local and Global References)

ネイティブコード内での注意点です。
JNIが返す、渡すあらゆるオブジェクトが、「ローカルの参照」です。変数のスコープ(有効期間)が現在のスレッドのネイティブメソッドの間だけである(メソッドを抜けると解放されること)を意味します。

ネイティブコード内で処理が終わった後、どこかでオブジェクトを保持しつづけていても、そのポインタの先の参照は無効となります(オブジェクトを覚え続けることに意味が無いばかりか、不正なアドレスへのアクセスになります)。
これはjclass、jstring、およびjarrayを含むjobjectのすべてのサブのクラスに適用されます。 (JNI checksが有効に設定されていれば、Dalvik VMは誤った参照を警告してくれます)

グローバル参照への変更

長期間、参照を保存したい場合、「グローバルな」参照に変える必要があります。
NewGlobalRef関数はローカルで与えられた引数を取得し、グローバルな参照として返却します。グローバル参照はDeleteGlobalRef関数が呼ばれて保証されます(DeleteGlobalRefが呼ばれれば、解放されます)。
グローバル変数へ変更するサンプルコード:

//FindClassでMyClassオブジェクトを見つける
jclass* localClass = env->FindClass("MyClass");
//ローカル参照をグローバル参照へ変更
jclass* globalClass = (jclass*) env->NewGlobalRef(localClass);

全てのJNIオブジェクトでグローバル参照への変更が可能です。ただし、同じオブジェクトへの参照であっても、返り値であるglobalClassは異なることがあります。ネイティブコードで等価(同じオブジェクトであるか)を判定したいときは==を使わず、必ずIsSameObject関数を利用してください!

オブジェクトへの参照(ローカル、グローバルを問わない)はユニークではありません。オブジェクトを指し示す32bit長の参照変数は、次の関数に処理が移ったとたん変わってしまうかもしれません。2個の異なったオブジェクトが同じ32bit変数になることも有り得るため、jobject(全てJNIオブジェクト)の変数値をトリガに何かの判断を行うことは避けてください

沢山のローカル参照は避ける

プログラミング上の注意として、沢山のローカル参照(JNIでの引き渡し)を利用するのは、メモリ上の制限から推奨されません。バーチャルマシン(VM)は16個分のローカル参照用領域を確保しています。それ以上のローカル参照を利用したい場合、
DeleteLocalRef関数を使って、ローカル参照を削除するか、EnsureLocalCapacity関数を使って、ローカル参照用領域を16以上に増やす必要があります。

メソッドIDとフィールドIDはオブジェクトへの参照では無い

クラス(のインスタンス)のメソッドとフィールドを示すIDは32bitの識別子でオブジェクトへの参照ではありません。NewGlobalRef関数を使ってグローバル参照に変換する必要はありません。
GetStringUTFChars関数や、GetByteArrayElements関数のような関数もRAW(生)データへのポインターを返すため、オブジェクトへの参照ではありません。

上記までで、JNIのTips、主要4つを解説しました。非常に濃い内容ですが、覚えておくといずれもすぐに役に立つ内容ですね。以下の項目も随時更新します。

以下作成中…

随時更新予定です。待てないよ!というせっかちさんは、是非元記事を、ご確認ください。

5. UTF-8 and UTF-16 Strings

6. Primitive Arrays

7. Region Calls

8. Exception

9. Extended Checking

10. Native Libraries

11. 64-bit Considerations

12. Unsupported Features

13. FAQ

Why do I get UnsatisfiedLinkError?

Why didn’t FindClass find my class?

How do I share raw data with native code?

5 Comments