Truffle 原生函數介面

Truffle 包含一種呼叫原生函數的方式,稱為原生函數介面 (NFI)。它實作為 Truffle 之上的內部語言,語言實作者可以透過標準的 polyglot eval 介面和 Truffle 互通性來存取。NFI 旨在用於例如實作語言的 FFI,或呼叫 Java 中不可用的原生執行階段常式。

NFI 使用 libffi。在標準 JVM 上,它使用 JNI 呼叫它,而在 GraalVM 原生映像檔上,它使用系統 Java。未來,它可能會由 Graal 編譯器在原生可執行檔中進行最佳化,以便直接從已編譯的程式碼進行原生呼叫。

穩定性 #

NFI 是一種專為語言實作者設計的內部語言。它被認為是不穩定的,介面和行為可能會在沒有警告的情況下變更。它不適合最終使用者直接使用。

基本概念 #

NFI 是透過您正在使用的任何語言的 polyglot 介面存取的。這可以是 Java,也可以是 Truffle 語言。這讓您可以使用 Java 語言實作程式碼或客體語言中的 NFI,以減少需要撰寫的 Java 程式碼量。

進入點是 polyglot eval 介面。這會執行特殊的 DSL,並傳回 Truffle 互通性物件,然後可以公開更多方法。

以下是一些使用 Ruby 的 polyglot 介面的範例,但也可以改用任何其他 JVM 或語言實作。

基本範例 #

以下是一個基本的工作範例,然後再深入探討細節

library = Polyglot.eval('nfi', 'load "libSDL2.dylib"')  # load a library
symbol = library['SDL_GetRevisionNumber']               # load a symbol from the library
signature = Polyglot.eval('nfi', '():UINT32')           # prepare a signature
function = signature.bind(symbol)                       # bind the symbol to the signature to create a function
puts function.call # => 12373                           # call the function

載入程式庫 #

若要載入程式庫,會評估以 'nfi' 語言 DSL 撰寫的指令碼。它會傳回代表已載入程式庫的物件。

library = Polyglot.eval('nfi', '...load command...')

載入命令可以是下列任何形式

  • 預設
  • load "filename"
  • load (flag | flag | ...) "filename"

default 命令會傳回一個偽程式庫,其中包含已載入程序中的所有符號,相當於 Posix 介面中的 RTLD_DEFAULT

load "filename" 命令會從檔案載入程式庫。您需要負責處理有關程式庫命名慣例和載入路徑的任何跨平台問題。

load (flag | flag | ...) "filename" 命令可讓您指定載入程式庫的旗標。對於預設後端 (後端將在稍後說明),以及在 Posix 平台上執行時,可用的旗標為 RTLD_GLOBALRTLD_LOCALRTLD_LAZYRTLD_NOW,它們具有傳統的 Posix 語意。如果未指定 RTLD_LAZYRTLD_NOW,則預設值為 RTLD_NOW

從程式庫載入符號 #

若要從程式庫載入符號,請將符號讀取為先前載入的程式庫物件的屬性。

symbol = library['symbol_name']

從符號產生原生函數物件 #

若要取得可用於叫用原生函數的可執行物件,請建立簽章物件並在其上呼叫 bind 方法,藉此 *繫結* 先前載入的符號物件。簽章物件需要與原生函數的實際類型簽章相符。

signature = Polyglot.eval('nfi', '...signature...')
function = signature.bind(symbol)

簽章的格式為 (arg, arg, ...) : return,其中 argreturn 是類型。

類型可以是下列其中一種簡單類型

  • VOID
  • UINT8
  • SINT8
  • UINT16
  • SINT16
  • UINT32
  • SINT32
  • UINT64
  • SINT64
  • FLOAT
  • DOUBLE
  • POINTER
  • STRING
  • OBJECT
  • ENV

陣列類型是透過將另一個類型放在方括號中來形成的。例如 [UINT8]。這些是 C 樣式的陣列。

函數指標類型是透過寫入巢狀簽章來形成的。例如,qsort 的簽章會是 (POINTER, UINT64, UINT64, (POINTER, POINTER) : SINT32) : VOID

對於具有可變引數的簽章的函數,您可以在可變引數開始的位置指定 ...,但之後您必須指定您將使用函數呼叫的實際類型。因此,您可能需要多次繫結相同的符號,以便使用不同的類型或不同數量的引數來呼叫它。例如,若要使用 %d %f 呼叫 printf,您會使用類型簽章 (STRING, ...SINT32, DOUBLE) : SINT32

類型運算式可以任意深度地巢狀。

另外兩個特殊類型 ENVOBJECT 會在本文件的稍後章節 (關於原生 API) 中說明。

類型可以用任何大小寫來書寫。

您需要負責將來自外來語言 (例如 C) 的類型對應到 NFI 類型。

呼叫原生函數物件 #

若要呼叫原生函數,請執行它。

return_value = function.call(...arguments...)

從原生程式碼回呼至受管理函數 #

使用巢狀簽章,函數呼叫可以取得函數指標作為引數。受管理呼叫端需要傳遞 Polyglot 可執行物件,該物件將轉換為原生函數指標。從原生端呼叫此函數指標時,會將 execute 訊息傳送至 Polyglot 物件。

void native_function(int32_t (*fn)(int32_t)) {
  printf("%d\n", fn(15));
}
signature = Polyglot.eval('nfi', '((SINT32):SINT32):VOID')
native_function = signature.bind(library['native_function'])
native_function.call(->(x) { x + 1 })

回呼函數的引數和傳回值會與一般函數呼叫的轉換方式相同,但轉換方向相反,亦即引數會從原生轉換為受管理,而傳回值會從受管理轉換為原生。

回呼函數指標本身可以具有函數指標引數。它的運作方式如您預期:函數接受原生函數指標作為引數,並將其轉換為 Truffle 可執行物件。將 execute 訊息傳送至該物件會呼叫原生函數指標,與呼叫一般 NFI 函數相同。

函數指標類型也支援作為傳回類型。

組合載入和繫結 #

您可以選擇性地將載入程式庫與載入符號和繫結它們結合。這是透過延伸的 load 命令來完成的,該命令會傳回一個具有已繫結函數作為方法的物件。

以下這兩個範例是等效的

library = Polyglot.eval('nfi', 'load libSDL2.dylib')
symbol = library['SDL_GetRevisionNumber']
signature = Polyglot.eval('nfi', '():UINT32')
function = signature.bind(symbol)
puts function.call # => 12373
library = Polyglot.eval('nfi', 'load libSDL2.dylib { SDL_GetRevisionNumber():UINT32; }')
puts library.SDL_GetRevisionNumber # => 12373

大括號 {} 中的定義可以包含多個函數繫結,以便可以一次從程式庫載入許多函數。

後端 #

載入命令可以用 with 作為前綴,以便選取特定的 NFI 後端。有多個 NFI 後端可用。預設值稱為 native,如果沒有 with 前綴或選取的後端不可用,則將使用它。

根據您正在執行的元件組態,可用的後端可能包括

  • native
  • llvm,它使用 GraalVM LLVM 執行階段來執行原生程式碼
  • panama

Panama 後端 #

Panama 後端使用 Panama 專案 所引入的外部函數和記憶體 API。此後端僅支援所有類型的一個子集。具體而言,它不支援 STRINGOBJECTENVFP80 或陣列類型。雖然功能不夠完整,但後端的效能通常較高。目前,從 JDK 21 開始可以使用 --enable-preview 標籤。

原生映像檔上的 Truffle NFI #

若要建置包含 Truffle NFI 的原生映像檔,只需使用 --language:nfi 引數,或在 native-image.properties 中指定 Requires = language:nfi 即可。可以使用 --language:nfi=<backend> 來選取要用於 native 後端的實作。

請注意,--language:nfi=<backend> 參數必須放在任何其他可能透過 Requires = language:nfi 將 NFI 作為依賴項引入的參數之前。第一個出現的 language:nfi 會決定要將哪個後端建置到原生映像檔中。

--language:nfi=<backend> 的可用參數為

  • libffi(預設值)
  • none

選擇 none 原生後端將會有效地停用使用 Truffle NFI 存取原生函式的功能。這將會破壞依賴原生存取 NFI 的使用者(例如 GraalVM LLVM Runtime,除非在 EE 上使用 --llvm.managed)。

原生 API #

NFI 可以與未修改、已編譯的原生程式碼一起使用,但也可以與原生程式碼使用的 Truffle 特定 API 一起使用。

特殊類型 ENV 會在簽章中新增一個額外的參數 TruffleEnv *env。額外的簡單類型 OBJECT 會轉換為不透明的 TruffleObject 類型。

trufflenfi.h 標頭檔提供了用於處理這些類型的宣告,然後原生程式碼可以透過 NFI 呼叫這些類型。如需此 API 的詳細說明,請參閱 trufflenfi.h

類型封送處理 #

本節詳細說明如何轉換函式簽章中所有類型的引數值和傳回值。

下表顯示 NFI 簽章中的可能類型,以及它們在原生端的對應 C 語言類型,以及這些引數在受管理端對應到的 polyglot 值

NFI 類型 C 語言類型 Polyglot 值
VOID void isNull == true 的 Polyglot 物件(僅在作為傳回類型時有效)。
SINT8/16/32/64 int8/16/32/64_t Polyglot isNumber,其 fitsIn... 對應的整數類型。
UINT8/16/32/64 uint8/16/32/64_t Polyglot isNumber,其 fitsIn... 對應的整數類型。
FLOAT float Polyglot isNumber,其 fitsInFloat
DOUBLE double Polyglot isNumber,其 fitsInDouble
POINTER void * isPointer == trueisNull == true 的 Polyglot 物件。
STRING char *(以零結尾的 UTF-8 字串) Polyglot isString
OBJECT TruffleObject 任意物件。
[type] type *(基本類型的陣列) Java 主機基本類型陣列。
(args):ret ret (*)(args)(函式指標類型) 具有 isExecutable == true 的 Polyglot 函式。
ENV TruffleEnv * 無(注入的引數)

以下章節詳細說明類型轉換。

函式指標的類型轉換行為可能會有些混淆,因為引數的方向是反轉的。如有疑問,請務必嘗試找出引數或傳回值的流動方向,是從受管理端到原生端,還是從原生端到受管理端。

VOID #

此類型僅允許作為傳回類型,用於表示不傳回值的函式。

由於在 Polyglot API 中,所有可執行物件都必須傳回一個值,因此從具有 VOID 傳回類型的原生函式傳回一個 isNull == true 的 Polyglot 物件。

具有 VOID 傳回類型的受管理回呼函式的傳回值將會被忽略。

基本數字 #

基本數字類型的轉換方式與您預期的相同。引數必須是 Polyglot 數字,且其值必須符合指定數字類型的值範圍。

需要注意的一件事是無符號整數類型的處理方式。即使 Polyglot API 沒有為符合無符號類型的值指定單獨的訊息,轉換仍然使用無符號的值範圍。例如,從原生端透過 SINT8 類型的傳回值傳遞給受管理端的值 0xFF 將會產生一個 Polyglot 數字 -1,其 fitsInByte。但是以 UINT8 傳回的相同值會產生 Polyglot 數字 255,其 fitsInByte

當從受管理程式碼將數字傳遞到原生程式碼時,將會忽略數字的帶符號性,只有數字的位元才是相關的。因此,舉例來說,將 -1 傳遞給 UINT8 類型的引數是被允許的,且原生端的結果是 255,因為它與 -1 具有相同的位元。反過來說,將 255 傳遞給 SINT8 類型的引數也是被允許的,且原生端的結果是 -1

由於目前的 Polyglot API 無法表示有符號 64 位元範圍之外的數字,因此 UINT64 類型目前是以帶符號語意來處理。這是 API 中的已知錯誤,將在未來的版本中變更。

POINTER #

此類型是泛型指標引數。在原生端,引數的確切指標類型並不重要。

如果可能,傳遞給 POINTER 引數的 polyglot 物件將會轉換為原生指標(必要時使用 isPointerasPointertoNative 訊息)。isNull == true 的物件將會以原生 NULL 傳遞。

POINTER 傳回值將會產生一個 isPointer == true 的 polyglot 物件。原生 NULL 指標也會有 isNull == true

STRING #

這是一個指標類型,具有針對字串的特殊轉換語意。

從受管理端使用 STRING 類型傳遞到原生端的 Polyglot 字串將會轉換為以零結尾的 UTF-8 編碼字串。對於 STRING 引數,指標由呼叫者擁有,且保證僅在呼叫期間保持存活。從受管理函式指標傳回給原生呼叫者的 STRING 值也由呼叫者擁有。它們在使用後必須以 free 釋放。

Polyglot 指標值或 null 值也可以傳遞給 STRING 引數。語意與 POINTER 引數相同。使用者有責任確保指標是有效的 UTF-8 字串。

從原生函式傳遞到受管理程式碼的 STRING 值行為類似於 POINTER 傳回值,但此外它們還具有 isString == true。使用者有責任擁有指標,且可能需要 free 傳回值,具體取決於被呼叫的原生函式的語意。釋放傳回的指標後,傳回的 polyglot 字串會失效,且讀取它會導致未定義的行為。從這個意義上來說,傳回的 polyglot 字串不是一個安全的物件,類似於原始指標。建議 NFI 的使用者在將傳回的字串傳遞給不受信任的受管理程式碼之前先複製它。

OBJECT #

此引數對應於 C 類型 TruffleObject。此類型定義於 trufflenfi.h 中,且是不透明的指標類型。TruffleObject 類型的值表示對任意受管理物件的參考。

原生程式碼無法對 TruffleObject 類型的值執行任何操作,除了將它們傳回給受管理程式碼,無論是透過傳回值還是將它們傳遞給回呼函式指標。

TruffleObject 參考的生命週期需要手動管理。如需用於管理 TruffleObject 參考生命週期的 API 函式,請參閱 trufflenfi.h 中的文件。

作為引數傳遞的 TruffleObject 由呼叫者擁有,且保證僅在呼叫期間保持存活。從回呼函式指標傳回的 TruffleObject 參考由呼叫者擁有,且在使用後需要釋放。從原生函式傳回 TruffleObject 不會轉移所有權(但 trufflenfi.h 中有一個 API 函式可以執行此操作)。

[...](原生基本類型陣列) #

此類型僅允許作為從受管理程式碼到原生函式的引數,且僅支援基本數字類型的陣列。

在受管理端,僅支援包含 Java 基本類型陣列的 Java 主機物件。在原生端,該類型是指向陣列內容的指標。使用者有責任將陣列長度作為單獨的引數傳遞。

該指標僅在原生呼叫期間有效。

在從呼叫傳回後,對內容的修改會傳播回 Java 陣列。在原生呼叫期間同時存取 Java 陣列的效果是不明確的。

(...):...(函式指標) #

在原生端,巢狀簽章類型對應於具有給定簽章的函式指標,該指標會回呼到受管理程式碼。

從受管理端使用函式指標類型傳遞到原生端的 Polyglot 可執行物件會轉換為可由原生程式碼呼叫的函式指標。對於函式指標引數,函式指標由呼叫者擁有,且保證僅在呼叫期間保持存活。函式指標傳回值由呼叫者擁有,且必須手動釋放。如需用於管理函式指標值生命週期的 API 函式,請參閱 polyglot.h

Polyglot 指標值或 null 值也可以傳遞給函式指標引數。語意與 POINTER 引數相同。使用者有責任確保指標是有效的函式指標。

函式指標傳回類型與一般的 POINTER 傳回類型相同,但此外,它們已繫結到給定的簽章類型。它們支援 execute 訊息,且行為與一般的 NFI 函式相同。

ENV #

此類型是 TruffleEnv * 類型的特殊引數。它僅作為引數類型有效,而不是作為傳回類型。它是在原生端注入的引數,在受管理端沒有對應的引數。

當用作原生函式的引數類型時,原生函式將在此位置取得環境指標。該環境指標可以用於呼叫 API 函式(請參閱 trufflenfi.h)。該引數是注入的,例如,如果簽章是 (SINT32, ENV, SINT32):VOID。這個函式物件預期會以兩個整數引數呼叫,且會以三個引數呼叫對應的原生函式:首先是第一個實際引數,然後是注入的 ENV 引數,然後是第二個實際引數。

ENV 類型用作函式指標參數的引數類型時,必須使用有效的 NFI 環境作為引數呼叫該函式指標。如果呼叫者已經有環境,則將其傳遞到回呼函式指標比在沒有 ENV 引數的情況下呼叫它們更有效。

呼叫慣例 #

原生函式必須使用系統的標準 ABI。目前不支援其他 ABI。

與我們聯繫