meryngii.neta

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

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++のクラスをラップする方法について書きます。