S-JIS[2006-06-18/2008-02-07] 変更履歴

JNI(Java Native Interface)

JavaからC言語/C++の関数を呼ぶ・あるいは逆にC/C++からJavaのメソッドを呼ぶための仕組み。

C言語またはC++で作ったライブラリを、Windowsの場合はDLLファイル、UNIXの場合は共有オブジェクト(so)ファイルにしておき、Javaから呼び出すことが出来る。
また、C言語/C++で作ったソースの中からJavaオブジェクトのメソッドを呼び出すことが出来る。
ただし当然 呼び出し方はJavaとC/C++(ネイティブ)とで整合性をとる必要があり、それがJNIである。


全体の手順

JNIでライブラリを作るには、以下のような手順で作業する。

  1. 呼び出す側のプログラム(とりあえずは宣言だけでよい)をJavaで書く。
  2. ひとまずJavaのソースをコンパイルしてclassファイルを生成する。
  3. javahを使ってclassファイルから C言語/C++のヘッダーファイルを生成する。
    build.xmlを書いておくと便利
  4. 生成したヘッダーファイルの実装をC言語/C++で書く。 →実装方法
  5. C言語/C++のソースをコンパイルしてDLL(SO)ファイルを生成する。
    JNI用ヘッダーファイルを探しに行く場所の指定が必要。
    →VC++の場合、ビルドしたDLLをEclipseのワークスペースに自動コピーするよう設定すると便利
  6. JavaでDLL(SO)ファイルを呼び出して実行する。

ある程度の生成作業は、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」となる。ちょっとくどいよね…
(生成される場所はあくまで指定したディレクトリ直下であり、パッケージ構成のディレクトリが作られるわけではない。)


C言語/C++での実装

生成したヘッダーファイルは、以下の様になっている。

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++のコンパイル

出来上がった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

Solarisの例:

% cc -G -I /usr/java/include -I /usr/java/include/solaris JniJikken.c -o libJniJikken.so

Solarisでのmakefileの例:

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での生成まで含んでいる。

VC++の例:

インクルードパスの追加方法参照。
↓JDK1.6の場合

C:\Program Files\Java\jdk1.6.0\include,C:\Program Files\Java\jdk1.6.0\include\win32

Javaから実行

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.dlllibJniJikken.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」が、環境変数PATHLD_LIBRARY_PATHを意味している。
(実際、Windowsの場合、System.getProperty("java.library.path")で取って来られる内容は、環境変数PATHの内容だ。)
この例では、Windowsの場合DLLファイルの「Jni_Jikken.dll」が、UNIXの場合SOファイルの「libJni_Jikken.so」が見つからない。


参考


Javaへ戻る / 技術メモへ戻る / C言語/C++の実装方法へ行く / JNIエラーへ行く
メールの送信先:ひしだま