EclipseとVisualStudioをつかってJNIプログラムを作る手順

ちょっと、JNIを使って、Java用のユーティリティを作ろうと思ったら、意外とチェックポイントが多かったので、サンプルプログラムを作り、手順をメモっておく。

題材

題材は何でもよかったが、手ごろな感じがするので、OSのメモリ使用量を調べるJNI用DLLを作成する。

概要

uml01

MemoryCheckerクラスは、Javaのウィンドウで、TextAreaにメモリ情報を出力するメソッドを追加した、MemoryListenerTextAreaを配置。

メモリ使用量を調査する抽象クラス、AbsMemoryManagerから、JVMのメモリ調査用クラスJavaMemoryManagerと、OSのメモリ調査用クラスJNIMemoryManagerを派生させる。

メモリ使用量データ保持用にMemoryStatusクラスを用意

uml02

動きとしては、MemoryChecker(ウィンドウ)から、JNIMemoryManagerを生成する。JNIMemoryManagerは、DLLをロードし、MemoryStatusのインスタンスを渡す。DLLでは、メモリ量を指定インターバル毎に調査して、保持するMemoryStatusのインスタンスにセットし、changeメソッドを呼ぶ。これが順に伝わり、TextAreaに状況を表示する。

win01

出来上がりはこんな感じ。

作成手順(Eclipse側)

DLL呼び出し元クラス

  1. DLLをロードする記述を追加
  2. 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++ ヘッダーファイルを作成

  1. コンパイル済みJavaクラスから、javah コマンドを利用して、C/C++用のヘッダファイルを生成
  2. C/C++での処理用に利用するJavaクラスのシグネチャをjavapコマンドを利用して取得

上記の1.は最低限必要な作業。2.はDLL側からJavaのクラスのメソッドを呼び出すときに、メソッド名と、戻り値、引数のシグネチャが必要だが、この記述がJavaでの通常の記述とはことなるので、簡単に取得するために行う。

また、上記作業をつどコマンドラインから実行するのも面倒なので、以下の手順でAntのタスクとして登録しておく。

eclipse01

プロジェクトから、新規作成-ファイルで、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

eclipse02

とりあえず、ここまででEclipse側は終了。

作成手順(VisualStudio側)

プロジェクトの作成

VisualStudio側では、VC++を利用して、MFC拡張DLLを作成する。

mfc01

プロジェクト名を、上記Java側でロードするDLLの名称にする。

mfc02

MFC拡張DLLを指定

JNI用ヘッダーファイルの取り込み

mfc03

先ほど作成した「info_typea_jnisample_mem_JNIMemoryManager.h」を、作成されたプロジェクトの適当なところにコピーして、ソリューションエクスプローラのヘッダーファイルのコンテキストメニューから、既存の項目を選んで取り込む。

mfc04

実装用に、「info_typea_jnisample_mem_JNIMemoryManager.cpp」ファイルを、同じくソリューションエクスプローラのソースファイルから新しい項目として追加する。

環境設定

JNI用のインクルードファイルの場所をVisualStudioに設定する。

メニュー-ツール-オプションでオプションダイアログを開き、「プロジェクトおよびソリューション-VC++ディレクトリ」を選択し、「ディレクトリを表示するプロジェクト」に、「インクルードファイル」を選択

mfc05

ここに、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のデバッグ環境から認識させるために、以下の設定を行う。

mfc06

実行するクラスのコンテキストメニュー-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関数を利用して、メソッドを呼び出す。

 

と。とりあえずはこんなところ。

まぁあとはリファレンスを見ながら何とかなるかな。

ソースコードはここ

ちょっと古い本だけど、JNIについて、丁寧に説明されています。

Follow me!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次の記事

12年ぶりの競馬場