S-JIS[2007-10-13] 変更履歴
JNIを使って、C言語/C++からJavaVMを実行してメソッドを呼び出すことも出来る。
C言語またはC++内でJavaVMを生成(取得)し、後は通常のJNIと同様にコーディングする。
リンク時に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のオブジェクトを作成するには、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を取得するには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はスレッド毎に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