S-JIS[2006-06-18/2024-09-23]変更履歴
JavaのJNIのメモ。
|
|
JNIは、JavaからC言語/C++の関数を呼ぶ・あるいは逆にC/C++からJavaのメソッドを呼ぶための仕組み。
C言語またはC++で作ったライブラリを、Windowsの場合はDLLファイル、UNIXの場合は共有オブジェクト(so)ファイルにしておき、Javaから呼び出すことが出来る。
また、C言語/C++で作ったソースの中からJavaオブジェクトのメソッドを呼び出すことが出来る。
ただし当然 呼び出し方はJavaとC/C++(ネイティブ)とで整合性をとる必要があり、それがJNIである。
Java22以降ではJNIより外部関数およびメモリーAPIを使う方が良いと思われる。[2024-09-23]
JNIでライブラリを作るには、以下のような手順で作業する。
ある程度の生成作業は、makefileにしたりbuild.xmlにしたりして自動化できる。
まず、Javaでライブラリの関数を宣言する。
nativeというキーワードを付け、関数本体を書かない。(インターフェースでメソッドを宣言するようなもの)
JniJikken.java:
public class JniJikken {privatenativebyte[]copy(String src);}このファイルをコンパイルし、JniJikken.classを生成しておく。
javahというコマンド(javacやjavaと同じディレクトリに入っている)を使って、C言語/C++のヘッダーファイルを生成する。
この生成には、nativeを宣言したJavaのクラスファイルを入力として使う。
>javah -classpathクラスのあるディレクトリ -d生成先ディレクトリクラス名>javah -classpathclasses -d%VCPP%\JniJikkenJniJikken
この例では、classesディレクトリ直下にあるJniJikken.classを元に、VC++のJniJikkenというディレクトリの下にJniJikken.hを生成する。
javahは優秀なので(笑)、何度実行しても 宣言が新しくなっていない限りヘッダーファイルを更新しない。
(-forceオプションを付ければ常に上書きする)
→Sunのjavah - Cヘッダーとスタブファイルジェネレータ
Eclipseを使っている場合は(そうでない場合でも)、Ant用のbuild.xmlやバッチファイルを作っておけば便利だろう。
なお、クラスがパッケージ内に存在する場合は、以下の様にパッケージを指定する。
>javah -classpathclasses -d%VCPP%\JniJikkenjp.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 __cplusplusextern "C" {#endif/* * Class:JniJikken * Method:copy * Signature: (Ljava/lang/String;)[B */JNIEXPORT jbyteArray JNICALLJava_JniJikken_copy (JNIEnv *, jobject, jstring);#ifdef __cplusplus}#endif#endifJniJikken.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, jobjectthisj, jstringsrcj){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-olibJniJikken.so
all: libJniJikken.solib%.so: %.c %.hcc -G$< -I /usr/java/include -I /usr/java/include/solaris -o$@%.h: %.classjavah$*%.class: %.javajavac$<
↑この例の場合、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: copyat 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: noJni_Jikken injava.library.pathat 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」が見つからない。