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)
変換前 | 変換後 |
---|---|
_ | _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 の不具合を裏付ける。メソッドをオーバーライドしない場合はマングリング名の解決処理が行われないため、ずっと見過ごされてきた不具合なんだろうと思う。