Java Native InterfaceJava Native Interface (JNI) は、Javaプラットフォームにおいて、Javaで記述されたプログラムと、他のプログラミング言語(たとえばCやC++など)で書かれた、実際のCPU上で動作するコード(ネイティブコード)とを連携するためのインタフェース仕様である。Java言語からネイティブコードを利用するためのABIと、逆にネイティブコードからJavaバイトコードを動作させるためにバーチャルマシン (VM) を利用するためのAPIの2つから成る。 JNIを使うことで、Java言語のVMで動作させるには処理速度の面で不利とされる計算量の多いプログラムを部分的にネイティブコードに置き換えて高速化したり、標準Javaクラスライブラリからはアクセスできないオペレーティングシステムあるいはハードウェアの機能を利用するプログラムを、あたかも通常のJavaクラスのメソッドのように呼び出したりできるようになる。逆に、Javaクラスライブラリによって実装されている高水準の機能を、C/C++などで書かれた下位層から利用することもできるようになる。JNIはJava言語以外のJava VM上で動作する言語 (AltJava) からも利用可能である。 JNIによる、Java VMからのネイティブコードの呼び出しは、VMの実行環境の一貫性を保つために、通常のJavaプログラムの実行時とは異なる例外的なメモリ管理や排他制御を必要とする場合があり、しばしばプログラムの実行速度の低下を招くことがある。そのため、単純にJNIを利用することで必ずしもアプリケーション性能を改善できるというわけではない。また、ネイティブコードを含むバイナリはプロセッサアーキテクチャやオペレーティングシステムに依存してしまうことになるため、Javaのみで記述されたプログラムと比べて再利用や再配布の面で難がある。 JDK 1.0ではJNIの前身となるNative Method Interface (NMI) が実装されていたが、ネイティブコードとJavaコードとが明確に分離されていなかった。JDK 1.1でJNIが実装され、NMIはJNIで置き換えられた。 Javaからのネイティブコード呼び出し以下ではC/C++言語によるネイティブコードの記述例を用いて説明する。なおJNI関連のシンボル定義や関数宣言を含むヘッダーファイルは、Java Development Kit (JDK) に付属している。 JNIフレームワークでは、ネイティブ関数はC/C++のソースファイル (拡張子は.cもしくは.cppなど) に分離して実装する。Java VMは、 JNIEXPORT void JNICALL
Java_PackageName_ClassName_MethodName
(JNIEnv *env, jobject obj)
{
/* ネイティブコードをここに記述する */
}
以下の例ではJava文字列をネイティブな文字列に変換し、C/C++の標準ライブラリを利用して標準出力に出力する。 /* C code */
JNIEXPORT void JNICALL
Java_com_example_TestClass_printString
(JNIEnv *env, jobject obj, jstring javaString)
{
/* Java 文字列より Modified UTF-8 形式のネイティブ文字列を取得する。 */
const char *nativeString = (*env)->GetStringUTFChars(env, javaString, NULL);
printf("%s\n", nativeString);
/* ネイティブ文字列を解放する。 */
(*env)->ReleaseStringUTFChars(env, javaString, nativeString);
}
// C++ code
extern "C" {
JNIEXPORT void JNICALL
Java_com_example_TestClass_printString
(JNIEnv *env, jobject obj, jstring javaString)
{
const char *nativeString = env->GetStringUTFChars(javaString, NULL);
std::cout << nativeString << std::endl;
env->ReleaseStringUTFChars(javaString, nativeString);
}
}
上記のようにしてCまたはC++で記述した関数が myjni ライブラリモジュール[注釈 1]に実装されていると仮定し、Javaプログラムから呼び出す例を示す。C/C++側の実装関数とJava側のメソッドバインディングは実行時に動的に解決される。 package com.example;
public class TestClass {
static {
System.loadLibrary("myjni");
}
public static native printString(String s);
public static void main(String[] args) {
printString("Hello, JNI.");
}
}
オブジェクト指向言語であるC++に対しては、Cよりも多少洗練されたJNI APIが提供される。C++ではJavaのように、オブジェクトのメソッド(メンバー関数)という概念と、メソッド呼び出しのセマンティクス(意味)を表現するシンタックス(構文)を持つため、C++のJNIコードはCのそれに比べて構文的に多少簡潔である[注釈 2]。 Cでは C++では 以降のコード例では、断りがない限りC++を前提とするものとする。 データ型のマッピング一部のネイティブデータ型とJavaデータ型はそれぞれ相互変換をすることができる。オブジェクトや配列、文字列などの合成型のために、ネイティブコードは 以下の表ではJavaとネイティブ間のデータ型のマッピングを示す[2]。C/C++の型は<jni.h>ヘッダーにてtypedefにより定義されたエイリアスであり、実装は処理系依存である[注釈 3]。
JNIでJavaの型を識別する概念として、「型シグネチャ」が定義されている。 "L fully-qualified-class ;"というシグネチャは名前によって一意に識別されるJavaクラスを意味する。
例えば文字列 表中の対応する型同士は暗黙的に相互変換可能である。実際の変換はJNIレイヤーが担う。プログラマーは型キャスト無しで、Javaの なお、 // !!! 誤ったコード !!! //
JNIEXPORT void JNICALL
Java_com_example_TestClass_printString
(JNIEnv *env, jobject obj, jstring javaString)
{
// 未定義動作を引き起こす。
printf("%s", javaString);
}
// 正しいコード //
JNIEXPORT void JNICALL
Java_com_example_TestClass_printString
(JNIEnv *env, jobject obj, jstring javaString)
{
const char *nativeString = env->GetStringUTFChars(javaString, NULL);
printf("%s", nativeString);
env->ReleaseStringUTFChars(javaString, nativeString);
}
このことはJava配列についても同様である。例えば 配列の全ての要素の合計を得る例を用いて以下に示す。 // !!! 誤ったコード !!! //
JNIEXPORT jint JNICALL
Java_TestClass_sumIntArray
(JNIEnv *env, jobject obj, jintArray arr)
{
int sum = 0;
const jsize len = env->GetArrayLength(arr);
for (jsize i = 0; i < len; i++) {
sum += arr[i];
}
return sum;
}
// 正しいコード //
JNIEXPORT jint JNICALL
Java_TestClass_sumIntArray
(JNIEnv *env, jobject obj, jintArray arr)
{
jint sum = 0;
const jsize len = env->GetArrayLength(arr);
jint *buf = env->GetIntArrayElements(arr, NULL);
// コピーする場合は GetIntArrayRegion() を用いる方法もある。
for (jsize i = 0; i < len; i++) {
sum += buf[i];
}
env->ReleaseIntArrayElements(arr, buf, JNI_ABORT);
return sum;
}
Javaの
なお、Javaの文字型および文字列型の内部表現はUTF-16であり、 JNIEnvJNIのAPI関数の多くはJava VM環境変数として、JNIEnvへのポインタを受け取る。 しかしながら、Javaからnativeメソッドを呼び出す際に、C/C++実装側の関数に第1引数として渡ってくるJNIEnvは、そのnativeメソッド呼び出しの間のみ有効となる。つまりスタックフレームの制御フローがJava側に戻ると無効になる。 またJNIEnvはスレッド間で共有できない。そのため、Java VMに関連付けられていないC/C++スレッド上でJNIEnvを取得するためには、以下のようにAttachCurrentThread()関数を使用してスレッドをVMにアタッチする必要がある。スレッドでJNIEnvが必要なくなったとき、あるいはスレッドを終了する際にDetachCurrentThread()関数を使用してスレッドをVMからデタッチする。 JavaVM *g_vm; // JNI_CreateJavaVM() を使って作成済み、または JNI_GetCreatedJavaVMs() を使って取得済みとする。
JNIEnv *env = NULL;
// 現在のスレッドを VM にアタッチする。
g_vm->AttachCurrentThread((void **)&env, NULL);
// 取得した JNIEnv を使って JNI 関数を実行する。
...
// 現在のスレッドを VM からデタッチする。
g_vm->DetachCurrentThread();
すでにJava VMに関連付けられているC/C++スレッド上でJNIEnvを取得するためには、GetEnv()関数を呼び出すだけでよい。 JNIEnv *env = NULL;
g_vm->GetEnv((void **)&env, JNI_VERSION_1_6);
JavaクラスへのアクセスJNIを経由することで、C/C++からJavaのクラスを利用することもできる。 C/C++からJNI関数を用いてJavaクラスをインスタンス化したり、メソッドを呼び出したり、フィールドを読み書きしたりするためには、まずJavaクラス ( 以下は void DoJniTest(JNIEnv *env) {
jclass jcMath = env->FindClass("java/lang/Math");
jfieldID jfMathPi = env->GetStaticFieldID(jcMath, "PI", "D"); // static double PI
jmethodID jmMathMinI = env->GetStaticMethodID(jcMath, "min", "(II)I"); // static int min(int a, int b)
jmethodID jmMathMaxI = env->GetStaticMethodID(jcMath, "max", "(II)I"); // static int max(int a, int b)
const jdouble pi = env->GetStaticDoubleField(jcMath, jfMathPi);
const jint arg1 = 10, arg2 = -7;
const jint minVal = env->CallStaticIntMethod(jcMath, jmMathMinI, arg1, arg2);
const jint maxVal = env->CallStaticIntMethod(jcMath, jmMathMaxI, arg1, arg2);
printf("%f, %d, %d\n", pi, static_cast<int>(minVal), static_cast<int>(maxVal));
env->DeleteLocalRef(jcMath);
}
FindClass()関数で取得した ローカル参照とグローバル参照特に断りがない限り、JNIのAPI関数が返却するJNIオブジェクト参照は、Javaオブジェクトへの「ローカル参照」を表すポインタである。これはJavaの参照型ローカル変数に相当する。ローカル参照はスタックフレームの制御フローがJava側に返る際に自動的に削除され、Javaオブジェクトはガベージコレクションの管理対象となるが、DeleteLocalRef()関数を使って手動で削除することもできる[5]。 JNIオブジェクト参照を長期間保持する場合は、「グローバル参照」を使わなければならない[6][7]。 具体的には、C/C++側の静的変数(グローバル変数)などのヒープ領域に Javaのスレッドから呼ばれたC関数内でローカル参照を作成した場合、スタックフレームの制御フローがJava側に戻った時点でローカル参照は自動解放される。また、C/C++のスレッド上でローカル参照を作成した場合、スレッドがJVMからデタッチされた時点でローカル参照は自動解放される[10]。一方、グローバル参照は自動解放されず、DeleteGlobalRef()関数で明示的に削除する必要がある。 グローバル参照はヒープメモリが許す限り自由に作成可能だが、ローカル参照の同時利用可能個数には上限がある。JNI仕様で保証されているのは少なくとも16個ということだけであり、JVMのスタック容量に依存する[3][4][11]。 JNI参照はJavaのヒープに対する直接のポインタではなく、メモリコンパクションによるJavaオブジェクトのアドレス移動の影響を受けない[12]。2つのJNI参照が同じJavaオブジェクトを参照しているかどうかを調べるには、IsSameObject()関数を使用する。 JNIのローカル参照またはグローバル参照が残っている間は、参照先のJavaオブジェクトはGCルートから到達可能であり、ガベージコレクションの対象とはならないため、これらを削除し忘れるとメモリリークしてしまう。 脚注注釈
出典
関連項目
外部リンク
|