Native Image 中的 Java 原生介面 (JNI)

Java 原生介面 (JNI) 是一種原生 API,可讓 Java 程式碼與原生程式碼互動,反之亦然。本頁概述 Native Image 中的 JNI 實作。

預設會啟用 JNI 支援,並內建於 Native Image 中。應透過 JNI 存取的個別類別、方法和欄位,必須在映像檔建置時於組態檔中指定 (請參閱下方)。

Java 程式碼可以使用 System.loadLibrary() 從共用物件載入原生程式碼。或者,原生程式碼可以使用 Invocation API 載入 JVM 的原生程式庫並附加到其 Java 環境。Native Image JNI 實作支援這兩種方法。

目錄 #

載入原生程式庫 #

使用 System.loadLibrary() (及相關 API) 載入原生程式庫時,Native Image 會先搜尋包含 Native Image 的目錄,然後再搜尋 Java 程式庫路徑。因此,只要要載入的原生程式庫與 Native Image 位於同一目錄中,就不需要其他設定。

反映中繼資料 #

JNI 支援依名稱查詢類別,以及依名稱和簽章查詢方法和欄位。這需要保留這些查詢所需的必要中繼資料。native-image 建置器必須事先知道將查詢哪些項目,以防它們無法以其他方式存取,因此不會包含在 Native Image 中。此外,native-image 必須為任何可透過 JNI 呼叫的方法預先產生包裝函式程式碼。因此,指定需要透過 JNI 存取的項目簡潔清單,可確保其可用性並減少佔用空間。此類清單應在 reachability-metadata.json 檔案中指定。

可以使用 GraalVM JDK 的 追蹤代理程式自動收集 JNI 設定。代理程式會追蹤一般 Java VM 上應用程式執行期間動態功能的所有用法。當應用程式完成且 JVM 結束時,代理程式會將組態寫入指定輸出目錄中的 JSON 檔案。如果您將產生的組態檔從該輸出目錄移動到類別路徑上的 META-INF/native-image/,則會在建置時自動使用這些檔案。native-image 建置器會搜尋 META-INF/native-image/ 及其子目錄中名為 reachability-metadata.json 的檔案,或舊版檔案 (例如 reflect-config.json 等)。

或者,自訂 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=<JNIRegistrationFeature 類別的完整名稱> 傳遞給 native-image 建置器。Native Image 建置組態說明如何使用 META-INF/native-image 中的 native-image.properties 檔案自動執行此操作。

java.lang.reflect 支援 #

JNI 函數 FromReflectedMethodToReflectedMethod 可用於取得 java.lang.reflect.Methodjava.lang.reflect.Constructor 物件的對應 jmethodID,反之亦然。函數 FromReflectedFieldToReflectedField 會在 jfieldIDjava.lang.reflect.Field 之間轉換。若要使用這些函數,必須啟用反映支援,且相關的方法和欄位必須包含在反映中繼資料中。

物件控制代碼 #

JNI 不允許直接存取 Java 物件。JNI 提供字組大小的物件控制代碼,這些控制代碼可以傳遞給 JNI 函數以間接存取物件。本機控制代碼僅在原生呼叫期間有效,且僅在呼叫者的執行緒中有效,而全域控制代碼在執行緒之間有效,且在明確終止之前保持有效。控制代碼 0 代表 NULL

Native Image 使用執行緒本機、不斷增長的參考物件陣列實作本機控制代碼,其中陣列中的索引是控制代碼值。「指針」指向將配置下一個控制代碼的位置。原生呼叫可以巢狀配置,因此在叫用原生方法之前,呼叫 Stub 會將目前的指針推送至堆疊,並在它傳回之後,從堆疊還原舊的指針,並將陣列中呼叫的所有物件參考設為 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 建置器也會在原生宣告方法的反映中繼資料中建立額外的連結物件。當呼叫原生方法時,呼叫包裝函式會在所有已載入的程式庫中查詢相符的符號,並將已解析的位址儲存在連結物件中以供未來呼叫。或者,Native Image 也支援 RegisterNatives JNI 函數,以便明確提供原生方法的程式碼位址,而不是需要符合 JNI 名稱轉換配置的符號。

原生對 Java 方法呼叫 #

原生程式碼可以透過先取得目標方法的 jmethodID,然後使用其中一個 Call<Type>MethodCallStatic<Type>MethodCallNonvirtual<Type>Method 函數來叫用 Java 方法。這些 Call... 函數也各有 Call...MethodACall...MethodV 變體,這些變體會將引數作為陣列或 va_list,而不是可變引數。例如

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 程式碼互動。Native Image 使用 @CEntryPoint 實作這些函數,例如

@CEntryPoint(...) private static void DeleteGlobalRef(JNIEnvironment env, JNIObjectHandle globalRef) { /* setup; */ JNIGlobalHandles.singleton().delete(globalRef); }

JNI 指定這些函數是透過 C 結構中的函數指標提供,該結構可透過 JNIEnv* 引數存取。此結構的自動初始化會在映像檔建置期間準備。

物件建立 #

JNI 提供兩種建立 Java 物件的方法,一種是呼叫 AllocObject 以配置記憶體,然後使用 CallVoidMethod 叫用建構函式,另一種是使用 NewObject 以單一步驟建立和初始化物件 (或變體 NewObjectANewObjectV)。例如

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

Native Image 支援這兩種方法。建構函式必須包含在 JNI 設定中,且方法名稱為 <init>。不為 NewObject 產生額外的呼叫包裝函式,而是重複使用一般的 CallVoidMethod 包裝函式,並偵測何時透過 NewObject 呼叫,因為會將目標類別的 Class 物件傳遞給它。在這種情況下,呼叫包裝函式會在叫用實際建構函式之前配置新的執行個體。

欄位存取 #

原生程式碼可以透過取得其 jfieldID,然後使用其中一個 Get<Type>FieldSet<Type>FieldGetStatic<Type>FieldSetStatic<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 存取的欄位,其在物件內 (或靜態欄位區域內) 的偏移會儲存在反映中繼資料中,並用作其 jfieldIDnative-image 建置器會為所有基本類型和物件欄位的欄位產生存取子方法。這些存取子方法會執行到 Java 程式碼的轉換和返回,並使用不安全的載入或儲存來直接操作欄位值。由於分析無法觀察透過 JNI 指派的物件欄位,因此它假設可在可透過 JNI 存取的欄位中發生欄位宣告類型的任何子類型。

JNI 也允許寫入宣告為 final 的欄位,必須使用組態檔中的 allowWrite 屬性為個別欄位啟用此功能。但是,由於最佳化,存取 final 欄位的程式碼可能無法以與非 final 欄位相同的方式觀察 final 欄位值的變更。

例外 #

JNI 規範指出,原生程式碼呼叫 Java 程式碼所產生的例外狀況必須被捕捉並保留。在 Native Image 中,這會在原生程式碼到 Java 程式碼的呼叫包裝器以及 JNI 函數的實作中完成。然後,原生程式碼可以使用 ExceptionCheckExceptionOccurredExceptionDescribeExceptionClear 函數來查詢並清除待處理的例外狀況。原生程式碼也可以使用 ThrowThrowNewFatalError 拋出例外狀況。當例外狀況在原生程式碼中未被處理,或原生程式碼本身拋出例外狀況時,Java 到原生程式碼的呼叫包裝器會在重新進入 Java 程式碼時重新拋出該例外狀況。

監控器 #

JNI 宣告了 MonitorEnterMonitorExit 函數來獲取和釋放物件的固有鎖。Native Image 提供了這些函數的實作。然而,native-image 建構器僅直接將固有鎖指派給分析可以觀察到在 Java synchronized 語句中使用,以及與 wait()notify()notifyAll() 一起使用的類別物件。對於其他物件,同步會退回到較慢的機制,該機制使用映射來儲存鎖關聯,而這本身也需要同步。因此,將同步操作包裝在 Java 程式碼中可能會有所幫助。

與我們聯繫