S-JIS[2007-10-13] 変更履歴

他言語からJava実行

JNIを使って、C言語/C++からJavaVMを実行してメソッドを呼び出すことも出来る。

C言語またはC++内でJavaVMを生成(取得)し、後は通常のJNIと同様にコーディングする。


Windowsでの実行環境

リンク時にjvm.libが必要。→(VC++)ライブラリの追加方法
jvm.libは「%JAVA_HOME%\lib」に在る。→(VC++)ライブラリのパス指定方法

実行時にjvm.dllが必要。
jvm.dllは「%JAVA_HOME%\jre\bin\client」や「%JAVA_HOME%\jre\bin\server」に在る。
(VC++)デバッグ実行時の環境変数PATHの追加方法


JavaVMの生成・破棄

JavaVMのオブジェクトを作成するには、jni.hで宣言されているJNI_CreateJavaVM関数を使う。
これに渡すための引数の初期化をJNI_GetDefaultJavaVMInitArgs関数で行う。

生成したJavaVMはDestroyJavaVM関数を呼び出すことによって終了させる。
この関数が呼び出されても、VM内で動いているユーザースレッドが無くなるまで待機する。
ロック等のリソースが自動的に解放されることはないので、その解放はプログラマーの責任。

sample.cpp:

#include <jni.h>

//CreateJavaVMによって実体を取得する
JavaVM *vm  = NULL;
JNIEnv *env = NULL;
int createVM()
{
	JavaVMInitArgs vm_args;
//	vm_args.version = JNI_VERSION_1_2;
	vm_args.version = JNI_VERSION_1_6;
	jint ret = JNI_GetDefaultJavaVMInitArgs(&vm_args);
	if (ret != JNI_OK) {
		printf("vm init args error:%d\n", ret);
		return ret;
	}
	printf("vm_args.version:%x\n", vm_args.version);

	JavaVMOption options[4];
	options[0].optionString = "-Djava.class.path=C:\\workspace32\\sample\\classes";
	options[1].optionString = "-Djava.compiler=NONE";
	options[2].optionString = "-Djava.library.path=C:\\Program Files\\Java\\jdk1.6.0\\lib";
	options[3].optionString = "-verbose:jni";

	vm_args.nOptions = 1; //上で4つのオプションを定義しているが、1つしか使わない
	vm_args.options  = options;

	//vmとenvを取得する
	ret = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args);
	if (ret != JNI_OK) {
		printf("create vm error:%d\n", ret);
		return ret;
	}

	return 0;
}
void endVM()
{
	if (vm != NULL) {
		vm->DestroyJavaVM();
	}
	vm  = NULL;
	env = NULL;
}
int main(int argc, char *argv[])
{
	int ret = createVM();

	system_out_println(env, "JavaVM call test ok!");

	endVM();

	return ret;
}

実験用の関数。System.out.println()を呼び出して文字列(UTF-8)を出力する。

void system_out_println(JNIEnv *env, const char *msg)
{
	jclass sysj = env->FindClass("Ljava/lang/System;");
	if (sysj==NULL){
		printf("get System error\n");
		return;
	}

	jfieldID f = env->GetStaticFieldID(sysj, "out", "Ljava/io/PrintStream;");
	if (f==NULL){
		printf("get PrintStream error\n");
		return;
	}

	jobject outj = env->GetStaticObjectField(sysj, f);
	if (outj==NULL){
		printf("get out error\n");
		return;
	}

	jclass clsj = env->GetObjectClass(outj);

	jmethodID println = env->GetMethodID(clsj, "println", "(Ljava/lang/String;)V");
	if(println==NULL){
		printf("get println error\n");
		return;
	}

	jstring strj = env->NewStringUTF(msg);
	env->CallVoidMethod(outj, println, strj);
}

JavaVMの取得

生成されているJavaVMを取得するにはJNI_GetCreatedJavaVMs関数を使う。
ただし、これは自分で生成したVMしか列挙されない模様。つまり、全然無関係なプロセスとして動いているjavaプログラムのVMが取得できるわけではない、ということ。

void enumVM()
{
	JavaVM *vm;
	jsize ct;
	jint ret = JNI_GetCreatedJavaVMs(&vm, 256, &ct); //最大256個取得
	printf("GetVM:%d\n", ret);
	printf("count:%d\n", ct);
}

ただまぁ、普通はJavaVMはプロセスに1つだけあればいいので、それを取得するだけでいいはず。

JavaVM* getVM()
{
	JavaVM *vm;
	jsize ct;
	jint ret = JNI_GetCreatedJavaVMs(&vm, 1, &ct);
	if (ret != JNI_OK) {
		return NULL;
	}

	return vm;
}

JNIEnvの取得

JNIEnvはスレッド毎にJavaVMから取得し直す必要がある。
JavaVMが分かっていない場合はJNI_GetCreatedJavaVMs関数を使って取得する。

JNIEnv* getJNIEnv(int *attach)
{
	*attach = 0;

	JavaVM *vm = getVM();
	if (vm == NULL) {
		return NULL;
	}

	JNIEnv *env;
	jint ret = vm->GetEnv((void**)&env, JNI_VERSION_1_6);
	if (ret == JNI_OK) {
		return env;
	}

	ret = vm->AttachCurrentThread((void**)&env, NULL);
	if (ret == JNI_OK) {
		*attach = 1; //自分でアタッチしたという印
		return env;
	}

	return NULL;
}
void detachJNIEnv()
{
	getVM()->DetachCurrentThread();
}

スレッドがVMにアタッチされていなかった場合、AttachCurrentThread関数によってアタッチする。
その場合は使用後にDetachCurrentThread関数でデタッチしなければならない。(スレッドが終了しないとJavaVMが終了できないから)
しかしデタッチすべきかどうかの判断は、自分でアタッチしたかどうかを覚えておくしかない模様…。

基本的に、
自分でCreateJavaVM関数によってVMを生成した場合、そのスレッドはVMにアタッチされているのでGetEnv()だけ呼べば取得できる。
マルチスレッド化して別スレッドから取得したい場合、初回のGetEnv()はエラーになるので アタッチする必要がある。
一度アタッチすれば、デタッチするまでの間はGetEnv()で取得することが出来る。

なお、アタッチ・デタッチを繰り返すとJavaVM側のスレッドは増えるようなので(ネイティブ側が同一スレッドであっても無関係)、リソースの事を考えるならアタッチは1回だけにするよう実装すべきかな。


ネイティブで実行されているかどうか?

JDK1.5で導入されたThreadInfoというクラスに、isInNativeというメソッドがある。
これは、そのスレッドがネイティブコードを実行中であればtrueを返す。ネイティブから呼ばれているかどうかやネイティブコードを経由しているかどうかを知るためのものではない。
という訳で、Java側のカレントスレッドでこのメソッドを呼び出してみると、常にfalseが返る。

	ThreadMXBean mx = ManagementFactory.getThreadMXBean();

	Thread t = Thread.currentThread();
	long id = t.getId();
	ThreadInfo ti = mx.getThreadInfo(id);
	System.out.println(id + ":" + "inNative:" + ti.isInNative()); //常にfalse

適当に一覧表示する例:

	for (int i = 0; i < 20; i++) {
		try {
			ThreadInfo ti = mx.getThreadInfo(i);
			System.out.println(i + ":" + "inNative:" + ti.isInNative());
			//ネイティブで実行しているスレッドがあればtrueになる
		} catch (Exception e) {}
	}

getThreadInfo()で存在しないスレッドIDを指定すると例外が発生するので、ここでは無視している。
で、ネイティブ側でアタッチ・デタッチを繰り返すと、その分スレッドが増えているように見えた。(JNI1.6)


グローバル参照

JNI内部でJavaのオブジェクトを使う場合、オブジェクトのリファレンスは(基本的に)ローカル参照というスコープになっている。
ローカル参照は JNI呼び出しが終了すると自動的に解放されるため、ローカル参照のオブジェクトは静的(JNI呼び出し間の受け渡しや別スレッドへの受け渡し用)に保持することが出来ない。

そういった目的で使用する場合、オブジェクトのスコープをグローバル参照に変更する。
グローバル参照のオブジェクトはJNI呼び出しが終了しても解放されないため、使い終わったら明示的に参照を終了させてやらなければならない。(JavaVMが終了するときには当然解放されるけど)

jobject g_object;

void setGlobal(JNIEnv *env, jobject obj)
{
	g_object = env->NewGlobalRef(obj);
}

void releaseGlobal(JNIEnv *env)
{
	env->DeleteGlobalRef(g_object);
	g_object = NULL;
}

ローカル参照もグローバル参照もjobject型であることに変わりはないので、Javaのオブジェクトとしては区別せずに扱える。


リスナーを使用する例

Java側からリスナーをネイティブ側に登録し、ネイティブ側で何かあったときにリスナーのメソッドを呼び出したい事がある。

この場合、まず、リスナーオブジェクトをグローバル参照にして保持する。そしてJNI呼び出しを一旦終了する。
その後ネイティブ側でリスナーに通知したいイベントが発生したら、該当スレッドのJNIEnvを取得し、それを使ってグローバル参照にしたリスナーのメソッドを呼び出してやる。

以下、VC++2005でタイマーを使ってリスナーのメソッドを呼び出す例。

ListenerSample.java:

public class ListenerSample {

	public static void main(String[] args) throws InterruptedException {
		System.out.println("main start");

		ListenerSample obj = new ListenerSample();
		setListener(obj);

		for (int i = 0; i < 10; i++) {
			synchronized (obj) {
				System.out.println("main:" + i);
			}
			Thread.sleep(500);
		}
		System.out.println("main end");
	}

	/** C++に当インスタンスを登録する */
	protected static native void setListener(ListenerSample listener);

	/** C++から呼ばれるメソッド */
	public synchronized void listen(int n) {
		System.out.println("listen:" + n);
	}
}

ListenerSample.cpp:

#include <windows.h>
#pragma comment(lib,"WinMM.lib")

#include "jp_hishidama_sample_jni_ListenerSample.h"

UINT g_uiTimerID;
int g_value = 0;
//リスナー呼び出し
void call_listener(jobject listener)
{
	int attach;
	JNIEnv *env = getJNIEnv(&attach);
	if (env==NULL) return;

	{
		jclass clsj = env->GetObjectClass(listener);
		if(clsj==NULL) goto end;

		//リスナーのlistenメソッドを取得する
		jmethodID func = env->GetMethodID(clsj, "listen", "(I)V");
		if(func==NULL) goto end;

		g_value++;

		//リスナーのlistenメソッドを呼び出す
		env->CallVoidMethod(listener, func, g_value);

		//今回の例では、リスナーを3回呼んだら終わり
		if (g_value >= 3) {
			::timeKillEvent(g_uiTimerID); //タイマー終了
			env->DeleteGlobalRef(obj);    //グローバル参照終了

			//ここで最後に1回だけデタッチする方法でも可
			//getVM()->DetachCurrentThread();
		}
	}
end:
	if (attach) {
		//今回は面倒なので毎回デタッチ
		getVM()->DetachCurrentThread();
	}
}

void CALLBACK TimerProc(UINT uTimerID, UINT uMsg, DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2)
{
	if (uTimerID == g_uiTimerID) {
		jobject listener = (jobject)dwUser;
		call_listener(listener);
	}
}
//リスナー登録
JNIEXPORT void JNICALL Java_jp_hishidama_sample_jni_ListenerSample_setListener
  (JNIEnv *env, jclass, jobject obj)
{
	jobject listener = env->NewGlobalRef(obj); //グローバル参照取得

	//1秒に1回、別スレッドでTimerProc関数を呼び出す
	g_uiTimerID = ::timeSetEvent(1000,1000, TimerProc, (DWORD_PTR)listener, TIME_PERIODIC);
}

この例では、Windowsのマルチメディアタイマーを使って別スレッドで定期的にリスナーのメソッドを呼び出している。
(ちなみに、マルチメディアタイマーで呼ばれるコールバック関数の引数の型がVC++2005ではDWORDからDWORD_PTRに変わっている)

実行結果:

main start
main:0
main:1
main:2
listen:1
main:3
main:4
listen:2
main:5
main:6
listen:3
main:7
main:8
main:9
main end

参考:


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