Javaを呼出して動かす(jobject、jstring、jclass)

>JNIの使い方

基本

一度JVMを作成できるようになればそんなに難しいことは無いです(C/C++からJVMを呼出すまで、はこちら)。Java呼び出し処理をマルチスレッドで動かしたい時の制限や方法が少し面倒なのでまとめたいと思います。
サンプルコードをこのページGitHubで公開しています。

最低限な事しか書かないですが、検索するといろいろ出てくるので困らないと思います。
 JavaVM* jvm = NULL;
 JNIEnv* env = NULL;
 int status;
 try {
  status = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
 } catch (...) {
 }
でenvを介してJavaを呼び出すことになります。
このjvmはプロセスで一個しか作れないのでグローバルにしておくといいと思います。

Javaでリフレクションを使ってメソッド名を指定して実行するようなのと同じやり方になります。
jclass stringCls = env->FindClass("Ljava/lang/String;");
でStringのクラスを取得します(NULLチェックします)。
jmethodID mid_toString = 
   env->GetMethodID(stringCls, "toString", "()Ljava/lang/String;");
でStringのtoStringメソッドIDを取得(NULLチェックします)します。この第3引数はメソッドの引数と戻り値を表す文字列(シグネチャという)です。
これはコマンドラインで 
> javap -s java.lang.String
をやるとStringに関する全てのメソッドのシグネチャが分かります(少し面倒です)。
コンストラクタもメソッドIDとして取得します。
オブジェクト生成:
 jobject obj = env->NewObjectA(someCls, mid_constracter, args);
メソッド実行:
 env->CallVoidMethod(obj , mid_method , arg1 , arg2);
voidのメソッドかオブジェクトを返すメソッドか、intを返すメソッドか、で呼び出す関数が違います。(CallVoidMethodとかCallIntegerMethodとかになります)。
メソッドに引数を与える方法はいくつか用意されていますが、例に挙げた書き方が3つ目の引数以降がそのまま引数になるのでこれが簡単だと思います。これしか使ってないですが今のところ特に問題になったことは無いです。

NewObject関数について

jclassとそのコンストラクタのjmethodId、引数を指定してjobjectを生成する。これだけ。
jobject someObj = env->NewObject(someCls, constractor /*, arg0, arg1...*/); 

DeleteLocalRef関数について

Javaと違ってC/C++ではGCが効かないので確保したjobjectは解放する。具体的にはenvのNewObjectメソッドでjobjectを生成したときは必ずDeleteLocalRefで削除する。Cっぽいですね。
env->DeleteLocalRef(objとか);
ここまではすぐ理解できたがjstringとjclassについて知っておくべきことがあるので書いておく。

jclassも実はjobject

こんなん知らんよ、と思う。
jclass someCls = env->FindClass("package/someCls");
で作ったsomeClsは実はjobjectなのでNewObjectで生成したjobjectと同様に開放する必要がある。
env->DeleteLocalRef(someCls);

文字列を受け渡した後の破棄について

文字列の受け渡しは混乱するポイントだと思う。その後の破棄も混乱するので纏めておく。Javaの文字列は単なるStringというクラスなので他と同じように使うことも出来るが、よく使うのでjstringという特別な型が用意されている。jobjectと同様使い終わったらDeleteLocalRefで解放する必要がある。また、jstringからC++で使える文字列(jcharの配列)を取り出した場合、その解放も必要になる。

文字列を受け渡すパターンとその後処理方法をまとめる。
1.C/C++側の文字列からjstringを生成してJavaに渡すなどの利用をした。
後処理は「DeleteLocalRef」。
valueがC/C++側の文字列。jcharに無理やりキャストするのでWCHARでないといけない。
jstring valStr = env->NewString(reinterpret_cast(value),  
                                static_cast(valueLen));
// javaに渡すような処理;
env->DeleteLocalRef(valStr);
2.Java側からjstringを取得し、さらにGetStringCharsでC言語の文字列を取得して利用した。
後処理は「ReleaseStringChars」と「DeleteLocalRef」
const jchar* msgStr = env->GetStringChars(message, false);
// C/C++で使う処理;(後処理で消えるので必要ならコピーします)
env->ReleaseStringChars(message, msgStr);
env->DeleteLocalRef(message);
このjcharとはUTF-16の文字列で、ユニコードを使う設定でのVC++のWCHARとキャストで相互変換できる。ユニコードを使う設定の場合WCHARを使えば文字化けしないので難しいことは無い。(VC++もJavaもたまたまUTF-16なので偶然化けない状態とも言える)

プログラム全体を_TCHARで作っている場合、いくら「Unicode文字セットを使う」になっているとは言え、いきなり_TCHERからjcharへキャストするのは気が引ける。VC++の場合、こういうときは(使えるなら)T2Wマクロで事前にWCHARであることを保証すると良い。
(Unicode文字セットを使う設定の場合そのマクロは何もせずにポインタを返す関数になり、プリコンパイラによって式自体無くなるはず。)
今はVC++なのでこれで良いがその他の場合はjcharにキャストする前にUTF-16の文字列に変換する必要がありそうだ。

エクセプションの受け取り方

エクセプションを投げる可能性のあるメソッドを呼び出した直後に
if (env->ExceptionCheck()) {}
でエラー処理に入れます。
エラーだった場合
  jthrowable e = env->ExceptionOccurred();
  
  jclass throwableCls = env->FindClass( "Ljava/lang/Throwable;" );
  jmethodID mid_getLocalizedMessage = 
      env->GetMethodID( throwableCls, "getLocalizedMessage","()Ljava/lang/String;");
  jstring message = (jstring) env->CallObjectMethod( e, mid_getLocalizedMessage);
  errorMessage  = (_TCHAR*) env->GetStringChars( message, false);

  // errorMessageをコピーする
  env->ReleaseStringChars(message, errorMessage);
  env->DeleteLocalRef( message );
  env->DeleteLocalRef( e );
  env->ExceptionClear();
  m_errorCode = eCode;
などとすることが出来ます。関数にまとめておくと便利。
errorMessageがC++で使えるエラーメッセージ(std::basic_string<_TCHAR> として定義)。

アクセス違反?

ついでに、JNIをVC++で動かしてると大量にアクセス違反が起こるのが見えるかもしれない。これはJava の仕様のようで、動作が止まったり結果が変にならなければそんなに気にするようなことではない。EclipseなどのJavaアプリケーションをVSのデバッガで見ると同様にアクセス違反が起こるのが見えるので。

0 件のコメント:

コメントを投稿