NoteBook > JNI (Java Native Interface)

JNI (Java Native Interface)

目次

はじめに

Java Native Interface(以下JNI)は、JAVAと他の言語で開発されたネイティブなプログラムを連携させるために用意されたAPIで、 これを利用すると、JAVAからC,C++などで作成されたDLLを実行することができます。 例えばJAVAから直接EXCELコンポーネントを制御する場合などに使用します。 主にJAVAでは提供されていない機能を他の言語(C言語など)で実現しておき、それを呼び出してJAVAの機能を拡張するような形で用いられるみたいですが、 非常に文献がとぼしいため、私が調べた内容を簡単に記述します。 環境はWINDOWSで、JDK1.4とVC6を使用しました。

作成の流れ

  1. JAVAクラスの作成
  2. JAVAHでC++ヘッダファイルの作成
  3. C++でのプログラムの作成

簡単なプログラムの作成(Hello World)

まず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 

これで次のヘッダファイルが作成されます。

HelloWorldJNI.h

/* 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のコード)

HelloWorldJNI.cpp

#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を参考に推測で使用したため、使い方に誤りがある可能性もあります。

JAVA側のソース(code.java)

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");
  }
}

C側のソース

#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);
}

JNI_ONLOAD関数

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
booleanjbooleanunsigned8bitz
bytejbytesigned8bitb
charjcharunsigned16bitc
shortjshortsigned16bits
intjintsigned32biti
longjlongsigned64bitj
floatjfloat32bitf
doublejdouble64bitd
voidvoid--
[オブジェクト]jobject-l
Stringjstring-java/lang/String

特に文字列周りの記述

Wide文字列をjstring文字列に変換する。

string jstrBuf = env->NewString(pVal,SysStringLen(pVal));

char文字列をjstring文字列に変換する

jstring (JNICALL *NewStringUTF) (JNIEnv *env, const char *utf);
-->string result = env->NewStringUTF("String Data");

jstring文字列をchar文字列に変換する

const char* (JNICALL *GetStringUTFChars)(JNIEnv *env, jstring str, jboolean *isCopy);
//Getをしたら必ずリリースを行うこと。実体はJava側にあるため?
void (JNICALL *ReleaseStringUTFChars)(JNIEnv *env, jstring str, const char* chars);

Java側のStringフィールドをC++側で取得する例

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);
}

javaがパッケージの場合のJNIヘッダの作成

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コンポーネントを呼び出していたのでかなりはまりましたが、実は簡単な対処法があるのかもしれません。