Java Native Interface
Java Native Interface (JNI) は、Javaプラットフォームにおいて、Javaで記述されたプログラムと、他のプログラミング言語(たとえばCやC++など)で書かれた、実際のCPUの上で動作するコード(ネイティブコード)とを連携するためのインタフェース仕様である。Java言語からネイティブコードを利用するためのABIと、逆にネイティブコードからJavaバイトコードを動作させるためにバーチャルマシン (VM) を利用するためのAPIの2つから成る。
JNIを使うことで、Java言語のVMで動作させるには処理速度の面で不利とされる計算量の多いプログラムを部分的にネイティブコードに置き換えて高速化したり、標準クラスライブラリからはアクセスできないオペレーティングシステムの機能を利用するプログラムを、あたかも通常のJavaクラスのように呼び出したりできるようになる。Java言語以外のJava VM上で動作する言語からも利用可能である。
JNIによる、Java VMからのネイティブコードの呼び出しは、VMの実行環境の一貫性を保つために、通常のJavaプログラムの実行時とは異なる例外的なメモリ管理や排他制御を必要とする場合があり、しばしばプログラムの実行速度の低下を招くことがある。そのため、単純にJNIを利用することでアプリケーション性能を改善できるというわけではない。
JNIの動作
JNIフレームワークでは、ネイティブ関数は.cもしくは.cppファイルに分離して実装する。Java VMは、JNIEnv
へのポインタ、Javaクラス参照 (jclass
) もしくはクラスインスタンス参照を指すjobject
、そしてJavaメソッドで定義されたすべてのJava引数を通して、ネイティブな関数を起動する。JNI関数は以下のようなC言語形式関数となる。C++のシグネチャは使えない。
JNIEXPORT void JNICALL
Java_PackageName_ClassName_MethodName
(JNIEnv *env, jobject obj)
{
/* ネイティブコードをここに記述する */
}
env
ポインタはJava VMへのインタフェースを含む構造体である。これはJava VMとの相互作用および、Javaオブジェクトとの連携に必要な全ての関数を含んでいる。JNI関数の例としては、ネイティブ配列とJava配列の相互変換、ネイティブ文字列とJava文字列の相互変換、オブジェクトのインスタンス化、例外の送出などが挙げられる。基本的には、Javaコードでできることは全てJNIEnv
を用いて行うことができる。
以下の例ではJava文字列をネイティブな文字列に変換する。
// C++ code
extern "C" {
JNIEXPORT void JNICALL
Java_com_example_TestClass_printString
(JNIEnv *env, jobject obj, jstring javaString)
{
// Java 文字列より UTF-8 形式のネイティブ文字列を取得する。
const char *nativeString = env->GetStringUTFChars(javaString, NULL);
std::cout << nativeString << std::endl;
// ネイティブ文字列を解放する。
env->ReleaseStringUTFChars(javaString, nativeString);
}
}
/* C code */
JNIEXPORT void JNICALL
Java_com_example_TestClass_printString
(JNIEnv *env, jobject obj, jstring javaString)
{
const char *nativeString = (*env)->GetStringUTFChars(env, javaString, NULL);
printf("%s\n", nativeString);
(*env)->ReleaseStringUTFChars(env, javaString, nativeString);
}
上記のようにしてCまたはC++で記述した関数が myjni ライブラリモジュール[1]に実装されていると仮定し、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++は、JNIに対して多少洗練されたインターフェイスを提供する。C++ではJavaのように、オブジェクトのメソッド(メンバー関数)という概念を持つため、C++のJNIコードはCのそれに比べて文法的に多少簡潔である。
Cではenv
パラメータは(*env)->
で被参照され、
さらにenv
パラメータはオブジェクトメソッド起動セマンティクスの一部として、明示的にJNIEnv
メソッドに渡されなければならない。
C++ではenv
パラメータはenv->
で被参照されるが、env
パラメータはオブジェクトメソッド起動セマンティクスの一部として暗黙的に渡される。
変数型のマッピング
ネイティブ変数型とJava変数型はそれぞれ相互変換をすることができる。オブジェクトや配列、文字列などの合成型のために、ネイティブコードはJNIEnv
のメソッド呼び出しにおいて、明示的なデータの変換を行う必要がある。
以下の表ではJavaとネイティブ間の変数型のマッピングを示す。C/C++の型は<jni.h>ヘッダーにてtypedefにより定義されたエイリアスであり、実装は処理系依存である[2]。
C/C++変数型 | Java変数型 | 説明 | 型シグネチャ |
---|---|---|---|
jboolean | boolean | 符号無し8ビット整数型(論理型) | Z |
jbyte | byte | 符号付き8ビット整数型 | B |
jchar | char | 符号無し16ビット整数型(文字型) | C |
jshort | short | 符号付き16ビット整数型 | S |
jint | int | 符号付き32ビット整数型 | I |
jlong | long | 符号付き64ビット整数型 | J |
jfloat | float | 32ビット単精度浮動小数点数 | F |
jdouble | double | 64ビット倍精度浮動小数点数 | D |
C/C++からJNI関数を用いてJavaクラスをインスタンス化したり、メソッドを呼び出したり、フィールドを読み書きしたりするためには、まずJavaクラスオブジェクトおよびメソッドIDあるいはフィールドIDを探索・取得する必要があるが、その際に「型シグネチャ」が使用される。
上記表に加えて、"L fully-qualified-class ;"というシグネチャは名前によって一意に指定されたJavaクラスを意味する。
例えば文字列"Ljava/lang/String;"
はクラスjava.lang.String
を参照する。また、接頭辞[
はその型の配列を意味する。例えば、[I
はJavaにおけるint
型の配列すなわちint[]
に相当する。
表中のプリミティブ型は相互変換可能である。プログラマーは型キャスト無しで、Javaのint
型と同様にjint
を使用することができる。jint
型からJavaのint
型への変換も同様である。
しかし、文字列のJava-ネイティブ間のマッピングについては様相が異なる。jstring
はJavaのString
型への間接参照であり、C/C++のポインタ型char*
とは無関係である。
// !!! 誤ったコード !!! //
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配列についても同様である。例えばjintArray
はJavaのint[]
型への間接参照であり、C/C++のポインタ型int*
とは無関係である。
配列の全ての要素の合計を得る例を用いて以下に示す。
// !!! 誤ったコード !!! //
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;
}
JNIEnv
JNI関数の多くはJava VM環境変数として、JNIEnvへのポインタを受け取る。
しかしながら、Javaからnative関数を呼び出す際に、C/C++側に引数として渡ってくるJNIEnvは、JNI呼び出しの間のみ有効となる。
またJNIEnvはスレッド間で共有できない。そのため、Java VMに関連付けられていないC/C++スレッド上でJNIEnvを取得するためには、以下のようにAttachCurrentThread()を使用してスレッドをVMにアタッチする必要がある。スレッドでJNIEnvが必要なくなったとき、あるいはスレッドを終了する際にDetachCurrentThread()関数を使用してスレッドをVMからデタッチする。
JavaVM *g_vm; // JNI_GetCreatedJavaVMs() を使って取得済みとする。
JNIEnv *env = NULL;
// 現在のスレッドを VM にアタッチする。
g_vm->AttachCurrentThread((void **)&env, NULL);
// 取得した JNIEnv を使って JNI 関数を実行する。
...
// 現在のスレッドを VM からデタッチする。
g_vm->DetachCurrentThread(g_vm);
すでにJava VMに関連付けられているC/C++スレッド上でJNIEnvを取得するためには、GetEnv()関数を呼び出すだけでよい。
関連項目
- Foreign function interface
- Java Native Access: JNI を用いずにネイティブコードを呼び出すためのライブラリ
- P/Invoke, C++/CLI: .NET Frameworkにおける JNI と類似の仕組み
- SWIG: 多言語に対応したインターフェイス生成ツールで、C/C++のライブラリ用の JNI コードを生成する
外部リンク
- Java SE 7 Java Native Interface 関連 API および 開発者ガイド -- Oracle
- Java SE 7 Java Native Interface-related API's and Developer Guides -- Oracle
- Best practices for using the Java Native Interface
- GNU CNI Tutorial
- A JNI Tutorial at CodeProject.com (Microsoft specific)
- JNI Tutorial at CodeToad.com
- JNI in XCode from Apple
- ^ Microsoft Windowsの場合はmyjni.dllファイル。UNIX/Linuxの場合はlibmyjni.soファイル。
- ^ C/C++ではプラットフォームによって組み込み型のサイズが異なる。