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関数を利用して、メソッドを呼び出す。
と。とりあえずはこんなところ。
まぁあとはリファレンスを見ながら何とかなるかな。
ソースコードはここ。

