JNI 呼叫 API

Native Image 可用於在 Java 中實作低階系統操作,並透過 JNI 呼叫 API 提供給在標準 JVM 上執行的 Java 程式碼使用。如此一來,可以使用相同的語言來編寫應用程式邏輯以及系統呼叫。

請注意,此文件描述的是與 JNI 通常做法相反的情況:通常低階系統操作是在 C 中實作,並使用 JNI 從 Java 呼叫。如果您有興趣了解 Native Image 如何支援常見的使用案例,請參閱Native Image 中的 Java 原生介面 (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 的慣例,稍後會很方便) 和 Native Image 簽章 (通常用於 JNI 方法)。第一個參數是指向 JNIEnv* 值的參考。第二個參數是指向宣告該方法的類別的 jclass 值的參考。第三個參數是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) 識別碼。其餘的引數會與進入點的參數相符。

載入原生程式庫 #

下一步是將 JDK 與產生的 .so 程式庫繫結。例如,請確保載入原生 Native.add 方法的實作。簡單的 loadloadLibrary 呼叫即可

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 執行時間,並傳回一個可攜式的識別 (例如,long),以保留Native Image 隔離執行緒的執行個體。然後,隔離執行緒可用於多次呼叫您程式碼的原生部分

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

從原生 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(...) 等。此外,謎題中還有另一個重要的部分遺失 - jvalue 聯集型別包裝了所有可能的 Java 基本和物件型別。以下是其 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 方法是一個特殊的 Native Image 建構,用於執行 C 指標算術。給定一個指標,可以將其視為陣列的初始元素,然後例如,使用 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 保留的陣列中,以八個 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。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 屬性來尋找必要的標頭檔。當然,實際的邏輯可以變得更加穩健且與作業系統無關。

現在,以 Java 實作任何 JVM 原生方法和/或使用 Native Image 回呼 JVM 應該像擴充給定的範例和呼叫 native-image 一樣容易。

與我們聯繫