S-JIS[2006-06-18/2008-02-07] 変更履歴
JavaからC言語/C++の関数を呼ぶ・あるいは逆にC/C++からJavaのメソッドを呼ぶための仕組み。
C言語またはC++で作ったライブラリを、Windowsの場合はDLLファイル、UNIXの場合は共有オブジェクト(so)ファイルにしておき、Javaから呼び出すことが出来る。
また、C言語/C++で作ったソースの中からJavaオブジェクトのメソッドを呼び出すことが出来る。
ただし当然 呼び出し方はJavaとC/C++(ネイティブ)とで整合性をとる必要があり、それがJNIである。
|
|
JNIでライブラリを作るには、以下のような手順で作業する。
ある程度の生成作業は、makefileにしたりbuild.xmlにしたりして自動化できる。
まず、Javaでライブラリの関数を宣言する。
nativeというキーワードを付け、関数本体を書かない。(インターフェースでメソッドを宣言するようなもの)
JniJikken.java:
public class JniJikken {
private native byte[] copy(String src);
}
このファイルをコンパイルし、JniJikken.classを生成しておく。
javahというコマンド(javacやjavaと同じディレクトリに入っている)を使って、C言語/C++のヘッダーファイルを生成する。
この生成には、nativeを宣言したJavaのクラスファイルを入力として使う。
>javah -classpath クラスのあるディレクトリ -d 生成先ディレクトリ クラス名 >javah -classpath classes -d %VCPP%\JniJikken JniJikken
この例では、classesディレクトリ直下にあるJniJikken.classを元に、VC++のJniJikkenというディレクトリの下にJniJikken.hを生成する。
javahは優秀なので(笑)、何度実行しても 宣言が新しくなっていない限りヘッダーファイルを更新しない。
(-forceオプションを付ければ常に上書きする)
→Sunのjavah - Cヘッダーとスタブファイルジェネレータ
Eclipseを使っている場合は(そうでない場合でも)、Ant用のbuild.xmlやバッチファイルを作っておけば便利だろう。
なお、クラスがパッケージ内に存在する場合は、以下の様にパッケージを指定する。
>javah -classpath classes -d %VCPP%\JniJikken jp.hishidama.jni.JniJikken
生成されるヘッダーファイルの名前はパッケージを含んだ形になる。
すなわち「jp_hishidama_jni_JniJikken.h」となる。ちょっとくどいよね…
(生成される場所はあくまで指定したディレクトリ直下であり、パッケージ構成のディレクトリが作られるわけではない。)
生成したヘッダーファイルは、以下の様になっている。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniJikken */
#ifndef _Included_JniJikken
#define _Included_JniJikken
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JniJikken
* Method: copy
* Signature: (Ljava/lang/String;)[B
*/
JNIEXPORT jbyteArray JNICALL Java_JniJikken_copy
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
JniJikken.javaで「copy」という名前で宣言したメソッドが、「Java_JniJikken_copy」という関数になっている。
この関数をC言語あるいはC++で実装してやることになる。
Javaでのbyte[]やStringは、jbyteArray・jstringといった型(実体は構造体のポインター)で表現される。
これらはjni.h(及びその中でincludeしているjni_md.h)で定義されている。
JniJikken.c: JniJikken.cpp:
#include "JniJikken.h" JNIEXPORT jbyteArray JNICALL Java_JniJikken_copy (JNIEnv *env, jobject thisj, jstring srcj) { return NULL; /*ひとまずコンパイルを通す為、意味のあることは書かない*/ }
関数定義はヘッダーファイルから持ってきて、変数を追加すればいいだろう。
最初の変数は皆envにしているようだ。
2つ目の引数は、Javaから呼び出した際のクラスのインスタンス(を表すもの)が入ってくる。
したがって名前をthisとするのが雰囲気的にはいいのだが、C++ではthisがキーワードとして使われているので、別のものにしなければならない。
単純にobjとしている人も多いようだが、自分はthisjにしておく。(VBでthisに相当するものがMeだからmeでいいかと思ったけど、統一の為)
なお、staticメソッドの場合は、jobjectでなくjclassになる。[2007-09-15]
3つ目以降の引数はJava側で定義したものなので、基本的にそちらの変数と同じ名前でいいだろう。
が、実際に使う場合にはC言語の変数を作ってやることが多いので、Javaの引数であるということを明示する為に、自分は末尾に「j」を付けている。(srcの場合はsrcj)(変数の先頭に付けないのは、そうするとJNIのクラス(jstringとか)と紛らわしいから)
実際の関数の中の書き方は後述。
出来上がったC言語/C++のソースをコンパイルして、DLL(Windowsの場合)やSO(UNIXの場合)を生成する。
(UNIXの共有オブジェクト(SO)のファイル名は、「lib」で始まらないといけないらしい)
その際、jni.h およびその中でincludeしているjni_md.hがある場所をコンパイラーに指示してやる必要がある。
たいていのコンパイラーは「-I」オプションでインクルードパスを指定できる。
ファイル | 概要 | 場所 | 例 | |
---|---|---|---|---|
jni.h | JNIの各構造体の定義 | javaをインストールしたディレクトリ/include | C:\j2sdk〜\include | Windows |
$JAVA_HOME/include | UNIX | |||
jni_md.h | 機種依存部分の定義 jni.hからincludeされている。 |
jni.hのあるディレクトリの下 | C:\j2sdk〜\include\win32 | Windows |
/usr/java/include/solaris | Solaris | |||
$JAVA_HOME/include/linux | Linux |
% cc -G -I /usr/java/include -I /usr/java/include/solaris JniJikken.c -o libJniJikken.so
all: libJniJikken.so lib%.so: %.c %.h cc -G $< -I /usr/java/include -I /usr/java/include/solaris -o $@ %.h: %.class javah $* %.class: %.java javac $<
↑この例の場合、javaのコンパイル・javahでの生成まで含んでいる。
インクルードパスの追加方法参照。
↓JDK1.6の場合
C:\Program Files\Java\jdk1.6.0\include,C:\Program Files\Java\jdk1.6.0\include\win32
DLL又はSOファイルを作ったら、Javaから呼び出してやる。
JniJikken.java:
public class JniJikken { private native byte[] copy(String src); public static void main(String[] args) { System.loadLibrary("JniJikken"); String src = "testテスト"; JniJikken me = new JniJikken(); byte[] dst = me.copy(src); System.out.println(dst); } }
DLL又はSOファイルを実行時に読み込むために、「System.loadLibrary」を
最初に呼んでやる必要がある。
この引数には、DLLやSOファイル名の中核部分を指定する。(JniJikken.dll
/libJniJikken.so
)
最初に一度だけ確実に実行する仕組みとして、staticブロック(静的初期化子)で呼ぶ人も多い。
public class JniJikken { static { System.loadLibrary("JniJikken"); } private native byte[] copy(String src); public static void main(String[] args) { 〜 } }
このloadLibraryを実行し忘れると、nativeメソッド(この例ではcopy)の実行時に以下のような例外が発生する。
java.lang.UnsatisfiedLinkError: copy
at JniJikken.copy(Native Method)
at JniJikken.main(JniJikken.java:11)
Exception in thread "main"
そして、loadLibraryで指定されたDLLやSOファイルをjavaが実行時に探す為には、環境変数を設定しておく必要がある。
Windowsの場合、環境変数PATHにDLLファイルのパスが入っていなければならない。
Eclipseの場合はアプリケーション毎にPATHを指定できる。
UNIXの場合は、環境変数LD_LIBRARY_PATHにSOファイルのパスが入っていなければならない。
環境変数を設定しない場合は、実行時引数を以下の様に設定してやる。
> java -Djava.library.path=DLLやSOのディレクトリ JniJikken
loadLibraryの実行時に 指定されたDLLやSOファイルが見つからないと、以下のような例外が発生する。
java.lang.UnsatisfiedLinkError: no Jni_Jikken in java.library.path at java.lang.ClassLoader.loadLibrary(Unknown Source) at java.lang.Runtime.loadLibrary0(Unknown Source) at java.lang.System.loadLibrary(Unknown Source) at JniJikken.main(JniJikken.java:8) Exception in thread "main"
System.loadLibrary("Jni_Jikken");
「java.library.path」が、環境変数PATHやLD_LIBRARY_PATHを意味している。
(実際、Windowsの場合、System.getProperty("java.library.path")で取って来られる内容は、環境変数PATHの内容だ。)
この例では、Windowsの場合DLLファイルの「Jni_Jikken.dll」が、UNIXの場合SOファイルの「libJni_Jikken.so」が見つからない。