meryngii.neta

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

Java向けにC++クラスをラップする方法

JNI (Java Native Interface)ネタは、この記事で一応最後です。
前々回は、Javaから渡された配列や文字列を簡単に受け取る方法について述べました。配列の受け渡しに関しては、最悪だと呼び出し毎にネイティブヒープへのコピーが発生する可能性があるので、ネイティブ側とVM側のどちらにデータを配置するのが正しいのかを考慮して設計する必要があります。
前回は、C++をJNI経由で呼び出す際の例外処理について述べました。STLに例外が必要となるわけですし、Android等でも基本的には-fexceptionsを有効にしてビルドした方がいいと思います。
さて、ここまでの知識だと、JavaからC言語インターフェイス経由で関数を呼び出すということは出来ますが、C++のクラスをJavaから管理するとなるともう少し工夫が必要です。今回は、オブジェクト指向なやり方でC++のクラスを利用するにはどうすればいいのかについて述べていきます。

ポインタなんて所詮整数だ

Javaには、CやC++のポインタを認識する仕組みはありません。*1Java仮想マシン上の"綺麗な世界"には、ポインタなどという穢らわしきモノは存在しないわけです。
しかし、結局のところ、ポインタ≒アドレスなんて所詮整数なので、Javaのクラスに整数メンバを用意しておけばアドレスを保存しておくことができます。そいつをJNI経由でC++側から読み出せば、C++側でインスタンスを特定できるという話です。今回の記事の根幹はここだけです。
この辺りの話題は、以下の記事を参考にしています。
http://thebreakfastpost.com/2012/01/21/wrapping-a-c-library-with-jni-introduction/
例として、以下の様なテストクラスをC++によって実装してみることにします。

public class WrapperTest
{
    private long mHandle = 0; // <=== こっそりアドレスを保存する

    public WrapperTest() {
        init();
    }
    
    private native void init();   
    public native void put(int x);
    public native void show();
    public native void destroy();
}

ほとんどネイティブ関数となっていますが、mHandleなる整数メンバを入れていることに注目です。*2こいつをC++から読み書きすることで、Javaオブジェクトにぶら下がったC++インスタンスのアドレスを特定します。
ちなみに、Android標準ライブラリの多くが裏でネイティブを呼び出す形になっているのですが、使われているテクニックはやはり同じのようです。

ラッパークラス用テンプレート

さて、ここからはC++での実装に入っていきます。理屈は極めて単純なものの、毎回新しいラッパークラスを作る度に整数<->ポインタ間キャストについて考えるのは面倒でしょう。というわけで、ラッパークラスを作るための補助テンプレートについて紹介します。参考記事よりはもう少し賢い実装になっていると思います。
ほぼ同様のコードはGitHubにも上げてあります: https://github.com/meryngii/jni_util

template <typename T>
class Wrapper
{
public:
    explicit Wrapper(JNIEnv* env, jobject objj)
        : env_(env), objj_(objj) { }

    Wrapper(const Wrapper&) = default;
    Wrapper& operator = (const Wrapper&) = default;
    
    T& get() const {
        T* ptr = getPointer();
        if (!ptr) {
            throw std::runtime_error("Uninitialized native object was used!");
        }
        
        return *ptr;
    }
    
    template <typename... Args>
    void create(Args&&... args) const {
        destroy(); // delete if an object exists
        setPointer(new T(std::forward(args)...));
    }
    
    void destroy() const {
        delete getPointer();
        setPointer(nullptr);
    }
    
    // Pointer Operations
    T* getPointer() const {
        return reinterpret_cast<T*>(env_->GetLongField(objj_, getFieldID(objj_)));
    }
    void setPointer(T* ptr) const { 
        env_->SetLongField(objj_, getFieldID(objj_), reinterpret_cast<jlong>(ptr));
    }
    
private:
    jfieldID getFieldID(jobject objj) const {
        if (!classj_) {
            classj_ = env_->GetObjectClass(objj_);
            checkException(env_);
        }
        
        if (!fieldID_) {
            fieldID_ = env_->GetFieldID(classj_, "mHandle", "J");
            checkException(env_);
        }
        
        return fieldID_;
    }

private:
    JNIEnv* env_;
    jobject objj_;
    static jclass classj_;
    static jfieldID fieldID_;
};

ポイントとなる関数はgetFieldID, set/getPointerの3つで、後はオマケです。Javaのメンバ変数を読み書きするにはjfieldIDが必要となるので、ネイティブ側でstatic変数にキャッシュしておきます。
jclassやjfieldIDといった値は、実行時に一度手に入れば(確か)プロセス終了時まで有効なので、必ずキャッシュしましょう。ちなみに、jclassやjobjectはクラスやオブジェクトが同じでも異なるポインタ値となる可能性があるので、env->IsSameObjectといった関数がわざわざ定義されています。jfieldIDやjmethodIDは一意性が保たれているのでキーとして使うことも可能ですが、個人的にあまり危なそうなことはオススメしません。
上のWrapperテンプレートを使えば、後はラッパークラスを作るのは簡単です。C++で普通にクラスを作って、それにJNIのインターフェイスを加えます。

class WrapperTest
{
public:
    void put(int x) { data_.push_back(x); }
    
    void show() {
        for (int x : data_)
            std::cout << x << " ";

        std::cout << std::endl;
    }
    
private:
    std::vector<int> data_;
};

extern "C"
{

JNIEXPORT void JNICALL Java_WrapperTest_init
  (JNIEnv* env, jobject thisj)
{
    convertException(env, [=] () {
        Wrapper<WrapperTest>(env, thisj).create();
    });
}

JNIEXPORT void JNICALL Java_WrapperTest_put
  (JNIEnv* env, jobject thisj, jint x)
{
    convertException(env, [=] () {
        auto& self = Wrapper<WrapperTest>(env, thisj).get();
        self.put(x);
    });
}

// ...

}

ネイティブオブジェクトの破棄

基本的な話としては以上ですが、まだオブジェクトの破棄について言及していませんでした。
JavaのオブジェクトはGCによって不定期に開放されます。finalizeメソッドをオーバーライドすることによって、GCが走った時にリソースの最終処理を行うことが可能になっているので、普通に考えればC++のオブジェクトもその際に開放するのが自然な発想でしょう。ところが、finalizeメソッドはいつ呼び出されるか予測が出来ず、しかもアプリケーションの終了タイミングによっては呼び出されない可能性があります。
C++プログラマは「デストラクタは必ず呼び出される」という考えでクラスを設計するので、finalizeにネイティブリソースの破棄を一任するのは良い考えとはいえないでしょう。結局のところ、少々面倒ではありますが素直に破棄用メソッドを作って、Java側のクラス利用者がfinallyなりで責任を持って呼び出すという手法が一番マシと考えられます。上のクラスにはdestroyメソッドを定義してあります。

extern "C"
JNIEXPORT void JNICALL Java_WrapperTest_destroy
  (JNIEnv* env, jobject thisj, jstring strj)
{
    convertException(env, [=] () {
       Wrapper<WrapperTest>(env, thisj).destroy();
    });
}

finalizeは当てになりませんが、一応保険として最後に破棄するようにしておくことは悪くないと思います。そのためにもdestroyは何度呼び出してもOKな作りにしておく必要があるでしょう。

@Override
protected void finalize() throws Throwable
{
    destroy();
    super.finalize();
}

おわりに

最後に、JNIを使う際の注意点について簡単にまとめておきます。

  • Java Native Interface を使用する上でのベスト・プラクティスをじっくり読む。
  • (上にも書いてあるが)JNIの返り値は全力でグローバルにキャッシュする。
  • C++の例外が漏れ出さないように気を付ける。逆に、Java側の例外が発生したまま継続しない。
  • finalizeが呼び出されない可能性に気を付ける。
  • UnsatistiedLinkErrorで泣かない。
    • extern "C"を付けたか?
    • メソッド名・クラス名は正しいか?
    • javahを試したか?
    • System.loadLibraryを忘れていないか?
  • ぶっちゃけJNIは生産性が低いので、出来ることなら使わないほうがいい。(今日のオチ)

お粗末さまでした。

*1:C#にはunsafeなる魔術があるらしいですね…。

*2:Javaのlong型は64bitです。可搬性を考慮して、ポインタを保持する型は大きめに取ったほうがいいと思います。2038年問題を教訓に…。

JNIとC++例外

JNI関連の話が続きます。
今日は短いですが、JNIとC++例外について。
最近ではAndroid NDKもC++の例外(やRTTI)に対応していて、そのおかげで例外が必要なSTLなどのライブラリを自由に使えるようになっています。しかし、Java側はC++の例外を理解してくれないので、もしJNIを介した関数がネイティブ領域で例外をキャッチし損ねると、すぐにSegmentation Faultで落ちます。
解決策としては、JNIから呼び出されたC++側の内部処理を、noexceptな関数に閉じ込めてしまえばよいわけです。もちろんエラーが発見できないのもまずいので、JNIを使ってJava側の例外に変換してしまいます。

JNIEXPORT void JNICALL Java_Foo_bar
  (JNIEnv* env, jobject thisj)
{
    jni_util::convertException(env, [=] () {
        // do something with env and thisj
        // ...
        throw std::runtime_error("Hoge failed!"); // C++ exception thrown!
        // ...
    });
    // converts C++ exceptions to Java exceptions
}

地味にC++11のラムダキャプチャの恩恵を得られる例でもあります。関数1つを見るとわずかな差ですが、クラスが増えてくると管理も大変になってくるので、記述量を減らす努力は重要です。
実装は以下のように非常に単純。例によってGitHubにも同じものがあります。
https://github.com/meryngii/jni_util

inline void throwRuntimeException(JNIEnv* env, const char* what)
{
    jclass classj = env->FindClass("Ljava/lang/RuntimeException:");
    if (classj == nullptr)
        return;
    
    env->ThrowNew(classj, what);
    env->DeleteLocalRef(classj);
}

template <typename Func>
inline void convertException(JNIEnv* env, Func func) noexcept
{
    try {
        func();
    } catch (std::exception& e) {
        throwRuntimeException(env, e.what());
    } catch (...) {
        throwRuntimeException(env, "Unindentified exception thrown (not derived from std::exception)");
    }
}

ここで、Java側の例外が発生しても、C++側はすぐに終了しないという重要な問題があります。JNIの関数を呼び出したときには、常に例外が発生しているかどうかを検査しなければいけません。こんな感じで。

class JavaException : public std::exception { };

// 例外が発生しうるJNIコールの後で呼ぶ
inline void checkException(JNIEnv* env)
{
    if (env->ExceptionOccurred())
        throw JavaException();
}

上と組み合わせると、C++側で例外を利用したコードを書きつつも、Java側の事情にも配慮することができるというわけです。
今回は次回の布石で、次回はJavaから利用できるようC++のクラスをラップする方法について書きます。

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って何のために使うんだろう…。