EclipseとVisualStudioをつかってJNIプログラムを作る手順
ちょっと、JNIを使って、Java用のユーティリティを作ろうと思ったら、意外とチェックポイントが多かったので、サンプルプログラムを作り、手順をメモっておく。
題材
題材は何でもよかったが、手ごろな感じがするので、OSのメモリ使用量を調べるJNI用DLLを作成する。
概要
MemoryCheckerクラスは、Javaのウィンドウで、TextAreaにメモリ情報を出力するメソッドを追加した、MemoryListenerTextAreaを配置。
メモリ使用量を調査する抽象クラス、AbsMemoryManagerから、JVMのメモリ調査用クラスJavaMemoryManagerと、OSのメモリ調査用クラスJNIMemoryManagerを派生させる。
メモリ使用量データ保持用にMemoryStatusクラスを用意
動きとしては、MemoryChecker(ウィンドウ)から、JNIMemoryManagerを生成する。JNIMemoryManagerは、DLLをロードし、MemoryStatusのインスタンスを渡す。DLLでは、メモリ量を指定インターバル毎に調査して、保持するMemoryStatusのインスタンスにセットし、changeメソッドを呼ぶ。これが順に伝わり、TextAreaに状況を表示する。
出来上がりはこんな感じ。
作成手順(Eclipse側)
DLL呼び出し元クラス
- DLLをロードする記述を追加
- DLLの関数を呼び出すメソッドをnative宣言
public class JNIMemoryManager extends AbsMemoryManager { /** DLLをロード */ static { System.loadLibrary("MemStat"); } public JNIMemoryManager(IMemoryStatusListener listener, long inverval) { super(listener, inverval); } public void changeStatus() { this.listener.statusChenged(getMemoryStatus()); } /** DLLからMemoryStatusを取得 */ public native MemoryStatus getMemoryStatus(); /** DLLにMemoryStatusをセット */ public native void setMemoryStatus(MemoryStatus memstat); /** DLLのメモリ調査を開始 */ public native void run(); /** DLLのメモリ調査を終了 */ public native void stop(); }
DLL指定の拡張子部分、dll、soは、Javaのライブラリが勝手に補完してくれるので不要。
C/C++ ヘッダーファイルを作成
- コンパイル済みJavaクラスから、javah コマンドを利用して、C/C++用のヘッダファイルを生成
- C/C++での処理用に利用するJavaクラスのシグネチャをjavapコマンドを利用して取得
上記の1.は最低限必要な作業。2.はDLL側からJavaのクラスのメソッドを呼び出すときに、メソッド名と、戻り値、引数のシグネチャが必要だが、この記述がJavaでの通常の記述とはことなるので、簡単に取得するために行う。
また、上記作業をつどコマンドラインから実行するのも面倒なので、以下の手順でAntのタスクとして登録しておく。
プロジェクトから、新規作成-ファイルで、build.xml という名前のファイルを追加し、以下の内容を記述する。
<project basedir="./"> <property name="cpdir" value="${basedir}\bin"/> <target name="create c header file"> <!-- JNI用のCヘッダーファイルを生成 -eg batch file cd "C:\Program Files\eclipse3.3.2\workspace\JNISample\bin" javah -jni info.typea.jnisample.mem.JNIMemoryManager --> <exec executable="javah"> <arg value="-classpath"/> <arg value="${cpdir}"/> <arg value="-jni"/> <arg value="info.typea.jnisample.mem.JNIMemoryManager"/> </exec> </target> <target name="print signeture (MemoryStatus)"> <!-- JNI用シグネチャ確認(標準出力に出力) -eg batch file cd "C:\Program Files\eclipse3.3.2\workspace\JNISample\bin" javap -private -s -classpath . info.typea.jnisample.mem.MemoryStatus --> <exec executable="javap"> <arg value="-private"/> <arg value="-s"/> <arg value="-classpath"/> <arg value="${cpdir}"/> <arg value="info.typea.jnisample.mem.MemoryStatus"/> </exec> </target> </project>
あとは、Eclipseのメニュー- Window- Show View から、Antを開くと、今の設定が表示されるので、該当を選択して、実行ボタンを押すとヘッダーファイルが作成される。
今回の例では、パッケージを展開したやたら長い名前のヘッダーファイルが作成される。
info_typea_jnisample_mem_JNIMemoryManager.h
とりあえず、ここまででEclipse側は終了。
作成手順(VisualStudio側)
プロジェクトの作成
VisualStudio側では、VC++を利用して、MFC拡張DLLを作成する。
プロジェクト名を、上記Java側でロードするDLLの名称にする。
MFC拡張DLLを指定
JNI用ヘッダーファイルの取り込み
先ほど作成した「info_typea_jnisample_mem_JNIMemoryManager.h」を、作成されたプロジェクトの適当なところにコピーして、ソリューションエクスプローラのヘッダーファイルのコンテキストメニューから、既存の項目を選んで取り込む。
実装用に、「info_typea_jnisample_mem_JNIMemoryManager.cpp」ファイルを、同じくソリューションエクスプローラのソースファイルから新しい項目として追加する。
環境設定
JNI用のインクルードファイルの場所をVisualStudioに設定する。
メニュー-ツール-オプションでオプションダイアログを開き、「プロジェクトおよびソリューション-VC++ディレクトリ」を選択し、「ディレクトリを表示するプロジェクト」に、「インクルードファイル」を選択
ここに、JavaSDKのjni.hを含む、includeディレクトリ、jni_md.hを含むwin32ディレクトリを指定する。
ちなみに、今回の環境だと、
- C:\Program Files\Java\jdk1.6.0_06\include
- C:\Program Files\Java\jdk1.6.0_06\include\win32
の2つとなる。
JVM引数の指定(Eclipse側)
テスト段階では、コンパイルされたDLLをEclipseのデバッグ環境から認識させるために、以下の設定を行う。
実行するクラスのコンテキストメニュー-Debug As-Open Debug Dialogを選択すると、上記ダイアログが起動するので、Argument タブの VM argumentsに、-Djava.library.path オプションを指定する。値は、DLLのパス。
今回の例では、
-Djava.library.path="C:\Documents and Settings\Administrator\My Documents\Visual Studio 2005\Projects\MemStat\Debug"
となる。
ここまでで、基本的な設定は完了なので、後は、実装を行う。
DLLの実装
JNIのリファレンスは、ここらあたりに、あるので結局はそこを読めばよいのだが、いかんせんなじみがないので、素直には読み進められなかった。
上記シナリオに基づいて、作成したコードに補足する。
#include "stdafx.h" #include "info_typea_jnisample_mem_JNIMemoryManager.h" jobject global_memstat; // (2) boolean _stopFlag; /* * Class: info_typea_jnisample_mem_JNIMemoryManager * Method: getMemoryStatus * Signature: ()Linfo/typea/jnisample/mem/MemoryStatus; */ JNIEXPORT jobject JNICALL Java_info_typea_jnisample_mem_JNIMemoryManager_getMemoryStatus (JNIEnv *env, jobject thisObj){ // (1) return global_memstat; } /* * Class: info_typea_jnisample_mem_JNIMemoryManager * Method: setMemoryStatus * Signature: (Linfo/typea/jnisample/mem/MemoryStatus;)V */ JNIEXPORT void JNICALL Java_info_typea_jnisample_mem_JNIMemoryManager_setMemoryStatus (JNIEnv *env, jobject thisObj, jobject memstat) { global_memstat = env->NewGlobalRef(memstat); // (2-1) } /* * Class: info_typea_jnisample_mem_JNIMemoryManager * Method: start * Signature: ()V */ JNIEXPORT void JNICALL Java_info_typea_jnisample_mem_JNIMemoryManager_run (JNIEnv *env, jobject thisObj){ // (3) jclass mng = env->GetObjectClass(thisObj); jmethodID jmIntvl = env->GetMethodID(mng, "getInterval", "()J"); jlong interval = env->CallLongMethod(thisObj, jmIntvl); _stopFlag = false; jclass jc = env->GetObjectClass(global_memstat); jmethodID jmChng = env->GetMethodID(jc, "change", "()V"); jmethodID jmMemLd = env->GetMethodID(jc, "setMemoryLoad", "(I)V"); jmethodID jmTtlPs = env->GetMethodID(jc, "setTotalPhys", "(I)V"); jmethodID jmAvlPs = env->GetMethodID(jc, "setAvailPhys", "(I)V"); jmethodID jmTtlPf = env->GetMethodID(jc, "setTotalPageFile", "(I)V"); jmethodID jmAvlPf = env->GetMethodID(jc, "setAvailPageFile", "(I)V"); jmethodID jmTtlVr = env->GetMethodID(jc, "setTotalVirtual", "(I)V"); jmethodID jmAvlVr = env->GetMethodID(jc, "setAvailVirtual", "(I)V"); MEMORYSTATUS memstat; while (!_stopFlag) { GlobalMemoryStatus( &memstat ); env->CallVoidMethod (global_memstat, jmMemLd, memstat.dwMemoryLoad); env->CallVoidMethod (global_memstat, jmTtlPs, memstat.dwTotalPhys / 1024); env->CallVoidMethod (global_memstat, jmAvlPs, memstat.dwAvailPhys / 1024); env->CallVoidMethod (global_memstat, jmTtlPf, memstat.dwTotalPageFile / 1024); env->CallVoidMethod (global_memstat, jmAvlPf, memstat.dwAvailPageFile / 1024); env->CallVoidMethod (global_memstat, jmTtlVr, memstat.dwTotalVirtual / 1024); env->CallVoidMethod (global_memstat, jmAvlVr, memstat.dwAvailVirtual / 1024); env->CallVoidMethod(global_memstat, jmChng); SleepEx(interval, FALSE); } } /* * Class: info_typea_jnisample_mem_JNIMemoryManager * Method: stop * Signature: ()V */ JNIEXPORT void JNICALL Java_info_typea_jnisample_mem_JNIMemoryManager_stop (JNIEnv *env, jobject thisObj) { _stopFlag = true; env->DeleteGlobalRef(global_memstat); // (2-1) global_memstat = NULL; }
(1) 関数宣言
1つ目の引数に、JNIEnvとして、JNI用の関数テーブルが渡される。これを利用して各種操作を行う。
2つ目の引数には、呼び出し元オブジェクトが渡される。
(2) グローバル参照
DLLの関数をまたがって、Javaのインスタンスを保持したい場合、(2)のように宣言して、初回の関数呼び出し時の引数jobjectを代入しておくだけでは、次回の関数呼び出し時に、その値は利用できない。そのような場合、グローバル参照を利用する必要があり、作成時には、
NewGlobalRef
破棄時には、
DeleteGlobalRef
を使用する。
(3) Javaメソッドの呼び出し
1: JNIEXPORT void JNICALL Java_info_typea_jnisample_mem_JNIMemoryManager_run
(JNIEnv *env, jobject thisObj){
2: jclass mng = env->GetObjectClass(thisObj);
3: jmethodID jmIntvl = env->GetMethodID(mng, "getInterval", "()J");
4: jlong interval = env->CallLongMethod(thisObj, jmIntvl);
今回の例で、nativeメソッド呼び出し元である、JNIMemoryManagerクラスのgetIntervalメソッドを、JNI関数の中から呼び出す場合、
1行目:JNIMemoryManagerのnativeメソッドrunが呼び出される。2つ目の引数に、JNIMemoryManager自身のインスタンスが設定されている。
2行目:JNIMemoryManager自身のインスタンスから、GetObjectClass関数を利用して、jclassを取得する。
3行目:取得した、jclassに、メソッド名、シグネチャを指定(ここで、上記で作成した、Antタスクを利用すると便利 ちなみに、()Jは、引数なし、戻り値long)してGetMethodIDを呼び出すと、jmethodIDを取得できる。
4:戻り値にあわせたCallxxxMethod関数を利用して、メソッドを呼び出す。
と。とりあえずはこんなところ。
まぁあとはリファレンスを見ながら何とかなるかな。
ソースコードはここ。