ネイティブ・イメージのJava Native Interface (JNI)

Java Native Interface (JNI)は、Javaコードとネイティブ・コードの相互の対話を可能にするネイティブAPIです。このページでは、ネイティブ・イメージのJNI実装の概要を説明します。

JNIサポートはデフォルトで有効になっており、ネイティブ・イメージに組み込まれています。JNIを介してアクセス可能にする個々のクラス、メソッドおよびフィールドは、イメージのビルド時に構成ファイルで指定する必要があります(後述の説明を参照)。

Javaコードで、System.loadLibrary()を使用して共有オブジェクトからネイティブ・コードをロードできます。または、ネイティブ・コードでJVMのネイティブ・ライブラリをロードし、JNIの呼出しAPIを使用してそのJava環境に接続できます。ネイティブ・イメージのJNI実装では両方のアプローチがサポートされています。

目次

リフレクション・メタデータ

JNIでは、名前によるクラスの参照、および名前とシグネチャによるメソッドとフィールドの参照がサポートされています。そのためには、これらのルックアップに必要なメタデータを保持する必要があります。native-imageビルダーでは、他の方法ではアクセスできないためにネイティブ・イメージに含まれない場合に備えて、参照されるアイテムを事前に認識しておく必要があります。さらに、native-imageでは、JNIを介してコールできるすべてのメソッドに対するコール・ラッパー・コードを事前に生成する必要があります。したがって、JNIを介してアクセス可能にする必要があるアイテムの簡潔なリストを指定すると、それらのアイテムを確実に使用でき、さらにフットプリントが小さくなります。このようなリストは、次のイメージ・ビルド引数で指定できます:

-H:JNIConfigurationFiles=/path/to/jniconfig

ここで、jniconfigはJSON構成ファイルです。JNIメタデータを指定するためのJSONスキーマは、こちらで確認してください。

native-imageビルダーでは、構成ファイルで参照されるすべてのクラス、メソッドおよびフィールドのJNIリフレクション・メタデータが生成されます。JNIConfigurationFilesに複数のパスを指定し、それらを,で区切ることで、複数のJNI構成を使用できます。また、-H:JNIConfigurationResourcesを指定すると、イメージ・ビルドのクラスパス(JARファイルなど)から1つ以上の構成ファイルをロードできます。

または、カスタムの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=<fully qualified name of JNIRegistrationFeature class>をnative-imageに渡す必要があります。ネイティブ・イメージ・ビルド構成では、META-INF/native-imagenative-image.propertiesファイルでこれを自動化する方法について説明します。

java.lang.reflectのサポート

JNI関数FromReflectedMethodおよびToReflectedMethodを使用すると、java.lang.reflect.Methodまたはjava.lang.reflect.Constructorオブジェクトに対応するjmethodIDを取得できます(その逆も可能)。関数FromReflectedFieldおよびToReflectedFieldでは、jfieldIDjava.lang.reflect.Fieldの間で変換が行われます。これらの関数を使用するには、リフレクションのサポートを有効にし、-H:ReflectionConfigurationFiles=で指定されたリフレクション構成に該当するメソッドおよびフィールドを含める必要があります。

オブジェクト・ハンドル

JNIではJavaオブジェクトへの直接アクセスは許可されていません。かわりに、JNIは、オブジェクトに間接的にアクセスするためにJNI関数に渡すことができるワードサイズのオブジェクト・ハンドルに対応しています。ローカル・ハンドルは、ネイティブ・コールの期間中にのみコール元のスレッドでのみ有効ですが、グローバル・ハンドルは、複数のスレッド間で有効であり、明示的に破棄されるまで有効に保たれます。ハンドル0はNULLを表します。

ネイティブ・イメージでは、スレッド固有の増大する参照オブジェクトの配列を使用してローカル・ハンドルが実装され、配列内のインデックスがハンドル値になります。fingerは、次のハンドルが割り当てられる場所を指します。ネイティブ・コールはネストできるため、ネイティブ・メソッドが呼び出される前に、コール・スタブによって現在のfingerがスタックにプッシュされ、復帰後に、スタックから古いfingerがリストアされて、配列に対するコールからのすべてのオブジェクト参照がNull化されます。

グローバル・ハンドルは、参照オブジェクトがアトミック操作を使用して挿入および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>MethodCallStatic<Type>MethodまたはCallNonvirtual<Type>Method関数のいずれかを使用することで、Javaメソッドを呼び出すことができます。これらの各Call...関数は、可変個引数のかわりに引数を配列またはva_listとして取るCall...MethodAおよびCall...MethodVバリアントでも使用できます。たとえば:

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では、2つの方法でJavaオブジェクトを作成できます。1つは、AllocObjectをコールしてメモリーを割り当ててから、CallVoidMethodを使用してコンストラクタを呼び出す方法、もう1つは、NewObjectを使用して単一のステップ(またはバリアント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);

ネイティブ・イメージでは、両方のアプローチがサポートされています。コンストラクタは、<init>というメソッド名とともにJNI構成に含まれている必要があります。NewObjectの追加のコール・ラッパーを生成するかわりに、通常のCallVoidMethodラッパーが再利用されます。このラッパーにターゲット・クラスのClassオブジェクトが渡されて、NewObjectを介したコールの実行が検出されます。その場合、コール・ラッパーにより、実際のコンストラクタを呼び出す前に新しいインスタンスが割り当てられます。

フィールドへのアクセス

ネイティブ・コードでは、jfieldIDを取得し、Get<Type>FieldSet<Type>FieldGetStatic<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コードで発生した例外を捕捉して保持する必要があります。ネイティブ・イメージでは、これはネイティブからJavaへのコール・ラッパーおよびJNI関数の実装で行われます。ネイティブ・コードでは、ExceptionCheckExceptionOccurredExceptionDescribeおよびExceptionClear関数を使用して、保留中の例外を問い合せてクリアできます。ネイティブ・コードでは、ThrowThrowNewまたはFatalErrorを使用して、例外をスローすることもできます。例外がネイティブ・コードで未処理のままであるか、ネイティブ・コード自体によって例外がスローされると、Javaコードの再入力時にJavaからネイティブへのコール・ラッパーによってその例外が再スローされます。

モニター

JNIでは、関数MonitorEnterおよびMonitorExitを宣言することによって、オブジェクトの組込みロックを取得および解放します。ネイティブ・イメージは、これらの関数の実装に対応しています。ただし、native-imageビルダーでは、Javaのsynchronized文とwait()notify()およびnotifyAll()で使用されているときに分析で監視できるクラスのオブジェクトにのみ、組込みロックが直接割り当てられます。その他のオブジェクトの場合、同期は低速なメカニズムにフォールバックされます。このメカニズムでは、マップを使用してロックの関連付けが格納され、それ自体で同期が必要になります。そのため、Javaコードで同期をラップすると効果的です。