JNIは、Javaで記述する部分とC/C++で記述する部分があります。Java側はJDKがあればよいのですが、C/C++側はC/C++コンパイラが別途必要となります。
Windows環境のJavaVMはネイティブメソッドをDLL(Dynamic Link Library)という形で用意されていないといけないので、DLLを構築できるC/C++コンパイラが必要となります。
Windows環境で使用できる代表的なコンパイラを以下にリストアップします。
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
c:\home\torutk\prog\hellojni>javac \ jp/gr/java_conf/torutk/exp/jni/hello/HelloJniWorld.java
c:\home\torutk\prog\hellojni>javah -jni \ jp.gr.java_conf.torutk.exp.jni.hello.HelloJniWorld
カレントディレクトリにヘッダファイルが生成されます。ヘッダファイル名は、
jp_gr_java_conf_torutk_exp_jni_hello_HelloJniWorld.h
元になったJavaクラス名(FQCN)のピリオドを'_'に置き変えたファイル名になっているようです。
c:\home\torutk\prog\hellojni>cl /I"C:\Program \ Files\Java\jdk1.7.0\include" /I"C:\Program \ Files\java\jdk1.7.0\include\win32" /c HelloJniWorldImpl.c
javahで生成されたヘッダファイルは、Java SE Development Kitに含まれるjni.h、jni_md.hをインクルードしているので、コンパイル時にはJava SE Development Kitの中にあるパスを指定します。
前の手順で作成したオブジェクトファイルを、共有ライブラリファイルにまとめます。
c:\home\torutk\prog\hellojni>link /dll HelloJniWorldImpl.obj
Javaのクラスを実行するときは、CLASSPATHに指定します。しかし共有ライブラリファイルは、環境変数PATHあるいはシステムプロパティjava.library.pathで指定します。
c:\home\torutk\prog\hellojni>set \ PATH=c:\home\torutk\prog\hellojni;%PATH% c:\home\torutk\prog\hellojni>java \ jp.gr.java_conf.torutk.exp.jni.hello.HelloJniWorld Hello JNI World c:\home\torutk\prog\hellojni>
c:\home\torutk\prog\hellojni>java \ -Djava.library.path=C:\home\torutk\prog\hellojni \ jp.gr.java_conf.torutk.exp.jni.hello.HelloJniWorld Hello JNI World c:\home\torutk\prog\hellojni>
Linux環境のJavaVMはネイティブメソッドを共有ライブラリファイル(Shared Library)という形で用意しなければならないので、共有ライブラリファイルを構築できるC/C++コンパイラが必要となります。
Linux環境では、GCCが標準で使用できます。
$ javac jp/gr/java_conf/torutk/exp/jni/hello/HelloJniWorld.java
$ javah -jni jp.gr.java_conf.torutk.exp.jni.hello.HelloJniWorld $ ls jp jp_gr_java_conf_torutk_exp_jni_hello_HelloJniWorld.h $
カレントディレクトリにヘッダファイルが生成されます。ヘッダファイル名は、元になったJavaクラス名(FQCN)のピリオドを'_'に置き変えたファイル名になっているようです。
$ gcc -fPIC -g -I/usr/java/jdk1.7.0/include \ -I/usr/java/jdk1.7.0/include/linux \ -c HelloJniWorldImpl.c
共有ライブラリファイルは、コードが複数のプロセスで共有されるため、特定のプロセスとのリンク時にアドレスを置き換えることができない。そこで、PIC(Position Independend Code)と呼ばれる、コードのアドレスに依存せずに実行できる形式で作成する。なお、PICを指定しなくても動いてしまいますが、多分その場合はライブラリがプロセス同士で共有されず、プロセス毎に別なメモリに置かれるため、共有ライブラリの意味が薄れてしまいます。
javahで生成されたヘッダファイルは、Java SE Development Kitに含まれるjni.h、jni_md.hをインクルードしているので、コンパイル時にはJava SE Development Kitの中にあるパスを指定します。
前の手順で作成したPICオブジェクトファイルを、共有ライブラリファイルにまとめます。
$ gcc -shared -o libHelloJniWorldImpl.so HelloJniWorldImpl.o
今回は、ユーザ個別の場所に共有ライブラリを置くため、簡易な作成を行っています。システムのライブラリ領域に置く場合は、バージョン番号等を付与して管理するため、もっと複雑な手順となります。
Javaのクラスを実行するときは、CLASSPATHに指定します。しかし共有ライブラリファイルは、環境変数LD_LIBRARY_PATHで指定します。例えば、/home/torutk/prog/hellojniに共有ライブラリファイルとJavaクラスファイルの起点を置いていた場合、
$ export \ LD_LIBRARY_PATH=/home/torutk/prog/hellojni:$LD_LIBRARY_PATH $ cd /home/torutk/prog/hellojini $ java jp.gr.java_conf.torutk.exp.jni.hello.HelloJniWorld Hello JNI World $
Solaris環境のJavaVMは、ネイティブメソッドを共有ライブラリファイル(Shared Library)の形で用意します。したがって、共有ライブラリファイルを構築できるC/C++コンパイラが必要となります。
Solaris環境の場合は、GCCまたはSunStudioコンパイラが無償で利用可能です。
Javaに関するコンパイル・実行は他のプラットフォームと同じですので、C/C++のソースファイルをコンパイルする手順だけを以下に示します。
$ CC -KPIC -mt -I/usr/jdk/jdk1.7.0/include \ -I/usr/jdk/jdk1.7.0/include/solaris -c HelloJniWorldImpl.c
コンパイラ・オプションについては、次のURLに解説があります。
簡単にまとめると以下の表になります。
オプション項目 | Intel x86 | Intel x64 | SPARC 32bit | SPARC 64bit | 備考 |
---|---|---|---|---|---|
-xarch | -xarch=pentium | -xarch=amd64 | -xarch=v8 | -xarch=v9 | JDKのネイティブ・コードとリンクするので同じオプションとする |
-KPIC | 共有ライブラリファイル生成時にリロケータブルなバイナリを生成 | ||||
-mt | マルチスレッドセーフ | ||||
-xregs | -xregs=no%frameptr | -xregs=no%appl |
$ CC -G HelloWorldImpl.o -o libHelloJniWorldImpl.so \ HelloJniWorldImpl.o
nativeメソッドで例外をスローする場合、C/C++側でどのように例外オブジェクトを生成してスローすればよいのかを見ていきます。
まず、以下のようなnativeメソッドをJava側で定義します。
package jp.gr.java_conf.torutk.exp.jni.hello; public class HelloCalc { static { System.loadLibrary("HelloCalcImpl"); } public native int calc(int a, int b) throws CalculateException; }
例外CalculateExceptionの定義を以下に示します。
package jp.gr.java_conf.torutk.exp.jni.hello; public class CalculateException extends Exception { public CalculateException() { } public CalculateException(String message) { super(message); } public CalculateException(String message, Throwable cause) { super(message, cause); } public CalculateException(Throwable cause) { super(cause); } }
Exceptionクラスで定義されるコンストラクタと同じ引数パターンのものを用意しています。それ以外には特に機能はないシンプルなアプリケーション定義例外クラスです。
上記クラスからjniコマンドで生成されるC/C++のヘッダーファイルは次のようになります。
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class jp_gr_java_conf_torutk_exp_jni_hello_HelloCalc */ #ifndef _Included_jp_gr_java_conf_torutk_exp_jni_hello_HelloCalc #define _Included_jp_gr_java_conf_torutk_exp_jni_hello_HelloCalc #ifdef __cplusplus extern "C" { #endif /* * Class: jp_gr_java_conf_torutk_exp_jni_hello_HelloCalc * Method: calc * Signature: (II)I */ JNIEXPORT jint JNICALL \ Java_jp_gr_java_1conf_torutk_exp_jni_hello_HelloCalc_calc (JNIEnv *, jobject, jint, jint); #ifdef __cplusplus } #endif #endif
JNIで定義されるC/C++側の関数宣言には、例外については何も登場しません。
ネイティブ・メソッドのC++での実装を記述します。
#include <jni.h> #include "jp_gr_java_conf_torutk_exp_jni_hello_HelloCalc.h" JNIEXPORT jint JNICALL \ Java_jp_gr_java_1conf_torutk_exp_jni_hello_HelloCalc_calc(JNIEnv* env, \ jobject obj, jint value1, jint value2) { if (value1 < 0 || value2 < 0) { jclass clazz = env->FindClass( "jp/gr/java_conf/torutk/exp/jni/hello/CalculateException"); env->ThrowNew(clazz, "Argument should not be negative"); return -1; // 例外をスローするので実際にはJava側にはリターンされない } jint result = value1 + value2; return result; }
jintは、JDK 6ではlong型のtypedefによる定義型となっています。なので、そのまま32bit整数値として扱われます。
ネイティブ・メソッドの中から例外をスローする場合は、JNIEnvのThrowまたはThrowNewメンバ関数を使用します。記述が簡単なのは、ThrowNewの方ですが、コンストラクタとして文字列を引数に取るものにしか対応できません。
ThrowまたはThrowNewメンバ関数を呼んでも制御がその場で飛ぶことはありません。C/C++側での処理は次の行に継続します。そこで、ここではThrowNew呼び出し直後にreturn文を入れて、関数そのものを終了されます。
ThrowNewメンバ関数の第2引数は、例外オブジェクトを生成する際にコンストラクタへ渡す引数となります。この第2引数の型はconst char*型となっています。ASCII文字の範囲であればそのまま使用できますが、非ASCII文字の場合はUTF-8形式にする必要があります。
以下は、3番目のネイティブ(C/C++側)で文字コードをMS-932からUTF-8に変換する場合のコードとなります。
if (value1 < 0 || value2 < 0) { jclass clazz = env->FindClass( "jp/gr/java_conf/torutk/exp/jni/hello/CalculateException"); const char* message = "引数は正の整数でなくてはなりません"; // MS932 -> Unicode変換後の長さを算出 int unicodeLength = MultiByteToWideChar( CP_ACP, 0, message, strlen(message), NULL, 0); WCHAR* unicodeBuffer = new WCHAR[unicodeLength]; // MS932 -> Unicode変換 MultiByteToWideChar( CP_ACP, 0, message, strlen(message), unicodeBuffer, unicodeLength); // Unicode -> UTF-8変換後の長さを算出 int utf8Length = WideCharToMultiByte( CP_UTF8, 0, unicodeBuffer, unicodeLength, NULL, 0, NULL, NULL); // Unicode -> UTF-8変換 char* utf8Buffer = new char[utf8Length + 1]; WideCharToMultiByte( CP_UTF8, 0, unicodeBuffer, unicodeLength, utf8Buffer, utf8Length, NULL, NULL); utf8Buffer[utf8Length] = 0; env->ThrowNew(clazz, utf8Buffer); delete unicodeBuffer; delete utf8Buffer; return -1; // 例外をスローするので実際にはJava側にはリターンされない }
まずMS-932からUnicodeへ変換後の文字列(WCHAR[])格納サイズを取得するために、第6引数に0を指定してMulitByteToWideCharを呼び出します。
次に、MS-932からUnicodeへ変換するために、MultitByteToWideCharを呼び出します。
それから、UnicodeからUTF-8へ変換後の文字列(char[])格納サイズを取得するために、第6引数に0を指定してWideCharToMultiByteを呼び出します。
そして、UnicodeからUTF-8へ変換するために、WideCharToMultiByteを呼び出します。
UTF-8へ変換した文字列(char *)をThrowNewの引数に渡して例外オブジェクトを生成しスローするよう設定し、動的にアロケートしたメモリをクリアしてからリターンします。
昨今のLinuxおよびSolarisは、デフォルトの日本語ロケールの文字コードがUTF-8なので、C/C++のソースファイルをUTF-8形式で保存しコンパイルするので、日本語文字列は問題ないようです。
EUC-JPを日本語ロケールに設定している場合でも、UTF-8環境がインストールされていれば、実行時にロケールをUTF-8に設定すれば大丈夫です。
$ export LANG=ja_JP.UTF-8 $
UTF-8環境がない場合、文字列をEUC-JPからUTF-8へ変換するのにLinux/Solaris上でのC/C++では、iconvライブラリかまたはIBMのICUライブラリを使用するのが一般的と思います。
JNIを用いると、C/C++アプリケーションからJavaVMを起動し、JavaのクラスをC/C++から利用することができます。
JNIのドキュメントに記載があります。Java Native Interfaceの仕様 5.呼び出し(Invocation) API
まずは、JavaVM自体をC/C++アプリケーション側から起動する方法を見てみます。
#include <jni.h> #include <iostream> int main(int argc, char* argv[]) { JavaVMOption options[3]; options[0].optionString = "-Xmx128m"; options[1].optionString = "-verbose:gc"; options[2].optionString = "-Djava.class.path=C:/home/torutk/java/jni"; JavaVMInitArgs vm_args; vm_args.version = JNI_VERSION_1_6; vm_args.options = options; vm_args.nOptions = 3; std::cout << "creating Java VM" << std::endl; JNIEnv* env; JavaVM* jvm; int res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); if (res < 0) { std::cerr << "JNI_CreateJavaVM Error: " << res << std::endl; return -1; } std::cout << "finding Java class `Hello`" << std::endl; jclass clazz = env->FindClass("Hello"); if (clazz == 0) { std::cerr << "FindClass Error for `Hello`" << std::endl; return -1; } std::cout << "getting static method ID of `main`" << std::endl; jmethodID mid = env->GetStaticMethodID(clazz, "main", \ "([Ljava/lang/String;)V"); if (mid == 0) { std::cerr << "GetStaticMethodID Error for `main` `([Ljava/lang/String;)V`" \ << std::endl; return -1; } std::cout << "invoking Hello#main" << std::endl; env->CallStaticVoidMethod(clazz, mid, NULL); std::cout << "destroying Java VM" << std::endl; jvm->DestroyJavaVM(); return 0; }
JavaVMを起動するオプションは、JNIの構造体JavaVMInitArgsおよび構造体JavaVMOptionで定義します。JavaVMOptionには、通常コマンドラインでJavaVMに与えるオプションと同じ文字列で指定します。
ここで起動したJavaVM上でクラスを実行させる(ロードさせる)には、JavaVMに対してクラスパスを指定しておく必要があります。通常javaコマンド(java.exe)で起動するときは、コマンドラインオプション-cpや-jarを使って指定しますが、JavaVM自身に渡すときは、システムプロパティjava.class.pathに設定します。
コンパイルは、JavaからC/C++を呼び出す場合と同じく、jni.hをインクルードするためのインクルードパスを指定します。
Visual Studio 2010のC++での実行例です。コンパイルは次のコマンドです。/cオプションを付けると、コンパイルのみ実行しリンクは実行しません。
C:\Users\torutk\Documents\work\launch> cl /I"C:\Program \ Files\Java\jdk1.7.0\include" /I"C:\Program \ Files\java\jdk1.7.0\include\win32" /c launch.cc C:\Users\torutk\Documents\work\launch>
リンクは次のコマンドです。このサンプルではJavaVMに対して「明示的リンク」をするので、リンク時にJavaVMのライブラリを指定します。リンク時に指定するライブラリは、インポートライブラリファイルで、<JDKインストールディレクトリ>\libにjvm.libのファイルとして置かれています。
C:\Users\torutk\Documents\work\launch> link launch.obj \ /libpath:"C:\Program Files\Java\jdk1.7.0\lib" jvm.lib C:\Users\torutk\Documents\work\launch>
これで、実行時にjvm.dllとリンクするlaunch.exeが生成されます。
Windows OSでは、実行時リンクするDLLファイルをカレントディレクトリおよび環境変数PATHに指定されたディレクトリから検索します。そこで、環境変数PATHに設定を追記して実行します。
jvm.dllは、Java SE Development Kit 7u6(32bit版)の場合、client VMとserver VMの2つがあり、それぞれ
にあります。使用したい方を環境変数PATHに設定し、プログラムを実行します。
C:\Users\torutk\Documents\work\launch> PATH=%PATH%;"C:\Program \ Files\Java\jdk1.7.0\jre\bin\server" C:\Users\torutk\Documents\work\launch> launch Hello world! C:\Users\torutk\Documents\work\launch>
先のサンプルでは、実行時に使用するJavaVM(jvm.dll)のパスを環境変数PATHに設定しておく必要があります。
Windows OSでOracle JDKをインストールすると、レジストリにそのマシンにインストールされているJavaのバージョンとそのパスが登録されます。JavaVMを起動するプログラム内でこのパスをレジストリから読み出し、そのパスから動的にjvm.dllをロードすることで、PATH設定は不要になります。
レジストリは次になります。
HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\CurrentVersion このマシンにインストールされているJava実行環境の現行(最新)バージョンが格納 HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\1.7\RuntimeLib Java実行環境のバージョン1.7のjvm.dllのパスが格納
説明のため、一切エラー処理を省いたサンプルコードを次に示します。実際に使用する際は、各APIの戻り値を取得し成否判定をするといった処理が不可欠です。
#include <jni.h> #include <windows.h> #include <string> namespace { const char* key_jre = "Software\\JavaSoft\\Java Runtime Environment"; } int main() { // レジストリキー"HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Environment"を // オープンし、そのキーの中から名前"CurrentVersion"の値を読み取る HKEY key; RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_jre, NULL, KEY_READ, &key); BYTE buffer[256]; DWORD size = sizeof(buffer); RegQueryValueEx(key, "CurrentVersion", NULL, NULL, buffer, &size); RegCloseKey(key); // レジストリキー"HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Environment\<N.N>" // (ここで<N.N>は、上記で読み込んだCurrentVersionの値)をオープンし、そのキーの中から // 名前"RuntimeLib"の値を読み取る std::string key_jre_ver(key_jre); key_jre_ver.append("\\").append(reinterpret_cast<char*>(buffer)); RegOpenKeyEx(HKEY_LOCAL_MACHINE, key_jre_ver.c_str(), NULL, KEY_READ, &key); size = sizeof(buffer); RegQueryValueEx(key, "RuntimeLib", NULL, NULL, buffer, &size); RegCloseKey(key); // 読み取った値をパスとして動的にjvm.dllライブラリーをロード HMODULE module = LoadLibrary(reinterpret_cast<char*>(buffer)); // 動的にロードしたjvm.dllライブラリから関数JNI_CreateJavaVMのアドレス取得 auto func_CreateJavaVM = reinterpret_cast<decltype(JNI_CreateJavaVM)*>( GetProcAddress(module, "JNI_CreateJavaVM") ); // JNI_CreateJavaVMを呼び出し JavaVMOption options[2]; options[0].optionString = "-Xmx64m"; options[1].optionString = "-Djava.class.path=."; JavaVMInitArgs vm_args; vm_args.version = JNI_VERSION_1_6; vm_args.options = options; vm_args.nOptions = 2; JNIEnv* env; JavaVM* jvm; (*func_CreateJavaVM)(&jvm, (void**)&env, &vm_args); // Helloクラスのロード jclass clazz = env->FindClass("Hello"); // Helloクラスのmainメソッド取得 jmethodID mid = env->GetStaticMethodID(clazz, "main", "([Ljava/lang/String;)V"); // mainメソッド実行 env->CallStaticVoidMethod(clazz, mid, NULL); // JVM破棄 jvm->DestroyJavaVM(); }
レジストリを取得するためにWin32 APIを使用しています。レジストリの読み方は、まず値を格納しているキーをRegOpenKeyEx関数で取得します。次に、キーに属する値をRegQueryValueEx関数で取得します。
動的にjvm.dllライブラリを黙示的リンク(実行中にロード)するためにWin32 APIのLoadLibrary関数を呼びます。
ロードしたライブラリが提供する関数のアドレスを取得するためにWin32 APIのGetProcAddress関数を呼びます。関数ポインタは型宣言が複雑なのですが、このコードではC++11(2011年のC++標準規格改訂)で新しく追加された言語仕様のautoおよびdecltypeを使って簡潔に記述しています。
この例では、レジストリのCurrentVersionを使用していますが、コマンドラインオプションでJava実行環境のバージョンを指定したり、インストールされているJava実行環境のバージョン一覧を表示し選択されたものを実行するといった応用も可能です。
コンパイルは前と同じです。
C:\Users\torutk\Documents\work\launch> cl /I"C:\Program \ Files\Java\jdk1.7.0\include" /I"C:\Program \ Files\java\jdk1.7.0\include\win32" /c launch.cc C:\Users\torutk\Documents\work\launch>
リンクは、Win32 APIのレジストリ操作APIを指定します。jvm.libの指定は黙示的リンクの場合は不要です。
C:\Users\torutk\Documents\work\launch> link launch.obj \ advapi32.lib C:\Users\torutk\Documents\work\launch>
実行時に環境変数PATHの設定追加は不要です。
C:\Users\torutk\Documents\work\launch> launch Hello world! C:\Users\torutk\Documents\work\launch>