目次

Java native Interface

だいぶ前に使ったが、すっかりわすれていて久し振りに使いたくなったので ついでに忘れてもいいようにここにもメモしとこうと思いました φ(。。

JDK6のドキュメントに含まれているJNIのドキュメントの日付は2003年となっているので、 新しいことは無いと思われるが、一応一通りRHEL-as4u4/jdk6u1でやってみる。

Java2native

まずは Javaクラスメソッドの実体をnativeで実装する場合からスタート。 このパターンは 順当にクラスメソッドの実体をnativeで実装することになる。 従って作業は以下の通り。
  • 1. Javaコード部分はinterfaceクラスみたいな宣言する
  • 2. 実装があるライブラリを動的にロードする
  • 3. メソッドの実体をnativeで作る
  • 4. 作った実装をshared libraryにする

さんぷる

基本の printf("Hello JNI World\n")だろうwと言うことで、こんな感じ。
  • doIt()メソッドで stdoutに Hello %s World\n を出力する。%s部分は引数のStringにする
  • doIt()の引数はString/戻り値はintで、successなら1、失敗は -1。
  • 引数文字列の文字列長が0、またはNULL Pointerの場合、失敗でなにもしない

1. クラスをつくる

まぁ、まんま。doItメソッドのmethod sigtatureに JNIのために定義されている nativeをつける必要がある。ついでに動かすためのmainもつけとく。
package info.imap4rev1
public class Main {
	native int doIt(String s);
	public static void main(String [] args) {
		Main m = new Main();
		m.doIt(args.length > 0 ? args[0] : null);
	}
}

2. libraryをloadする

methodの本体となるnativeの実装となるlibraryは、staticブロックに含めて、JVMがclassをloadする際に 併せてlibraryをdynamic loadするように書いておく。今回のshared libraryの名称は libtest.soにしよう。
package info.imap4rev1
public class Main {
	public native int doIt(String s);
	public static void main(String [] args) {
		Main m = new Main();
		m.doIt(args.length > 0 ? args[0] : null);
	}

	static {
		System.loadLibaray("test");
	}

}

3. メソッドの実装をnativeでつくる

まず実装する場合の関数名だが、以下のnaming ruleに従う。
  • Java_[FQCN]_[method名](__引数名)
  • (*1) 末尾のかっこは、このmethodがoverloadされている場合、他との区別のために必要

こういうのは実際のコードを見るのが一番分り易いと思う。何年か昔だったら、J2MEの Linux向けriが配付されていたので、それを見れと言うところだが、今の時分では opekjdkなる 時代になったので、jdk6/jdk7(=OpenJDK)のソースを直接見て、Sunが提供しているJDKの 直接のソースをこの辺から見ることが可能である。 いい時代になったねぇ。

ま、よた話はおいといて、今回の場合のmethodに対応する関数名は以下の通りになる。

  • Java_info_imap4rev1_Main_doIt

で、引数String、戻り値intに対応するJNIの型が jstring/jintとなるので、 実装する関数は、文字列の取りだし方は色々あるが、例えばこうなる。

JNIEXPORT jint JNICALL Java_info_imap4rev1_Main_doIt(
	JNIEnv *env,  // interface pointer
	jobject this, // "this" pointer
	jstring s     // arg #1
	) {
	jint i = 0;
	jboolean isCopy;
	char *p;

	if ( s != NULL && (*env)->GetStringLength(env, s) > 0 ) {
		p = (*env)->GetStringUTFChars(env, s, &isCopy);
		printf("Hello %s World\n", p);
	} else { 
		i = -1;
	}

	return i;
}
ここではStringと言うjava.langパッケージで提供されていると言うか、JNIにも型として 存在するクラスを引数にしているので、方法が色々あるし、取りだしも簡単になっている。 無論、メソッドやフィールドにアクセスする方法が提供されていて、reflectionみたいな 感じでアクセスするi/fが提供されている。(後述するかも)

マクロの目的

関数名の前後にJNIEXPORTJNICALLというマクロが (javahコマンドで生成されるヘッダファイルのプロトタイプに)貼ってある。

jdk6のソースツリーから考えると JNIEXPORT/JNICALL共にLinux環境では 書いていなくても問題ない。(Windowsの場合、このマクロに意味がある)

4. 作った実装をshared libraryにする

実装は3.のでいいが、肝心の書きはじめがガイドには無いw

仕方がないので作り方の手順もここに含めておく。

  • 1. Javaクラスファイルをjavacでつくっておく
  • 2. 1.で作ったクラスファイルをjavahに食わせるとheaderファイルが出来る
    • javah -classpath . info.imap4rev1.Main
    • このコマンドの結果、info_imap4rev1_Main.hが下記の通り、作成されるだろう。
    • classpathの通り方は普通にjvmを使っている時と一緒なので .を通す必要もないけど、書いといた。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class info_imap4rev1_Main */

#ifndef _Included_info_imap4rev1_Main
#define _Included_info_imap4rev1_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     info_imap4rev1_Main
 * Method:    doIt
 * Signature: (Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_info_imap4rev1_Main_doIt
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif
  • 3. 2.で出来たheaderを元に実体を書く。
  • 4. $JAVA_HOME/includeと $JAVA_HOME/include/[arch]を-Iのパスに通してコンパイルする。
    • gcc -c -o hoge.o -fPIC -I/usr/java/include -I/usr/java/include/linux hoge.c (*1)
  • 5. shard libraryにする。(無論、4とセットでもいい)
    • gcc -shared -o libtest.so hoge.o (*1)

  • (*1) x86_64のlinuxで、javaをLinux64 bit対応のバイナリも併せて展開していない(=32bit VM)場合は、dynamic loadするライブラリも32bitにしないとUnsatisfiedLinkError?になる(ファイルはあるのに cannot open shared object file: No such file or directoryという怪しい状態でこのランタイムエラーが出る)。この場合、gccでobject/libraryを作成する際に-m32とarchを指定しておく。

実行結果

csh -c "setenv LD_LIBRARY_PATH .; java -classpath . info.imap4rev1.Main hoge"
Hello hoge World
まぁ順当。文字列の抜きだしが GetStringChars?()だとばらばらに抜けちゃうので、 GetStringUTFChars()を使うように。

あと、segvった場合、hs_err_xxxxファイルに色々ダンプされる。stackを見れば nativeコードの何番地でsegvったか判るので libraryを objdump -S してやれば デバッグに役立つ。なので安心してcrashするのをがしがし書いてみようw

native2Java

続いて、nativeからJavaクラスを利用する場合。

今度はJava2nativeで作ったMainクラスをロードして Main.doIt()を 呼び出してみる。作業は1個のnative実装を作るだけで、中でやることは下記の通り。

  • 1. JVMの起動引数を用意して、CreateJavaVMする
  • 2. Main classをloadして、newする
  • 3. コマンドライン引数をdoItの引数にするためのコンテナStringをnewする
  • 4. doItをinvokeする

さんぷる

動作仕様はこんな感じでいいだろう。
  • Mainのクラスをinstance化する
  • Main.doIt()の引数にコマンドライン引数があれば、コマンドライン引数を渡す。コマンドライン引数が無い場合はNULLを渡す

どうみてもテンプラコードでokなので、読んでるチュートリアルのコードをぱくるとこんな感じ。
爽やかにjdk6の実装を参考にしつつ、書いてみました。java2nativeの時に比べて 説明の章割りが少ないのは、作るものが1個だからです。 例によってコードのコメントとして解説をうめました。

途中でめんどうになったので、見れば判る通り例外処理を大量に端折ってます。 ちゃんと書くなら NULL checkとかbounds checkはちゃんと書きますよ?w

#include    <jni.h>
#include    <stdio.h>
#include    <string.h>
#include    <stdlib.h>
#include    <unistd.h>

/*
typedef struct JavaVMInitArgs {
    jint version;

    jint nOption;
    JavaVMOption *options;
    jboolean ignoreUnrecognized;
}JavaVMInitargs;

typedef struct JavaVMOption {
    char *optionString;
    void *extraInfo;
} JavaVMOption;
*/

jstring getString(JNIEnv *env, char *p) {
    jstring str = NULL;
    int plen;
    jclass sclz;
    jmethodID mid;
    jbyteArray array;

    plen = strlen(p);
    if ( (sclz = (*env)->FindClass(env, "java/lang/String")) 
        == NULL ) {
        return NULL;
    }
    // ここでは String(Byte [])のコンストラクタを使うことにしたので
    // p/f defaultのencoding込みで入力文字列はUnicodeとして保持される
    if ( (mid = (*env)->GetMethodID(env, sclz, "<init>", "([B)V"))
        == NULL ) {
        return NULL;
    }

    // ここで作った arrayはmallocしているので、この関数から戻る前に
    // free(=DeleteLocalRef)しないとmemory leakになる
    if ( (array = (*env)->NewByteArray(env, plen)) == NULL ) {
        return NULL;
    }
    (*env)->SetByteArrayRegion(env, array, 0, plen, (jbyte *)p);
    if ( (*env)->ExceptionOccurred(env) ) {
        return NULL;
    }

    // ここのJNI関数NewObject()が new String(byte [])の呼び出しに対応する
    str = (*env)->NewObject(env, sclz, mid, array);
    // 使ったのを片付け
    (*env)->DeleteLocalRef(env, array);

    return str;
}

int doIt(char *p) {
    JavaVM *jvm;
    JNIEnv *env;
    JavaVMInitArgs vm_args;
    JavaVMInitArgs *vap = &vm_args;
    int ret;
    jclass clz;
    jmethodID mid;
    jstring js;
    jobject mobj;

    // JVMの初期化。ここはテンプラだ
    vap->version = JNI_VERSION_1_6;
    if ( (ret = JNI_GetDefaultJavaVMInitArgs(vap)) < 0 ) {
        fprintf(stderr, "JNI_GetDefaultJavaVMInitArgs() error\n");
        return ret;
    }
    if ( (ret = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args)) < 0 ) {
        fprintf(stderr, "JNI_CreateJavaVM() error\n");
        return ret;
    }

    // クラスのload。きっとClassLoader.loadClass()と一緒だろう。
    clz = (*env)->FindClass(env, "info/imap4rev1/Main");

    // methodのsignatureはclassファイルがあるなら javap -sで出力できる
    // よくわかんないときは javap -sを見よう。(voidのsigが何かって書いてない
    // JDK docsに附属している仕様って。。。)
    mid = (*env)->GetMethodID(env, clz, "<init>", "()V");
    mobj = (*env)->NewObject(env, clz, mid);

    mid = (*env)->GetMethodID(env, clz, "doIt", "(Ljava/lang/String;)I");
    if ( p == NULL ) { 
        js = getString(env, "");
    } else {
        js = getString(env, p);
    }
    ret = (*env)->CallIntMethod(env, mobj, mid, js);

    // closing
    (*jvm)->DestroyJavaVM(jvm);

    return ret;
}

int main(int argc, char *argv[]) {
    return doIt(argc > 1 ? argv[1] : NULL);
}

コンパイルと実行上の注意

jvmのi/fを含む soは jreに含まれてます。linux向けのJavaSE6 SDKの場合、 以下3点が同梱されてました。
 /usr/jdk1.6.0_01/jre/lib/i386/client/libjvm.so 32bit client VM
 /usr/jdk1.6.0_01/jre/lib/i386/server/libjvm.so 32bit server VM
 /usr/jdk1.6.0_01/jre/lib/amd64/server/libjvm.so 64bit server VM
server VM/client VMの違いはこの辺にゆずるとして、 64bit linux向けJVM/HotSpot?には、server VMしかないという知らなかった事実を知りましたw Linux/x86_64でJavaアプリケーションを使うときは、使い方次第では 64bitはインスコしない方がいいかもしれません。

さてさて、上記の通りなので、コンパイルと実行の際のldがライブラリを 探すパスに上記の libjvm.soが含まれるディレクトリが入るようにする必要があります。 またコンパイルの際には、ビルドを通す為に -ljvmが必要です。

爽やかな実行結果

 csh -c "setenv LD_LIBRARY_PATH ${LDPATH} ./doit hoge"
 Hello hoge World

観想と結論w

JNI絡みの日本語資料はけっこうgoogleと出て来ますが、引数のjava objectを 用意して其を元にinvokeするのはあんま無い気がするから、ちょっとはここにも 情報価値があるっぽぃ?w

やはりJavaSE6のjvmソースを読みながら書いてみるのがいいらしいです。 上記の実装はそこかしこパクっていて、 例えば、getString()は NewPlatformString?()抜粋wです。

参考リンク

  • JNIドキュメント
  • OpenJDK
    • http://openjdk.java.net/
    • glassfish(=J2EE5)の時も、dev.java.netは偉大wだったけど、JavaDeveloper?には一段と有用なサイトになったようです。IBMのdWの方が有用な情報がjava.sun.comドメインより多いって思った時期もありましたが、見ることが出来るsrcを提供してるから、性質は違うけど有用度はこちらもとても高いって言うか、最期の依どころですな

Last-modified: 2007-06-12 17:06:52