S-JIS[2008-02-02] 変更履歴

JNIのエラー

JNIはJavaとC言語/C++間をつなぐので、ミスをするとJavaだけで作っているよりも複雑な障害が発生する。


UnsatisfiedLinkError

System.loadLibrary()で指定したJNIのライブラリー(dllやso)が見つからないとUnsatisfiedLinkErrorが発生する。

		System.loadLibrary("SampleJNI");
java.lang.UnsatisfiedLinkError: no SampleJNI in java.library.path
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1682)
	at java.lang.Runtime.loadLibrary0(Runtime.java:823)
	at java.lang.System.loadLibrary(System.java:1030)
	at jp.hishidama.sample.jni.JniSample.<clinit>(JniSample.java:20)
Exception in thread "main"

java.library.path」が、環境変数PATH(実行ファイル(dllファイルも含む)を探す場所)や環境変数LD_LIBRARY_PATH(UNIXでsoファイルを探す場所)を意味している。

この例外(エラー)が発生するのは、たいていの場合、DLLやSOのファイル名が間違っているか、そのファイルが存在している場所のパス指定が間違っている。

ファイル名は、loadLibrary("SampleJNI")の場合、Windowsなら「SampleJNI.dll」、UNIXなら「libSampleJNI.so」。

パスはSystem.getProperty("java.library.path")で確認できる。


戻り値のクラス違い

JNIでは特殊なクラス(jclassとかjthrowableとか)を除けばString(jstring)くらいしか専用のクラスは無いので、String以外の普通のクラスは全てjobjectで扱う。

JNI用のヘッダーファイルはJavaソースからjavahで生成するので、メソッド用の引数のクラスはまず間違えない。(呼び出し側で違うクラスを指定しても、当然javacのコンパイルエラーになる 。あるいは無理矢理キャストしてもClassCastExceptionになる)
JNI側で引数のクラスを勘違いして別クラスとして扱っても、GetObjectClass()やらGetMethodID()やらGetFieldID()やらを使っている分には致命的なことにはならない。(これらの関数では間違っていればNULLが返ってくるので、エラーチェックをちゃんとやっていれば暴走するような事にはならない。同名メソッド・同名フィールドがあれば正しく動くが…それは普通に意図した動作だよね^^;)

しかし戻り値のクラスは話が違ってくる。
JNI上ではjobjectなので、元のメソッドで指定されているクラスとは異なるどんなオブジェクトでも返すことが出来る。(コンパイル時点でチェックされない)
Java内であれば、一旦Objectクラスに代入して別の無関係なクラスに入れ直そうとしてもClassCastExceptionになってくれるが、JNIからJavaへ帰るときにはキャストのチェックは行われないようだ。

平然と実行される例:

	public static void main(String[] args) {
		System.loadLibrary("SampleJNI");

		Long val = retLong();

		System.out.println(val.getClass());
		System.out.println(val.intValue());
	}

	protected static native Long retLong();
JNIEXPORT jobject JNICALL Java_jp_hishidama_sample_jni_JniSample_retLong
  (JNIEnv *env, jclass)
{
	jclass clsj = env->FindClass("Ljava/lang/Integer;");
	jmethodID cns = env->GetMethodID(clsj, "<init>", "(I)V");
	jobject obj = env->NewObject(clsj, cns, 123);
	return obj;
}

Longを返すように指定されているのに、この例ではIntegerのインスタンスを作って返している。
なのに平然と正常終了する!

class java.lang.Integer
123

たまたまメソッドとかフィールドが同じだから上手く(?)動いているのだろうか。


これがBigIntegerだったりStringだと正しい動作にはならない。(でも落ちもしない!

JNIEXPORT jobject JNICALL Java_jp_hishidama_sample_jni_JniSample_retLong
  (JNIEnv *env, jclass)
{
	jclass clsj = env->FindClass("Ljava/math/BigInteger;");
	jmethodID cns = env->GetMethodID(clsj, "<init>", "(J)V");
	jobject obj = env->NewObject(clsj, cns, 123);
	return obj;
}
class java.math.BigInteger
580939912
JNIEXPORT jobject JNICALL Java_jp_hishidama_sample_jni_JniSample_retLong
  (JNIEnv *env, jclass)
{
	return env->NewStringUTF("test!");
}
class java.lang.String
580931616

何の数字が表示されてるんだか分からないけど、バイトコードでは色々なものを番号で表しているから、適当にそれが表示されているのかなぁ。
どうも実体のクラスがそれなりに同様のメソッドを持っていると、それが無理矢理実行されるようだ。
適当にとにかく動くっちゅうのはちょっと恐ろしい話だが。


しかし本当に実体に存在しないメソッドを呼び出すと、(JRE1.5までは)JavaVM自体が落ちる。

	public static void main(String[] args) {
		System.loadLibrary("SampleJNI");
		ZZZ val = retLong();
		System.out.println(val.getClass());
		System.out.println(val.getData());
	}

	protected static native ZZZ retLong();
class ZZZ {
	public String getData() {
		return "zzz";
	}
}
class java.lang.Integer	getClass()はちゃんと実行される	↓getData()はIntegerには存在しないので、JVM自体が落ちる
#
# An unexpected error has been detected by HotSpot Virtual Machine:
#
#  Internal Error (43113F32554E54494D45110E435050030A), pid=2856, tid=2112
#
# Java VM: Java HotSpot(TM) Client VM (1.4.2_13-b06 mixed mode)
# An error report file with more information is saved as hs_err_pid2856.log
#
# If you would like to submit a bug report, please visit:
#   http://java.sun.com/webapps/bugreport/crash.jsp
#

JRE1.6ではチェックが強化されたようで、この場合は例外発生になる。

class java.lang.Integer
Exception in thread "main" java.lang.AbstractMethodError: jp.hishidama.sample.jni.ZZZ.getData()Ljava/lang/String;
	at jp.hishidama.sample.jni.JniSample.main(JniSample.java:13)

クラスがNULL

JNIのコーディングでは、GetMethodID()・GetFieldID()やNewObject()など、jclassを引数にとる関数がある。
これらの関数では、jclassにNULLを渡すとJavaVM自体が落ちる。

#
# An unexpected error has been detected by Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x6d895268, pid=3772, tid=1528
#
# Java VM: Java HotSpot(TM) Client VM (1.6.0-b105 mixed mode, sharing)
# Problematic frame:
# V  [jvm.dll+0xd5268]
#
# An error report file with more information is saved as hs_err_pid3772.log
#
# If you would like to submit a bug report, please visit:
#   http://java.sun.com/webapps/bugreport/crash.jsp
#

jvm.dll+〜」の部分が実際に落ちた場所を示している模様。
関数の引数のjclassにNULLを渡して試した結果、以下の様になった。

  1.4.2_13-b06 1.5.0_09-b03 1.6.0-b105
NewObject() jvm.dll+0x77a98 jvm.dll+0x8a9c2 jvm.dll+0xc8d80
GetMethodID() jvm.dll+0x785e0 jvm.dll+0x8b29f jvm.dll+0xd5268
GetFieldID() jvm.dll+0x7b229 jvm.dll+0x8df13 jvm.dll+0xd0e20

まぁこんないかにもバージョンによって変化しそうな値に頼るよりは、各関数の戻り値のNULLチェック(特にFindClass()の)をちゃんとすべきでしょうなぁ。

	jclass clsj = env->FindClass("Ljava/lang/Integer;");
	if (clsj == NULL) {
//		env->FatalError("FindClass('Integer')");
		return NULL;
	}

FatalError関数を使った場合:

FATAL ERROR in native method: FindClass('Integer')
	at jp.hishidama.sample.jni.JniSample.retLong(Native Method)
	at jp.hishidama.sample.jni.JniSample.main(JniSample.java:13)

自由にメッセージを出したいなら、FatalError関数を使うのが便利。

FatalError関数を使わない場合:(FindClass()は、失敗すると自動的に例外を発生させる)

Exception in thread "main" java.lang.NoClassDefFoundError: Ljava/lang/Integer;
	at jp.hishidama.sample.jni.JniSample.retLong(Native Method)
	at jp.hishidama.sample.jni.JniSample.main(JniSample.java:13)

JNIの作成手順へ戻る / JNI実装方法へ行く / Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま