C/C++からJavaのライブラリを呼ぶ with JNI

JavaのライブラリをC/C++から呼ぶ方法のメモ.
フルのソースコートこちら
参考にしたページ: シンプルな C言語からJava言語を呼び出すJNIサンプル


ここでは以下のようなJavaのクラスをC++から利用することを想定する.

package my.hoge;

public class Hoge {
	public Hoge()
	{
	}

	public void printHoge(){
		System.out.println("Hoge!");
	}

	public String getHoge(){
		return new String("Hoge");
	}

	public int add(int i, int j)
	{
		return i + j;
	}

	public String getMsg(String msg){
		return new String("Hoget is " + msg);
	}
}


JNIを使ってあるクラスインスタンスのメソッドを呼ぶ時に必要な手順は以下の通り

  1. JavaVMの起動
  2. クラスの検索
  3. インスタンス生成(= メソッドの呼び出しと同じ)
  4. メソッドを呼び出して遊ぶ
  5. JavaVMの終了

1. JavaVMの起動
JavaVMの起動に当たってまずは, JavaVM起動のオプションを指定します。よくわからないので借りてきたものそのまま
おそらくjarファイルとかにまとめてあるライブラリの時とかは変える必要がある?

	JNIEnv *jnienv;
	JavaVM *javavm;
	JavaVMInitArgs vm_args;
 
	JavaVMOption options[1];
	options[0].optionString = "-Djava.class.path=.";
	vm_args.version = JNI_VERSION_1_2;
	vm_args.options = options;
	vm_args.nOptions = 1;
	vm_args.ignoreUnrecognized = true;

次に実際にJavaVMを起動する(?)

	int result = JNI_CreateJavaVM(&javavm, (void **)&jnienv, &vm_args);

ここまでの流れはおそらくテンプレで良いはず.

2. クラスの検索
次にこのコードで使いたいクラスを引っ張ってくる必要があります.
このコードはC/C++なのでJavaのコードのようにはいかず, 使いたいクラスを文字列で表す必要があります.

	jclass cls = jnienv->FindClass("my/hoge/Hoge");

3. インスタンス生成
2でクラスの情報は引っ張ってこれたので次にこれをインスタンス化します.
インスタンス化は以下の流れで行います.

  1. クラス情報からメソッドのメソッドIDの取得
  2. メソッドIDを指定してNewObjectメソッドの呼び出し.

1のメソッドのID取得は以下のように行います。

	jmethodID cns = jnienv->GetMethodID(cls, "<init>", "()V");

GetMethodIDは第一引数に取得したいメソッドを持つクラスであるjclass,
第二引数にIDを取得したいメソッド名, 第3引数に取得したいメソッドのシグネチャを指定します.
これは次のインスタンスメソッドの呼び出しでも多用します.


第3引数のシグネチャについては, 「"()V" = 引数なしでvoid型を返すメソッド」という表現になります.
逐一どんなシグネチャか考えるのは面倒なので、手っ取り早く既存のクラスのシグネチャを調べる方法としてjavapコマンドが利用できます.

$ javap -s my/hoge/Hoge

このように入力すると以下のように、各々のメソッドのシグネチャを出力してくれます.
今回のメソッドはつまりmy.hoge.Hoge()なので"()V"で良い、ということになります.

public class my.hoge.Hoge extends java.lang.Object{
public my.hoge.Hoge();
  Signature: ()V
public void printHoge();
  Signature: ()V
public java.lang.String getHoge();
  Signature: ()Ljava/lang/String;
public int add(int, int);
  Signature: (II)I
public java.lang.String getMsg(java.lang.String);
  Signature: (Ljava/lang/String;)Ljava/lang/String;
}

なおシグネチャーについてはここにより詳しいことが載ってます.


このようにメソッドのメソッドID(cns)が取得できたら, NewObjectを呼びます

	jobject obj = jnienv->NewObject(cls, cns);

第2引数に先ののメソッドID, 第1引数に取得したjclassを入れるとobjとしてインスタンス(のようなもの?)が得られます. 以降、これを指定することでそのインスタンスを扱うことができます.


3.メソッドを呼び出して遊ぶ
JavaVMを起動して使いたいクラス情報を入手してインスタンスを生成したらあとはメソッドを好き勝手に呼び出すだけ!
一般的にメソッド呼び出しは以下のような流れで行います

  1. 呼び出したいメソッドIDの取得
  2. CallMethodでメソッドの呼び出し

メソッドの時と同じく、メソッド呼び出しにはメソッドIDをクラス情報から引っ張ってくる必要があります.
先にもやったようにGetMethodID()を呼びます. 今回のHogeクラスではこの部分はそれぞれ以下のようになります.

// void printHoge()
jmethodID printHogeId = jnienv->GetMethodID(cls, "printHoge", "()V");
// int add(int i, int j)
jmethodID addId = jnienv->GetMethodID(cls, "add", "(II)I");
// string getHoge()
jmethodID getHogeId = jnienv->GetMethodID(cls, "getHoge", "()Ljava/lang/String;");
// string getMsg(string msg)
jmethodID getMsgId = jnienv->GetMethodID(cls, "getMsg", "(Ljava/lang/String;)Ljava/lang/String;");


メソッドIDをゲットしたら、これとインスタンス情報を元にメソッドを呼び出します.
このとき使う関数が"CallMethod()"として用意されています.
は返値の型を表すようでIntやらDoubleやらを指定します.
今回の場合それぞれのメソッドについて以下のように呼び出すことができます.

// printHoge();
jnienv->CallVoidMethod(obj, printHogeId);

// int addResult = add(10, 100);
int addResult = (int)jnienv->CallIntMethod(obj, addId, 10, 100);

// string jstr1 = getHoge();
jstring jstr1 = (jstring)jnienv->CallObjectMethod(obj, getHogeId);

// string jstr2 = getMsg("Hell to the Sack code!");
jstring jstr2 = (jstring)jnienv->CallObjectMethod(obj, getMsgId, jnienv->NewStringUTF("Hell to the Sack code!"));

3.5 返値と引数の型
3でメソッドを呼び出すことはできますが, 引数に与える型や返値は若干特殊な形で与えられます.
詳細はJNIのドキュメントにある「第3章 JNIの型とデータ構造」に詳細があります.
ここでは特に今回用いているもののみを対象としてメモっときます.

  • int to jint, jint to int

基本的にキャストで何とかなる模様(add()の呼び出し部分参照).

  • jstring to char*

getHoge()やgetMsg()の返値としてjstringが返ってきています. ここからcharに変換するためにここでは以下のような手順を踏んでいます.

	const char* str1 = jnienv->GetStringUTFChars(jstr1, 0); // 実はGetStringCharsでもよい?
	char* resStr1 = strdup(str1);
	jnienv->ReleaseStringUTFChars(jstr1, str1);

GetStringUTFCharsとReleaseStringUTFCharsはセットで使う必要があります. のでここでは早々にstrdupしています.

  • char* to jstring

getMsg()の引数のようにC/C++の側から文字列を渡したいときは以下のようにNewStringUTF()を用いています.

jnienv->NewStringUTF("Hell to the Sack code!") // => jstring

4.JavaVMの終了
何らかの処理が終わって、終了するときにはついでにJavaVMも終わらす必要があります.

	result = javavm->DestroyJavaVM();