Java Native Interface(以下JNI)は、JAVAと他の言語で開発されたネイティブなプログラムを連携させるために用意されたAPIで、 これを利用すると、JAVAからC,C++などで作成されたDLLを実行することができます。 例えばJAVAから直接EXCELコンポーネントを制御する場合などに使用します。 主にJAVAでは提供されていない機能を他の言語(C言語など)で実現しておき、それを呼び出してJAVAの機能を拡張するような形で用いられるみたいですが、 非常に文献がとぼしいため、私が調べた内容を簡単に記述します。 環境はWINDOWSで、JDK1.4とVC6を使用しました。
まずHelloWorldJNI.javaを作成します。
public class HelloWorldJNI {
static {
// ライブラリをロードします
System.loadLibrary("HelloWorldJNI");
}
// ネイティブメソッドを宣言します
public native String displayHelloWorld();
public static void main(String[] args) {
HelloWorldJNI hello = new HelloWorldJNI();
// メソッドを実行して表示します
System.out.println(hello.displayHelloWorld());
}
}
次に作成したHelloWorldJNI.javaをコンパイルし、HelloWorldJNI.classを作成する。
コマンドプロンプトより、次のコマンドを実行。
>javac HelloWorldJNI.java
コンパイルが成功したら、C++言語のヘッダーファイルの生成を行う。
>javah -jni HelloWorldJNI
これで次のヘッダファイルが作成されます。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorldJNI */
#ifndef _Included_HelloWorldJNI
#define _Included_HelloWorldJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloWorldJNI
* Method: displayHelloWorld
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_HelloWorldJNI_displayHelloWorld
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
作成されたヘッダファイルの内容を元に、C++言語のソースファイルの作成を行います。(サンプルはVCのコード)
#include "stdafx.h"
#include "HelloWorldJNI.h"
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) {
return TRUE;
}
JNIEXPORT void JNICALL Java_HelloWorldJNI_displayHelloWorld(JNIEnv *env, jobject obj) {
printf("Hello world!\n");
return;
}
上記は、まず
#include "HelloWorldJNI.h"
で作成されたヘッダーファイルを読込み、
JNIEXPORT void JNICALL Java_HelloWorldJNI_displayHelloWorld(JNIEnv *env, jobject obj)
と、ヘッダーファイルの関数名と全く同じ宣言で書いているところが重要なポイントです。
さて次に、HelloWorldJNI.cppをコンパイルし、HelloWorldJNI.dllを作成します。(LINUXの場合はHelloWorldJNI.so)
あとは作成されたDLLをパスの通ったディレクトリに配置し(パス指定が面倒な場合、WINDOWSだとSYSTEM32におくと良い)JAVAクラスを実行するだけです。
>java HelloWorldJNI
Hello World
Linuxの場合環境変数LD_LIBRARY_PATHを設定し、libHelloWorldJNI.soファイルがあるディレクトリにパスを通します。
カレントディレクトリにパスを通す場合は以下の通りです。
$ export LD_LIBRARY_PATH=.
ここからが、本題です。上のサンプルでreturnによる値の受け渡しは簡単に行えるのですが、これだけではあまり意味がありません。
やはり柔軟に受け渡しを行いたいと思うので、TRY&ERRORを繰り返して実現できた値の受け渡し方法を以下のサンプルプログラムで示します。
今のところ不具合はありませんが、Cとjavaの値の直接参照のところは、メモリの扱いが不十分かもしれず注意が必要です。
また、JNI関数郡についてはJNI.hを参考に推測で使用したため、使い方に誤りがある可能性もあります。
public class code { static{ // ネイティブライブラリをロードします System.loadLibrary("code"); } /*引数でC側へ値を渡す場合*/ private native long test(String str1,String str2); public String p_str; public long p_long; public double p_dbl; public static void main(String[] args) { HelloWorldJNI obj_code = new code(); // メソッドを実行 obj_code.test("text1","text2"); } }
#include <malloc.h> #include <iostream.h> //共通関数 BSTR Multi2Wide (const char *); BSTR jstring2BSTR (JNIEnv *, jstring ); BSTR Get_jstringFld2BSTR (JNIEnv *,jobject ,jclass, const char *); long Get_jlongFld2long (JNIEnv *,jobject ,jclass ,const char *); double Get_jdoubleFld2double (JNIEnv *,jobject ,jclass ,const char *); void Set_BSTR2jstringFld (JNIEnv *,jobject ,jclass ,const char *,BSTR ); void Set_LPSTR2jstringFld (JNIEnv *,jobject ,jclass ,const char *,LPSTR ); void Set_long2jlongFld (JNIEnv *,jobject ,jclass ,const char *,long ); void Set_double2jdoubleFld (JNIEnv *,jobject ,jclass ,const char *,double ); JNIEXPORT jlong JNICALL Java_code_test (JNIEnv *env, jobject mythis, jstring str1, jstring str2) { BSTR bstr; long lngbuf; double dblbuf; //引数の値を取得 bstr = jstring2BSTR(env,str1); //BSTRに変換 //ここで使用 SysFreeString(bstr);//BSTRの開放 //java側の実体を取得 jclass clazz = env->GetObjectClass(mythis); //java側のコードを直接get、set bstr = Get_jstringFld2BSTR(env,mythis,clazz,"p_str"); //p_strをここで使用 Set_BSTR2jstringFld(env,mythis,clazz,"p_str",bstr); SysFreeString(bstr);//BSTRの開放 Get_jlongFld2long(env,mythis,clazz,"p_lng") //p_lngをここで使用 Set_long2jlongFld(env,mythis,clazz,"p_lng",lngbuf); Get_jdoubleFld2double(env,mythis,clazz,"p_dbl") //p_dblをここで使用 Set_double2jdoubleFld(env,mythis,clazz,"p_dbl",dblbuf); //オブジェクトの解放 env->DeleteLocalRef(clazz); return 0; } /****************************************************************** * SJIS(Char)からUNIコード(BSTR)への変換。 * for All * SJISからUNIコードへ変換する。 * // 使用後は戻り値を SysFreeString()すること * @return UNIコード文字列。 ******************************************************************/ BSTR Multi2Wide(const char *str){ int intWLen = MultiByteToWideChar(CP_ACP, 0 , str, strlen(str), NULL, 0); BSTR pVal = SysAllocStringLen(NULL, intWLen); MultiByteToWideChar(CP_ACP, 0 /*MB_PRECOMPOSED*/, str, strlen(str), pVal, intWLen); return pVal; } /****************************************************************** * JstringをUNIコード(BSTR)へ変換して返す。 * for All * // 使用後は戻り値を SysFreeString()すること * @return UNIコード文字列。 ******************************************************************/ BSTR jstring2BSTR(JNIEnv *env, jstring jstr){ const jchar* jc = env->GetStringChars(jstr, NULL); int len = env->GetStringLength(jstr); BSTR bstr = SysAllocStringLen(jc, len); env->ReleaseStringChars(jstr, jc); return bstr; } /****************************************************************** * JAVA側から指定されたフィールド値を取得し、JstringをUNIコード(BSTR)へ変換して返す。 * for All * // 使用後は戻り値を SysFreeString()すること * @return UNIコード文字列。 ******************************************************************/ BSTR Get_jstringFld2BSTR (JNIEnv *env, jobject mythis, jclass clazz, const char *FieldName){ //オブジェクトのクラスを取得 //指定のフィールドIDの取得 jfieldID fid = env->GetFieldID(clazz, FieldName, "Ljava/lang/String;"); //javaフィールドからjstring取得 jstring jstr = (jstring)env->GetObjectField(mythis, fid); const jchar* jc = env->GetStringChars(jstr, NULL); //jstring -> jchar int len = env->GetStringLength(jstr); //jstrのサイズ BSTR bstr = SysAllocStringLen(jc, len); //jstring -> BSTR env->ReleaseStringChars(jstr, jc); //jc の解放 env->DeleteLocalRef(jstr); //jstr の解放 jstr = NULL; jc = NULL; return bstr; } /****************************************************************** * JAVA側から指定されたフィールド値を取得し、Jlongをlongへ変換して返す。 * for All * @return long ******************************************************************/ long Get_jlongFld2long (JNIEnv *env, jobject mythis, jclass clazz, const char *FieldName){ jfieldID fid = env->GetFieldID(clazz, FieldName, "J"); return (long)env->GetLongField(mythis, fid); } /****************************************************************** * JAVA側から指定されたフィールド値を取得し、Jdoubleをdoubleへ変換して返す。 * for All * @return double ******************************************************************/ double Get_jdoubleFld2double (JNIEnv *env, jobject mythis, jclass clazz, const char *FieldName){ jfieldID fid = env->GetFieldID(clazz, FieldName, "D"); return (long)env->GetDoubleField(mythis, fid); } /****************************************************************** * 受け渡された値(bstr)を指定されたJAVA側フィールド(FieldName)へ設定します。 * for All * ※受け渡されたBSTRの解放も行っています。 * @return なし ******************************************************************/ void Set_BSTR2jstringFld (JNIEnv *env, jobject mythis, jclass clazz, const char *FieldName, BSTR bstr){ //オブジェクトのクラスを取得 jstring jstrBuf = env->NewString(bstr,SysStringLen(bstr)); jfieldID fid = env->GetFieldID(clazz, FieldName, "Ljava/lang/String;"); env->SetObjectField(mythis, fid,jstrBuf); env->DeleteLocalRef(jstrBuf); SysFreeString(bstr); jstrBuf = NULL; bstr = NULL; } /****************************************************************** * 受け渡された値(bstr)を指定されたJAVA側フィールド(FieldName)へ設定します。 * for All * * @return なし ******************************************************************/ void Set_LPSTR2jstringFld (JNIEnv *env, jobject mythis, jclass clazz, const char *FieldName, LPSTR str){ jstring jstrBuf = env->NewStringUTF(str); jfieldID fid = env->GetFieldID(clazz, FieldName, "Ljava/lang/String;"); env->SetObjectField(mythis, fid,jstrBuf); env->DeleteLocalRef(jstrBuf); } /****************************************************************** * 受け渡された値(lngbuf)を指定されたJAVA側フィールド(FieldName)へ設定します。 * for All * @return なし ******************************************************************/ void Set_long2jlongFld (JNIEnv *env, jobject mythis, jclass clazz, const char *FieldName, long lngbuf){ jfieldID fid = env->GetFieldID(clazz, FieldName , "J"); env->SetLongField(mythis, fid,lngbuf); } /****************************************************************** * 受け渡された値(dblbuf)を指定されたJAVA側フィールド(FieldName)へ設定します。 * for All * @return なし ******************************************************************/ void Set_double2jdoubleFld (JNIEnv *env, jobject mythis, jclass clazz, const char *FieldName, double dblbuf){ jfieldID fid = env->GetFieldID(clazz, FieldName , "D"); env->SetDoubleField(mythis, fid,dblbuf); }
C側に次のコードを記述することによって、JNIのバージョンを指定できるようです。
これはJNIから自動的に呼び出されるので、書いておくだけで呼び出す必要はありません。
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { // return JNI_VERSION_1_1; return JNI_VERSION_1_2; }
JNIの仕様書 によると、Javaの基本型はネイティブ C の型に対して以下のようにマッピングされています。
また、JvalueはJNI関数の型の識別子として使われていたので調べた結果を併記しています。
Javaの型 | Cの型 | 説明 | Jvalue |
---|---|---|---|
boolean | jboolean | unsigned8bit | z |
byte | jbyte | signed8bit | b |
char | jchar | unsigned16bit | c |
short | jshort | signed16bit | s |
int | jint | signed32bit | i |
long | jlong | signed64bit | j |
float | jfloat | 32bit | f |
double | jdouble | 64bit | d |
void | void | - | - |
[オブジェクト] | jobject | - | l |
String | jstring | - | java/lang/String |
string jstrBuf = env->NewString(pVal,SysStringLen(pVal));
jstring (JNICALL *NewStringUTF) (JNIEnv *env, const char *utf);
-->string result = env->NewStringUTF("String Data");
const char* (JNICALL *GetStringUTFChars)(JNIEnv *env, jstring str, jboolean *isCopy);
//Getをしたら必ずリリースを行うこと。実体はJava側にあるため?
void (JNICALL *ReleaseStringUTFChars)(JNIEnv *env, jstring str, const char* chars);
JNIEXPORT jstring JNICALL Java_scs_1score_1jni_NCalc (JNIEnv *env, jobject mythis) { jclass clazz = env->GetObjectClass(mythis); jfieldID fid = env->GetFieldID(clazz, "ExaminationClass", "Ljava/lang/String;"); jstring val = (jstring)env->GetObjectField(mythis, fid); return val; staticとインスタンスかで、利用するメソッド名が微妙に違います。 jclass cl = env->GetObjectClass(instance); jmethodID method = env->GetMethodID(cl, "getMessage", "()Ljava/lang/String;"); jstring text1 = (jstring)env->CallObjectMethod(instance, method); printMessage(env, text1); jfieldID field = env->GetFieldID(cl, "message", "Ljava/lang/String;"); jstring text2 = (jstring)env->GetObjectField(instance, field); printMessage(env, text2); }
test.javaが、例えばjp.co.common.toolなどというパッケージの場合、次の通りコンパイルしていきます。
まずカレントディレクトリをjpフォルダのあるディレクトリ(ルート)とし、
>javac jp\co\common\tool\test.java
と実行してコンパイルします。次に、
>javah jp\co\common\tool\test
を実行してJNIのヘッダーファイルを作成します。
DLLは再入可能プログラムとして作成しておく必要があります。
JAVA側からJNIを使用して、System.loadLibrary()で呼び出したDLLはStaticなので、最初に使用する際にオブジェクトが生成され、呼び出された後はずっと実体化されたままです。
毎回実体化する手間を省いて高速に動作させるためにそうなっていると思われますが、これは特にWEBなどのマルチスレッドの際に、予期せぬ結果を招くことがあります。
例えばファイルをオープンして書き込みを行った後に終了するような場合、
非同期で呼び出されかつ、1度しか実体化のプロセスを通らないため、DLLMAINなどの初期化処理にオープン処理を記述しても、JAVA VMが生きている間DLLは同じものを使用しますので、最初に呼び出された時にしか実行されません。
私はC側でセマフォなどを用いて、利用しているオブジェクト数をカウントし実体の有無を、オブジェクトIDを作って利用オブジェクトの識別をしました。
複数のプログラムから使用する共有のCOMコンポーネントを呼び出していたのでかなりはまりましたが、実は簡単な対処法があるのかもしれません。