- JDK 23対応GraalVM (最新)
- JDK 24対応GraalVM (早期アクセス)
- JDK 21対応GraalVM
- JDK 17対応GraalVM
- アーカイブ
- 開発ビルド
ネイティブイメージにおけるJava Native Interface (JNI)
Java Native Interface (JNI) は、Javaコードとネイティブコードが相互作用することを可能にするネイティブAPIです。このページでは、ネイティブイメージにおけるJNIの実装の概要について説明します。
JNIのサポートはデフォルトで有効になっており、ネイティブイメージに組み込まれています。JNIを介してアクセス可能にする必要がある個々のクラス、メソッド、およびフィールドは、イメージのビルド時に設定ファイルで指定する必要があります(下記参照)。
Javaコードは、`System.loadLibrary()`を使用して共有オブジェクトからネイティブコードをロードできます。あるいは、ネイティブコードはJVMのネイティブライブラリをロードし、呼び出しAPIを使用してJava環境にアタッチできます。ネイティブイメージのJNI実装は、どちらの方法もサポートしています。
目次 #
- ネイティブライブラリのロード
- リフレクションメタデータ
- オブジェクトハンドル
- Javaからネイティブへのメソッド呼び出し
- ネイティブからJavaへのメソッド呼び出し
- JNI関数
- オブジェクトの作成
- フィールドアクセス
- 例外
- モニタ
ネイティブライブラリのロード #
`System.loadLibrary()`(および関連API)を使用してネイティブライブラリをロードする場合、ネイティブイメージはJavaライブラリパスを検索する前に、ネイティブイメージを含むディレクトリを検索します。そのため、ロードするネイティブライブラリがネイティブイメージと同じディレクトリにある限り、他の設定は必要ありません。
リフレクションメタデータ #
JNIは、名前でクラスを検索し、名前とシグネチャでメソッドとフィールドを検索することをサポートしています。 これには、これらのルックアップに必要なメタデータを保持する必要があります。 `native-image`ビルダーは、そうでなければ到達できない可能性があり、ネイティブイメージに含まれない場合に備えて、どの項目が検索されるかを事前に知っておく必要があります。 さらに、`native-image`は、JNIを介して呼び出すことができるすべてのメソッドに対して、事前にラッパーコードを生成する必要があります。 したがって、JNIを介してアクセス可能にする必要がある項目の簡潔なリストを指定することで、それらの可用性が保証され、フットプリントを小さくすることができます。 このようなリストは、_reachability-metadata.json_ファイルで指定する必要があります。
JNI構成は、GraalVM JDKのトレースエージェントを使用して自動的に収集できます。 エージェントは、通常のJava VMでのアプリケーション実行中に、動的機能のすべての使用状況を追跡します。 アプリケーションが完了し、JVMが終了すると、エージェントは指定された出力ディレクトリにJSONファイルとして構成を書き込みます。 生成された設定ファイルをその出力ディレクトリからクラスパス上の_META-INF/native-image/_に移動すると、ビルド時に自動的に使用されます。 `native-image`ビルダーは、_META-INF/native-image/_とそのサブディレクトリで、_reachability-metadata.json_という名前のファイル、または_reflect-config.json_などのレガシーファイルを検索します。
または、カスタム`Feature`実装は、`JNIRuntimeAccess`クラスを使用して、イメージビルドの分析フェーズの前および分析フェーズ中にプログラム要素を登録できます。 例えば
class JNIRegistrationFeature implements Feature {
public void beforeAnalysis(BeforeAnalysisAccess access) {
try {
JNIRuntimeAccess.register(String.class);
JNIRuntimeAccess.register(String.class.getDeclaredField("value"));
JNIRuntimeAccess.register(String.class.getDeclaredField("hash"));
JNIRuntimeAccess.register(String.class.getDeclaredConstructor(char[].class));
JNIRuntimeAccess.register(String.class.getDeclaredMethod("charAt", int.class));
JNIRuntimeAccess.register(String.class.getDeclaredMethod("format", String.class, Object[].class));
JNIRuntimeAccess.register(String.CaseInsensitiveComparator.class);
JNIRuntimeAccess.register(String.CaseInsensitiveComparator.class.getDeclaredMethod("compare", String.class, String.class));
} catch (NoSuchMethodException | NoSuchFieldException e) { ... }
}
}
カスタム機能を有効にするには、`--features=<JNIRegistrationFeatureクラスの完全修飾名>`を`native-image`ビルダーに渡します。 ネイティブイメージのビルド設定では、_META-INF/native-image_にある`native-image.properties`ファイルを使用してこれを自動化する方法について説明しています。
java.lang.reflectのサポート #
JNI関数`FromReflectedMethod`と`ToReflectedMethod`を使用して、`java.lang.reflect.Method`または`java.lang.reflect.Constructor`オブジェクトに対応する`jmethodID`を取得したり、その逆も可能です。 関数`FromReflectedField`と`ToReflectedField`は、`jfieldID`と`java.lang.reflect.Field`を変換します。 これらの関数を使用するには、リフレクションのサポートが有効になっている必要があり、問題のメソッドとフィールドがリフレクションメタデータに含まれている必要があります。
オブジェクトハンドル #
JNIは、Javaオブジェクトへの直接アクセスを許可しません。 代わりに、JNIは、オブジェクトに間接的にアクセスするためにJNI関数に渡すことができるワードサイズのオブジェクトハンドルを提供します。 ローカルハンドルは、ネイティブ呼び出しの期間中のみ、呼び出し元の スレッドでのみ有効ですが、グローバルハンドルはスレッド全体で有効であり、明示的に破棄されるまで有効です。 ハンドル0は`NULL`を表します。
ネイティブイメージは、参照されるオブジェクトのスレッドローカルの増加配列を使用してローカルハンドルを実装します。ここで、配列内のインデックスはハンドル値です。 「フィンガー」は、次のハンドルが割り当てられる場所を指します。 ネイティブ呼び出しはネストできるため、ネイティブメソッドが呼び出される前に、呼び出しスタブは現在のフィンガーをスタックにプッシュし、戻った後にスタックから古いフィンガーを復元し、配列内の呼び出しからのすべてのオブジェクト参照を無効にします。
グローバルハンドルは、参照されるオブジェクトが挿入され、アトミック操作を使用して無効にされる可変数のオブジェクト配列を使用して実装されます。 グローバルハンドルの値は、包含配列のインデックスと配列内のインデックスから決定される負の整数です。 したがって、JNIコードは、符号を見るだけでローカルハンドルとグローバルハンドルを区別できます。 分析はオブジェクトハンドルによって妨げられることはありません。これは、オブジェクト参照のフロー全体を観察でき、ネイティブコードに渡されるハンドルは数値のみであるためです。
Javaからネイティブへのメソッド呼び出し #
`native`キーワードで宣言されたメソッドは、ネイティブコードでJNI準拠の実装を持っていますが、他のJavaメソッドと同様に呼び出すことができます。 例えば
// Java declaration
native int[] sort0(int[] array);
// native declaration with JNI name mangling
jintArray JNICALL Java_org_example_sorter_IntSorter_sort0(JNIEnv *env, jobject this, jintArray array)
イメージビルドがネイティブとして宣言されたメソッドに遭遇すると、ネイティブコードとの間の遷移を実行するラッパーを含むグラフを生成し、`JNIEnv *`と`this`引数を追加し、ハンドル内のオブジェクト引数をボックス化し、オブジェクト戻り値型の場合は、返されたハンドルをボックス化解除します。
実際のネイティブ呼び出しターゲットアドレスは、実行時にのみ決定できます。 したがって、`native-image`ビルダーは、ネイティブ宣言メソッドのリフレクションメタデータに追加のリンケージオブジェクトも作成します。 ネイティブメソッドが呼び出されると、呼び出しラッパーはロードされたすべてのライブラリで一致するシンボルを検索し、将来の呼び出しのために解決されたアドレスをリンケージオブジェクトに格納します。 あるいは、JNI名前マングリングスキームに準拠するシンボルを要求する代わりに、ネイティブイメージは`RegisterNatives` JNI関数をサポートして、ネイティブメソッドのコードアドレスを明示的に提供します。
ネイティブからJavaへのメソッド呼び出し #
ネイティブコードは、最初にターゲットメソッドの`jmethodID`を取得し、次に呼び出しに`Call<Type>Method`、`CallStatic<Type>Method`、または`CallNonvirtual<Type>Method`関数のいずれかを使用することで、Javaメソッドを呼び出すことができます。 これらの各`Call ...`関数は、`Call ... MethodA`と`Call ... MethodV`バリアントでも使用できます。これらは、可変個引数ではなく、配列または`va_list`として引数を取ります。 例えば
jmethodID intcomparator_compare_method = (*env)->GetMethodID(env, intcomparator_class, "compare", "(II)I");
jint result = (*env)->CallIntMethod(env, this, intcomparator_compare_method, a, b);
`native-image`ビルダーは、提供されたJNI構成に従って、JNIを介して呼び出すことができる各メソッドの呼び出しラッパーを生成します。 呼び出しラッパーは、メソッドに適したJNI `Call ...`関数のシグネチャに準拠しています。 ラッパーは、Javaコードとの間の遷移を実行し、引数リストをターゲットJavaメソッドのシグネチャに適合させ、渡されたオブジェクトハンドルをボックス化解除し、必要に応じて、戻り値の型をオブジェクトハンドルにボックス化します。
JNIを介して呼び出すことができる各メソッドには、リフレクションメタデータオブジェクトがあります。 このオブジェクトのアドレスは、メソッドの`jmethodID`として使用されます。 メタデータオブジェクトには、メソッドの生成されたすべての呼び出しラッパーのアドレスが含まれています。 各呼び出しラッパーは対応する`Call ...`関数シグネチャに正確に準拠しているため、`Call ...`関数自体は、渡された`jmethodID`に基づいて適切な呼び出しラッパーに無条件にジャンプするだけです。 別の最適化として、呼び出しラッパーは、`JNIEnv *`引数から現在のスレッドのJavaコンテキストを効率的に復元できます。
JNI関数 #
JNIは、ネイティブコードがJavaコードと対話するために使用できる一連の関数を提 供します。 ネイティブイメージは、`@CEntryPoint`を使用してこれらの関数を実装します。例:
@CEntryPoint(...) private static void DeleteGlobalRef(JNIEnvironment env, JNIObjectHandle globalRef) { /* setup; */ JNIGlobalHandles.singleton().delete(globalRef); }
JNIは、これらの関数が、`JNIEnv *`引数を介してアクセスできるC構造体の関数ポインタを介して提供されることを指定しています。 この構造体の自動初期化は、イメージビルド中に準備されます。
オブジェクトの作成 #
JNIは、Javaオブジェクトを作成する2つの方法を提供します。AllocObject
を呼び出してメモリを割り当て、次にCallVoidMethod
を使用してコンストラクタを呼び出す方法、またはNewObject
を使用してオブジェクトの作成と初期化を1つのステップで行う方法(またはバリアントのNewObjectA
またはNewObjectV
)です。例えば
jclass calendarClass = (*env)->FindClass(env, "java/util/GregorianCalendar");
jmethodID ctor = (*env)->GetMethodID(env, calendarClass, "<init>", "(IIIIII)V");
jobject firstObject = (*env)->AllocObject(env, calendarClass);
(*env)->CallVoidMethod(env, obj, ctor, year, month, dayOfMonth, hourOfDay, minute, second);
jobject secondObject = (*env)->NewObject(env, calendarClass, ctor, year, month, dayOfMonth, hourOfDay, minute, second);
Native Imageは両方のアプローチをサポートしています。コンストラクタは、メソッド名が<init>
であるJNI構成に含まれている必要があります。 NewObject
用の追加の呼び出しラッパーを生成する代わりに、通常のCallVoidMethod
ラッパーが再利用され、ターゲットクラスのClass
オブジェクトが渡されるため、NewObject
を介して呼び出されたときに検出されます。その場合、呼び出しラッパーは、実際のコンストラクタを呼び出す前に新しいインスタンスを割り当てます。
フィールドアクセス #
ネイティブコードは、jfieldID
を取得し、Get<Type>Field
、Set<Type>Field
、GetStatic<Type>Field
、またはSetStatic<Type>Field
関数のいずれかを使用することで、Javaフィールドにアクセスできます。例えば
jfieldID intsorter_comparator_field = (*env)->GetFieldID(env, intsorter_class, "comparator", "Lorg/example/sorter/IntComparator;");
jobject value = (*env)->GetObjectField(env, self, intsorter_comparator_field);
JNIを介してアクセス可能なフィールドの場合、オブジェクト内(または静的フィールド領域内)のオフセットはリフレクションメタデータに格納され、jfieldID
として使用されます。 native-image
ビルダーは、すべてのプリミティブ型のフィールドとオブジェクトフィールドのアクセサメソッドを生成します。これらのアクセサメソッドは、Javaコードへの遷移とその逆を実行し、安全でないロードまたはストアを使用してフィールド値を直接操作します。分析では、JNIを介したオブジェクトフィールドの割り当てを観察できないため、JNIを介してアクセス可能なフィールドには、フィールドの宣言された型の任意のサブタイプが存在すると想定されます。
JNIでは、final
と宣言されたフィールドへの書き込みも許可されています。これは、構成ファイルのallowWrite
プロパティを使用して個々のフィールドに対して有効にする必要があります。ただし、最適化のため、finalフィールドにアクセスするコードは、final以外のフィールドと同じ方法でfinalフィールド値の変更を観察できない場合があります。
例外 #
JNIは、ネイティブコードからの呼び出しの結果であるJavaコードの例外をキャッチして保持する必要があることを指定しています。Native Imageでは、これはネイティブからJavaへの呼び出しラッパーとJNI関数の実装で行われます。ネイティブコードは、ExceptionCheck
、ExceptionOccurred
、ExceptionDescribe
、およびExceptionClear
関数を使用して、保留中の例外を照会およびクリアできます。ネイティブコードは、Throw
、ThrowNew
、またはFatalError
を使用して例外をスローすることもできます。ネイティブコードで例外が処理されないままになっている場合、またはネイティブコード自体が例外をスローした場合、Javaからネイティブへの呼び出しラッパーは、Javaコードに再入するときにその例外を再スローします。
モニタ #
JNIは、オブジェクトの固有ロックを取得および解放するための関数MonitorEnter
およびMonitorExit
を宣言します。Native Imageは、これらの関数の 実装を提供します。ただし、native-image
ビルダーは、分析でJavaのsynchronized
ステートメントおよびwait()
、notify()
、notifyAll()
で使用されていると観察できるクラスのオブジェクトにのみ固有ロックを直接割り当てます。他のオブジェクトの場合、同期は、ロックの関連付けを格納するマップを使用する低速なメカニズムにフォールバックします。このマップ自体は同期を必要とします。そのため、Javaコードで同期をラップすると beneficial な場合があります。