JNI呼出しAPI

ネイティブ・イメージを使用すると、Javaで低レベルのシステム操作を実装し、標準JVMで実行されるJavaコードでJNI呼出しAPIを介して使用できるようにすることができます。そのため、同じ言語を使用してアプリケーション・ロジックとシステム・コールを記述できます。

このドキュメントでは、JNIを介して一般的に実行される処理とは反対のことについて説明します。通常、低レベルのシステム操作はCで実装され、JNIを使用してJavaから呼び出されます。ネイティブ・イメージにおける一般的なユース・ケースのサポートの詳細は、ネイティブ・イメージのJava Native Interface (JNI)を参照してください。

共有ライブラリの作成

まず、native-imageビルダーを使用して、JNI互換のエントリ・ポイントを持つ共有ライブラリを生成する必要があります。次のJavaコードから始めます:

package org.pkg.implnative;

import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.word.Pointer;

public final class NativeImpl {
    @CEntryPoint(name = "Java_org_pkg_apinative_Native_add")
    public static int add(Pointer jniEnv, Pointer clazz, @CEntryPoint.IsolateThreadContext long isolateId, int a, int b) {
        return a + b;
    }
}

native-imageビルダーによるコードの処理後、Java_org_pkg_apinative_Native_add (名前は後で便利なようにJNIの命名規則に準拠) C関数が公開され、JNIメソッドの標準的なネイティブ・イメージ・シグネチャが公開されます。1つ目のパラメータは、JNIEnv*値の参照です。2つ目のパラメータは、メソッドを宣言するクラスのjclass値の参照です。3つ目のパラメータは、ネイティブ・イメージ分離スレッドの移植可能な(longなど)識別子です。残りのパラメータは、次の項で説明するJava Native.addメソッドの実際のパラメータです。--sharedオプションを使用してコードをコンパイルします:

$JAVA_HOME/bin/native-image --shared -H:Name=libnativeimpl -cp nativeimpl

libnativeimpl.soが生成されます。標準Javaコードから使用できるようになりました。

Javaネイティブ・メソッドのバインド

前のステップで生成されたネイティブ・ライブラリを使用するには、別のJavaクラスが必要です:

package org.pkg.apinative;

public final class Native {
    private static native int add(long isolateThreadId, int a, int b);
}

クラスのパッケージ名とメソッド名は、前に導入された@CEntryPointの名前に対応している必要があります(JNIマングリングの後)。1つ目の引数は、ネイティブ・イメージ分離スレッドの移植可能な(longなど)識別子です。残りの引数は、エントリ・ポイントのパラメータと一致します。

ネイティブ・ライブラリのロード

次のステップでは、生成された.soライブラリにJDKをバインドします。たとえば、ネイティブのNative.addメソッドの実装がロードされていることを確認します。単純なloadまたはloadLibraryコールでは、次の処理が実行されます:

public static void main(String[] args) {
    System.loadLibrary("nativeimpl");
    // ...
}

これは、LD_LIBRARY_PATH環境変数が指定されているか、java.library.path Javaプロパティが正しく設定されていることを前提としています。

ネイティブ・イメージ分離の初期化

Native.addメソッドをコールする前に、ネイティブ・イメージ分離を作成する必要があります。ネイティブ・イメージには、そのための特別な組込み機能(CEntryPoint.Builtin.CREATE_ISOLATE)が用意されています。他の既存の@CEntryPointメソッドに沿って別のメソッドを定義します。パラメータを渡さずにIsolateThreadが返されるようにします:

public final class NativeImpl {
    @CEntryPoint(name = "Java_org_pkg_apinative_Native_createIsolate", builtin=CEntryPoint.Builtin.CREATE_ISOLATE)
    public static native IsolateThread createIsolate();
}

その後、ネイティブ・イメージにより、メソッドのデフォルトのネイティブ実装が最終的な.soライブラリに生成されます。このメソッドにより、ネイティブ・イメージ・ランタイムが初期化され、ネイティブ・イメージ分離スレッドのインスタンスを保持する移植可能なID (longなど)が返されます。分離スレッドは、コードのネイティブ部分の複数の呼出しに使用できます:

package org.pkg.apinative;

public final class Native {
    public static void main(String[] args) {
        System.loadLibrary("nativeimpl");

        long isolateThread = createIsolate();

        System.out.println("2 + 40 = " + add(isolateThread, 2, 40));
        System.out.println("12 + 30 = " + add(isolateThread, 12, 30));
        System.out.println("20 + 22 = " + add(isolateThread, 20, 22));
    }

    private static native int add(long isolateThread, int a, int b);
    private static native long createIsolate();
}

標準JVMが起動されます。ネイティブ・イメージ分離が初期化され、現在のスレッドが分離にアタッチされて、ユニバーサル・アンサー42が分離内で3回計算されます。

ネイティブJavaからのJVMのコール

ネイティブ・イメージのCインタフェースに関する詳細なチュートリアルが用意されています。次の例は、JVMへのコールバックを行う方法を示しています。

従来の設定では、CでJVMをコールする必要がある場合、jni.hヘッダー・ファイルが使用されます。このファイルでは、必須のJVM構造(JNIEnvなど)に加え、JVM内のクラスの検査、フィールドへのアクセスおよびメソッドのコールを行うために呼び出すことができる関数を定義します。前述の例のNativeImplクラスからこれらの関数をコールするには、jni.h概念の適切なJava APIラッパーを定義する必要があります:

@CContext(JNIHeaderDirectives.class)
@CStruct(value = "JNIEnv_", addStructKeyword = true)
interface JNIEnvironment extends PointerBase {
    @CField("functions")
    JNINativeInterface getFunctions();
}

@CPointerTo(JNIEnvironment.class)
interface JNIEnvironmentPointer extends PointerBase {
    JNIEnvironment read();
    void write(JNIEnvironment value);
}

@CContext(JNIHeaderDirectives.class)
@CStruct(value = "JNINativeInterface_", addStructKeyword = true)
interface JNINativeInterface extends PointerBase {
    @CField
    GetMethodId getGetStaticMethodID();

    @CField
    CallStaticVoidMethod getCallStaticVoidMethodA();
}

interface GetMethodId extends CFunctionPointer {
    @InvokeCFunctionPointer
    JMethodID find(JNIEnvironment env, JClass clazz, CCharPointer name, CCharPointer sig);
}

interface JObject extends PointerBase {
}

interface CallStaticVoidMethod extends CFunctionPointer {
    @InvokeCFunctionPointer
    void call(JNIEnvironment env, JClass cls, JMethodID methodid, JValue args);
}

interface JClass extends PointerBase {
}
interface JMethodID extends PointerBase {
}

現時点では、JNIHeaderDirectivesの意味を除けば、残りのインタフェースはjni.hファイルにあるCポインタの型安全な表現です。JClassJMethodIDおよびJObjectはすべてポインタです。前述の定義により、これらのオブジェクトのインスタンスをネイティブJavaコードで型安全な方法で表現するためのJavaインタフェースが作成されました。

JNI APIのコア部分は、JVMと通信するときにコールできる関数のセットです。それらは多数ありますが、JNINativeInterface定義では、単に、この例で必要なものに対してラッパーを定義します。この場合も、適切な型を指定して、ネイティブJavaコードでGetMethodId.find(...)CallStaticVoidMethod.call(...)などを使用できるようにします。欠落するパズルのピースとなるもう1つの重要な部分は、使用可能なすべてのJavaプリミティブ型およびオブジェクト型をラップするjvalue共用体型です。getterおよびsetterの定義を次に示します:

@CContext(JNIHeaderDirectives.class)
@CStruct("jvalue")
interface JValue extends PointerBase {
    @CField boolean z();
    @CField byte b();
    @CField char c();
    @CField short s();
    @CField int i();
    @CField long j();
    @CField float f();
    @CField double d();
    @CField JObject l();


    @CField void z(boolean b);
    @CField void b(byte b);
    @CField void c(char ch);
    @CField void s(short s);
    @CField void i(int i);
    @CField void j(long l);
    @CField void f(float f);
    @CField void d(double d);
    @CField void l(JObject obj);

    JValue addressOf(int index);
}

addressOfメソッドは、Cポインタ演算を実行するために使用される特殊なネイティブ・イメージ構造体です。ポインタを指定すると、それを配列の初期要素として扱い、たとえば、addressOf(1)を使用して後続の要素にアクセスできます。これにより、コールバックの実行に必要なすべてのAPIが得られます。前に導入したNativeImpl.addメソッドを再定義して適切に型指定されたポインタが受け入れられるようにし、これらのポインタを使用してJVMメソッドを呼び出してから、a + bの合計を計算します:

@CEntryPoint(name = "Java_org_pkg_apinative_Native_add")
static int add(JNIEnvironment env, JClass clazz, @CEntryPoint.IsolateThreadContext long isolateThreadId, int a, int b) {
    JNINativeInterface fn = env.getFunctions();

    try (
        CTypeConversion.CCharPointerHolder name = CTypeConversion.toCString("hello");
        CTypeConversion.CCharPointerHolder sig = CTypeConversion.toCString("(ZCBSIJFD)V");
    ) {
        JMethodID helloId = fn.getGetStaticMethodID().find(env, clazz, name.get(), sig.get());

        JValue args = StackValue.get(8, JValue.class);
        args.addressOf(0).z(false);
        args.addressOf(1).c('A');
        args.addressOf(2).b((byte)22);
        args.addressOf(3).s((short)33);
        args.addressOf(4).i(39);
        args.addressOf(5).j(Long.MAX_VALUE / 2l);
        args.addressOf(6).f((float) Math.PI);
        args.addressOf(7).d(Math.PI);
        fn.getCallStaticVoidMethodA().call(env, clazz, helloId, args);
    }

    return a + b;
}

前述の例では、静的メソッドhelloを検索し、スタック上のStackValue.getによって予約されている配列内の8つのJValueパラメータを使用して呼び出します。個々のパラメータはaddressOf演算子を使用してアクセスされ、コールが発生する前に適切なプリミティブ値が入力されます。メソッドhelloはクラスNativeに定義されています。このメソッドにより、すべてのパラメータの値が出力されるため、NativeImpl.addのコール元から正しく伝播されたことを確認できます:

public class Native {
    public static void hello(boolean z, char c, byte b, short s, int i, long j, float f, double d) {
        System.err.println("Hi, I have just been called back!");
        System.err.print("With: " + z + " " + c + " " + b + " " + s);
        System.err.println(" and: " + i + " " + j + " " + f + " " + d);
    }

説明が必要な最後のピースはJNIHeaderDirectivesです。ネイティブ・イメージのCインタフェースでは、C構造体のレイアウトを認識できる必要があります。JNINativeInterface構造体のどのオフセットでGetMethodId関数へのポインタを検出できるかを認識する必要があります。そのためには、コンパイル時にjni.hおよび追加ファイルが必要になります。これらは、@CContext注釈とそのDirectivesの実装によって指定できます:

final class JNIHeaderDirectives implements CContext.Directives {
    @Override
    public List<String> getOptions() {
        File[] jnis = findJNIHeaders();
        return Arrays.asList("-I" + jnis[0].getParent(), "-I" + jnis[1].getParent());
    }

    @Override
    public List<String> getHeaderFiles() {
        File[] jnis = findJNIHeaders();
        return Arrays.asList("<" + jnis[0] + ">", "<" + jnis[1] + ">");
    }

    private static File[] findJNIHeaders() throws IllegalStateException {
        final File jreHome = new File(System.getProperty("java.home"));
        final File include = new File(jreHome.getParentFile(), "include");
        final File[] jnis = {
            new File(include, "jni.h"),
            new File(new File(include, "linux"), "jni_md.h"),
        };
        return jnis;
    }
}

利点は、すべてのJDK内にjni.hが含まれていることから、java.homeプロパティを使用して必要なヘッダー・ファイルを検索できることです。もちろん、実際のロジックは、より堅牢でOS非依存にすることもできます。

JavaでのJVMネイティブ・メソッドの実装またはネイティブ・イメージを使用したJVMへのコールバック(あるいはその両方)は、提示された例を発展させてnative-imageを呼び出すことで簡単に実行できるようになりました。