- 適用於 JDK 23 的 GraalVM (最新)
- 適用於 JDK 24 的 GraalVM (搶先體驗)
- 適用於 JDK 21 的 GraalVM
- 適用於 JDK 17 的 GraalVM
- 封存
- 開發版本
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
方法的實作。簡單的 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 執行時間,並傳回一個可攜式的識別 (例如,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 指標的型別安全表示法。JClass
、JMethodID
和 JObject
都是指標。由於上述定義,您現在有了 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
一樣容易。