meryngii.neta

今日も新たな"ネタ"を求めて。

JNIの自動リソース解放

久々に記事を書こうと思い立ったので書きます。ブログという形態が情報共有に適切なのかどうかはさておいて、小さなひらめきであっても何らかの形で記録していかなければ自分の糧にならない、と改めて感じている今日この頃です。

最近Android NDKで作業する機会を得ていまして、C++をモリモリ書ける喜びを感じています。Androidデバイス本来の力を生かすにはネイティブコードは現状必須です。初期のAndroidのモッサリ感は、限られた資源の中でわざわざJava VM上にアプリケーションを実行しているせいだったのではないかと勘ぐることもしばしばです。
ネイティブコードを利用したAndroidアプリケーションを作るにはJNI (Java Native Interface)を使うことが必須となるのですが、このJNIがいろんな意味で非常に厄介です。JNIの問題点を適当にあげつらってみます。

  • グルーコードを書くのが非常に面倒。(アプリケーションの本質でないところに労力をさく必要)
  • ネイティブ領域で各種IDをいちいちキャッシュしないと実行速度が大幅に低下する。
  • JavaC++も静的コンパイルされる言語なのに、実行時まで正常に関数が呼び出されるかわからない。(失敗するとUnsatisfiedLinkErrorがthrowされる)
  • ローカル参照は512個まで。(DeleteLocalRefで解放せずにそれ以上確保しようとすると即死)

以上のような内容に関して、IBMのサイトの以下の記事が参考になります。多くのこういったバッドノウハウ系の記事は読む気がしないものですが、この記事に関しては隅々まで非常に実践的な内容なのでおすすめです。
http://www.ibm.com/developerworks/jp/java/library/j-jni/

今回は、文字列と配列アクセスのグルーコードを書く手間を削減する方法についてです。大した内容ではないですが、一応ソースコードGitHubにあげてあります。
https://github.com/meryngii/jni_util
Javaから渡される文字列と配列へのアクセスには、事前にネイティブ領域にロードする関数を呼び出す必要があります。そしてさらに、ネイティブ関数が終了するときには解放処理の関数をまた呼び出す必要があります。こんな感じで。

extern "C"
JNIEXPORT void JNICALL Java_Foo_bar
  (JNIEnv* env, jobject thisj, jintArray arrayj)
{
    jboolean isCopy;
    int* elements = env->GetIntArrayElements(arrayj, &isCopy);
    // Do something with array...
    env->ReleaseIntArrayElements(arrayj, elements, 0);
}

C++プログラマとしてはRelease系の関数を直接呼び出したくないもので、これはRAII的にデストラクタで処理させるものでしょう。同じ考えのコードは探してみたらありました。
JNI C++ templates to acquire/release resources
あといちいち要素の型名を書かなければいけないのも気に食わないですね。ここはテンプレートでうまいことやるべきでしょう。というわけで以下のように。

#include "jni_util.hpp"

extern "C"
JNIEXPORT void JNICALL Java_Foo_bar
  (JNIEnv* env, jobject thisj, jintArray arrayj)
{
    jni_util::CopiedArrayAccess<jint> array(env, arrayj);
    int* elements = array.elements();
    size_t size = array.size();
    // Do something with array...

    // Destructor automatically releases the resource
}

まあ結局jintとか書いてる訳ですが…。本当はちゃんとiteratorをつけてコンテナとしての要件を満たすようにしようかなとも思うのですが、とりあえず実用上はこれで十分でしょう。
JNIでのJava配列へのアクセスには2種類あって、

  • Get***ArrayElementsで一気にコピーする
  • Get***ArrayRegionで少しずつコピーする

の2種類あります。それぞれ適当にCopied, Regionalと名前をつけて対応させておきました。
Release時の第3引数はmodeで、0, JNI_COMMIT, JNI_ABORTから選べます。0にしてReleaseしないと変更した値がJava側で反映されません。読み出しのみの場合はJNI_ABORTでOKです。*1modeに関してもコンストラクタに与える第3引数として付加してあります。

とりあえずこんなところで。

*1:JNI_COMMITって何のために使うんだろう…。