JNI 呼び出し API

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

このドキュメントでは、通常 JNI で行われることとは逆のことを説明していることに注意してください。通常、低レベルのシステム操作は C で実装され、JNI を使用して Java から呼び出されます。一般的なユースケースで Native Image がどのようにサポートしているかについては、「Native Image における 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 ビルダーによって処理された後、コードは C 関数 Java_org_pkg_apinative_Native_add (名前は後で役立つ JNI の規約に従います) と、JNI メソッドに典型的な Native Image シグネチャを公開します。最初のパラメータは JNIEnv* 値への参照です。2 番目のパラメータは、メソッドを宣言するクラスの jclass 値への参照です。3 番目のパラメータは、Native Image アイソレートスレッド のポータブル (たとえば、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);
}

クラスのパッケージ名とメソッド名は、( JNI マンギング の後) 前に導入した @CEntryPoint の名前に対応する必要があります。最初の引数は、Native Image アイソレートスレッドのポータブル (たとえば、long) 識別子です。残りの引数は、エントリポイントのパラメータと一致します。

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

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

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

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

Native Image アイソレートの初期化 #

Native.add メソッドを呼び出す前に、Native Image アイソレートを作成する必要があります。Native Image は、それを可能にする特別な組み込み機能を提供します: 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();
}

次に Native Image は、メソッドのデフォルトのネイティブ実装を最終的な .so ライブラリに生成します。このメソッドは、Native Image ランタイムを初期化し、Native Image アイソレートスレッド のインスタンスを保持するためのポータブル識別 (たとえば、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 が起動されます。Native Image アイソレートを初期化し、現在のスレッドをアイソレートにアタッチすると、ユニバーサルな答え 42 がアイソレート内で 3 回計算されます。

ネイティブ Java から JVM を呼び出す #

Native Image の 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 ポインタの型安全な表現です。JClassJMethodIDJObject はすべてポインタです。上記の定義のおかげで、型安全な方法でネイティブ Java コードでこれらのオブジェクトのインスタンスを表すための Java インターフェイスができました。

すべての JNI API の中核部分は、JVM との対話時に呼び出すことができる一連の関数です。数十ありますが、JNINativeInterface 定義では、例で必要な数個のラッパーのみを定義します。ここでも、適切な型を指定すると、ネイティブ Java コードで GetMethodId.find(...)CallStaticVoidMethod.call(...) などを使用できます。さらに、パズルに欠けている重要な部分がもう 1 つあります。それは、可能なすべての Java プリミティブ型とオブジェクト型をラップする jvalue 共用体型です。次に、そのゲッターとセッターの定義を示します。

@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 ポインタ演算を実行するために使用される特別な Native Image コンストラクトです。ポインタを指定すると、それを配列の最初の要素として扱うことができ、たとえば、addressOf(1) を使用して後続の要素にアクセスできます。これでコールバックを行うために必要なすべての API が揃いました。以前に導入した NativeImpl.add メソッドを、適切に型指定されたポインタを受け入れるように再定義し、これらのポインタを使用して a + b の合計を計算する前に JVM メソッドを呼び出します。

@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);
    }

最後に説明するものが 1 つだけあります。それは JNIHeaderDirectives です。Native Image 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;
    }
}

幸いなことに、jni.h はすべての JDK の内部にあるため、java.home プロパティを使用して必要なヘッダーファイルを特定できます。実際のロジックは、もちろん、より堅牢で OS に依存しないようにすることができます。

Java で JVM ネイティブ メソッドを実装したり、Native Image で JVM にコールバックしたりすることは、与えられた例を拡張して native-image を呼び出すのと同じくらい簡単になるはずです。

お問い合わせください