Java でネイティブメソッドをオーバーライドして、インナークラスを引数で渡す場合の注意点

Java で以下のように native メソッドを定義する。

public class JniTest {
    static {
        System.loadLibrary("JniTest");
    }
    class Inner {
    }
    native void hello(Inner inner);

    public static void main(String[] args) {
        JniTest test = new JniTest();
        test.hello(test.new Inner());
    }
}

特に何をしているわけではないんだけど、ポイントはネイティブメソッドにインナークラスを渡しているところ。

これは javah でインターフェイスを出力すると、以下のようになる。

/*
 * Class:     JniTest
 * Method:    hello
 * Signature: (LJniTest/Inner;)V
 */
JNIEXPORT void JNICALL Java_JniTest_hello
  (JNIEnv *, jobject, jobject);


ここまでは問題ない。

さて、Java 側で以下のように native メソッドをオーバーライドしてみた。

public class JniTest {
    static {
        System.loadLibrary("JniTest");
    }
    class Inner {
    }
    native void hello(Inner inner);
    native void hello(int i); // これを追加

    public static void main(String[] args) {
        JniTest test = new JniTest();
        test.hello(test.new Inner());
    }$
}

このクラスを javah にかけると以下の様なインターフェイスを出力する。

/*
 * Class:     JniTest
 * Method:    hello
 * Signature: (LJniTest/Inner;)V
 */
JNIEXPORT void JNICALL Java_JniTest_hello__LJniTest_Inner_2
  (JNIEnv *, jobject, jobject);

/*
 * Class:     JniTest
 * Method:    hello
 * Signature: (I)V
 */
JNIEXPORT void JNICALL Java_JniTest_hello__I
  (JNIEnv *, jobject, jint);

メソッド名の後ろに引数の型が付与され、関数名がマングリングされている。これはこれで良いのだが、javah のマングリングの方法が不正なため、このままコンパイル、実行すると UnsatisfiedLinkError が発生する。

Exception in thread "main" java.lang.UnsatisfiedLinkError: JniTest.hello(LJniTest$Inner;)V
        at JniTest.hello(Native Method)
        at JniTest.main(JniTest.java:13)


以下、マングリングの変換規則(VMソースコードから)。

変換前 変換後
_ _1
; _2
[ _3
/ _
$ _00024
非ASCII _0%04x

特別な文字がアンダースコア付きのシーケンスに変換される。文字が $ とコードポイントが 127 超過の場合は、sprintf(buf, "_0%04x", ch) 相当の 16 進数文字列としてエスケープされる。


つまり、インナークラスの完全修飾クラス名には $ が含まれるため

Java_JniTest_hello__LJniTest_Inner_2

は本当は

Java_JniTest_hello__LJniTest_00024Inner_2

でなければならないはずだ。手動でこのように変更することで、UnsatisfiedLinkError は発生しなくなる。

なお、javah で出力したコメントの

/*
 * Class:     JniTest
 * Method:    hello
 * Signature: (LJniTest/Inner;)V
 */

シグネチャが、本来は UnsatisfiedLinkError 発生時の (LJniTest$Inner;)V となっていなければならないはずなのに、(LJniTest/Inner;)V となっているのも javah の不具合を裏付ける。メソッドをオーバーライドしない場合はマングリング名の解決処理が行われないため、ずっと見過ごされてきた不具合なんだろうと思う。