- 適用於 JDK 23 的 GraalVM (最新)
- 適用於 JDK 24 的 GraalVM (搶先體驗)
- 適用於 JDK 21 的 GraalVM
- 適用於 JDK 17 的 GraalVM
- 封存
- 開發版本
- Truffle 語言實作框架
- Truffle 分支檢測
- 動態物件模型
- 靜態物件模型
- 針對直譯器程式碼的 Host 優化
- Truffle 函式內嵌方法
- 分析 Truffle 直譯器
- Truffle 互操作性 2.0
- 語言實作
- 使用 Truffle 實作新的語言
- Truffle 語言和工具移轉至 Java 模組
- Truffle 原生函式介面
- 優化 Truffle 直譯器
- 選項
- 堆疊替換
- Truffle 字串指南
- 特殊化直方圖
- 測試 DSL 特殊化
- 基於 Polyglot API 的 TCK
- Truffle 編譯佇列方法
- Truffle 程式庫指南
- Truffle AOT 概觀
- Truffle AOT 編譯
- 輔助引擎快取
- Truffle 語言安全點教學課程
- 單態化
- 分割演算法
- 單態化使用案例
- 向執行階段報告多型特殊化
Truffle 互操作性 2.0
此文件目標讀者為客語和工具實作者。建議先閱讀Truffle 程式庫教學課程,再繼續閱讀。
動機 #
在 Truffle 1.0 RC15 版本中,引入了一個名為「Truffle 程式庫」的新 API。Truffle 程式庫允許使用者使用具有分析/快取支援的多型。透過 Interop 2.0,計劃將 Truffle 程式庫用於互操作性協定。目前的互操作性 API 已經成熟且經過良好測試,並已被語言和工具採用。
以下列出變更目前互操作性 API 並引入 Interop 2.0 的原因
- 佔用空間:在目前的互操作性 API 中,每次傳送訊息都會透過
CallTarget
,並且引數會被封裝成Object[]
。這使得目前的互操作性對於直譯器呼叫效率不高,並且需要額外的記憶體。Truffle 程式庫使用簡單的節點和類型專用的呼叫簽章,不需要引數陣列封裝或呼叫目標。 - 未快取的分派:無法從慢速路徑執行目前的互操作性訊息而不配置臨時節點。Truffle 程式庫會自動產生每個匯出訊息的未快取版本。這允許在慢速路徑/執行階段中使用互操作性訊息,而無需配置任何臨時資料結構。
- 重複使用多個訊息的分派:在目前的互操作性中,傳送每個訊息都會重複對匯出訊息的分派。如果需要傳送多個訊息,並且接收者類型變成多型,則會產生不良的程式碼。互操作性程式庫實例可以針對輸入值進行特殊化。這允許使用者執行一次分派,並在不重複分派的情況下調用多個訊息。這會在多型案例中產生更有效率的程式碼。
- 支援預設實作:目前的互操作性只能用於
TruffleObject
的實作。Truffle 程式庫可以用於任何接收者類型。例如,可以在基本數字上調用 isExecutable 訊息,它只會傳回false
。 - 容易出錯:訊息解析有一些常見問題,Truffle 程式庫會嘗試避免這些問題,例如混淆接收者類型或實作錯誤的類型檢查。Truffle 程式庫的新斷言功能允許指定訊息特定的斷言,以驗證不變量、前置條件和後置條件。
- 文件中的冗餘:目前的互操作性會在
Message
常數和ForeignAccess
靜態存取方法中記錄訊息。這導致大部分的冗餘文件。透過 Truffle 互操作性,只有一個地方可以找到文件,那就是程式庫類別中的實例方法。 - 通用性:Truffle 程式庫可以用於語言表示抽象,因為它現在在記憶體消耗和直譯器效能方面已足夠有效率。目前的互操作性 API 因為這個問題而無法以這種方式實際使用。
- 解決協定問題:目前的互操作性 API 有一些設計問題,Interop 2.0 嘗試解決這些問題 (請參閱稍後)。
相容性 #
從互操作性 1.0 到 2.0 的變更是以相容的方式完成的。因此,舊的互操作性應該會繼續運作,並且可以逐步採用。這表示如果一個語言仍然使用舊的互操作性 API 進行呼叫,而另一個語言已經採用了新的互操作性 API,則相容性橋接器會對應這些 API。如果您對它的運作方式感到好奇,請尋找 DefaultTruffleObjectExports
類別,以了解對舊互操作性的新互操作性呼叫。以及 LegacyToLibraryNode
以了解對新互操作性的舊互操作性呼叫。請注意,使用相容性橋接器可能會導致效能降低。這就是為什麼語言應該盡早移轉的原因。
互操作性協定變更 #
Interop 2.0 帶來許多協定變更。本節旨在為這些變更提供基本原理。如需完整詳細的參考文件,請參閱 InteropLibrary Javadoc。注意:每個已淘汰的 API 都會在以 @deprecated
標記的 Javadoc 中描述其移轉路徑。
使用明確類型取代 IS_BOXED 和 UNBOX #
IS_BOXED/UNBOX 設計有一些問題
- 為了找出值是否為特定類型 (例如字串),必須先將值取消封裝。取消封裝可能是一個昂貴的操作,導致程式碼效率不佳,而只是為了檢查值的類型。
- 舊的 API 無法用於未實作
TruffleObject
的值。因此,基本數字的處理需要與 TruffleObject 案例分開,使得 UNBOX 設計有必要重複使用現有的程式碼。Truffle 程式庫支援基本接收者類型。 - UNBOX 的設計依賴於它傳回的特定基本類型集合。很難以這種方式引入其他新的互操作性類型,因為語言會直接參考基本類型。
在 InteropLibrary
中引入了以下新訊息作為替代方案
boolean isBoolean(Object)
boolean asBoolean(Object)
boolean isString(Object)
String asString(Object)
boolean isNumber(Object)
boolean fitsInByte(Object)
boolean fitsInShort(Object)
boolean fitsInInt(Object)
boolean fitsInLong(Object)
boolean fitsInFloat(Object)
boolean fitsInDouble(Object)
byte asByte(Object)
short asShort(Object)
int asInt(Object)
long asLong(Object)
float asFloat(Object)
double asDouble(Object)
InteropLibrary
為接收者類型 Boolean
、Byte
、Short
、Integer
、Long
、Float
、Double
、Character
和 String
指定預設實作。此設計可以擴展以支援新的值 (例如大數或自訂字串抽象),因為 Java 基本類型不再直接使用。不再建議直接在特殊化中使用基本類型,因為互操作性基本類型的集合將來可能會變更。相反,請務必使用互操作性程式庫檢查特定類型,例如,使用 fitsInInt
而不是 instanceof Integer
。
透過使用新訊息,可以模擬原始的 UNBOX 訊息,如下所示
@Specialization(limit="5")
Object doUnbox(Object value, @CachedLibrary("value") InteropLibrary interop) {
if (interop.isBoolean(value)) {
return interop.asBoolean(value);
} else if (interop.isString(value)) {
return interop.asString(value);
} else if (interop.isNumber(value)) {
if (interop.fitsInByte(value)) {
return interop.asByte(value);
} else if (interop.fitsInShort(value)) {
return interop.asShort(value);
} else if (interop.fitsInInt(value)) {
return interop.asInt(value);
} else if (interop.fitsInLong(value)) {
return interop.asLong(value);
} else if (interop.fitsInFloat(value)) {
return interop.asFloat(value);
} else if (interop.fitsInDouble(value)) {
return interop.asDouble(value);
}
}
throw UnsupportedMessageException.create();
}
注意:不建議像這樣取消封裝所有基本類型。相反,語言只應取消封裝為它實際使用的基本類型。理想情況下,不需要取消封裝作業,並且直接使用互操作性程式庫來實作作業,如下所示
@Specialization(guards = {
"leftValues.fitsInLong(l)",
"rightValues.fitsInLong(r)"}, limit="5")
long doAdd(Object l, Object r,
@CachedLibrary("l") InteropLibrary leftValues,
@CachedLibrary("r") InteropLibrary rightValues) {
return leftValues.asLong(l) + rightValues.asLong(r);
}
陣列和成員元素的明確命名空間 #
通用 READ 和 WRITE 訊息最初的設計主要是考量 JavaScript 的使用案例。隨著更多語言採用互操作性,很明顯需要用於陣列和物件成員的明確命名空間。隨著時間的推移,當數字使用 READ 和 WRITE 時,其解讀會變更為表示陣列存取,當字串使用 READ 和 WRITE 時,則會變更為表示物件成員存取。HAS_SIZE 訊息會被重新解讀為數值是否包含具有額外保證的陣列元素,例如,陣列元素是否可在索引 0 和大小之間進行反覆運算。
為了改善語言之間的互操作性,需要明確的雜湊/對應/字典項目命名空間。最初,它的目的是重複使用此處的通用 READ/WRITE 命名空間。對於 JavaScript,這是可行的,因為字典和成員命名空間是相等的。但是,大多數語言會將對應項目與物件成員分開,這會導致不明確的索引鍵。來源語言 (協定實作者) 無法知道如何解決此衝突。相反,透過擁有明確的命名空間,我們可以讓目標語言 (協定呼叫者) 決定如何解決不明確性。例如,現在可以在目標語言作業中決定字典或成員元素是否應優先。
變更了以下互操作性訊息
READ, WRITE, REMOVE, HAS_SIZE, GET_SIZE, HAS_KEYS, KEYS
具有個別成員和陣列命名空間的更新協定在 InteropLibrary 中的樣子如下
物件命名空間
hasMembers(Object)
getMembers(Object, boolean)
readMember(Object, String)
writeMember(Object, String, Object)
removeMember(Object, String)
invokeMember(Object, String, Object...)
陣列命名空間
hasArrayElements(Object)
readArrayElement(Object, long)
getArraySize(Object)
writeArrayElement(Object, long, Object)
removeArrayElement(Object, long)
陣列存取訊息不再擲回 UnknownIdentifierException
;而是擲回 InvalidArrayIndexException
。這是原始設計中的錯誤,其中存取的數字需要轉換為 UnknownIdentifierException
中的識別碼字串。
使用個別訊息取代 KeyInfo #
在上一節中,我們沒有提到 KEY_INFO 訊息。KEY_INFO 訊息可用於查詢成員或陣列元素的所有屬性。雖然這是一個方便的小型 API,但它通常效率不高,因為它要求實作者傳回所有索引鍵資訊屬性。同時,呼叫者很少真的需要所有索引鍵資訊屬性。透過 Interop 2.0,我們移除了 KEY_INFO 訊息。相反,我們為每個命名空間引入了明確的訊息,以解決這個問題。
物件命名空間
isMemberReadable(Object, String)
isMemberModifiable(Object, String)
isMemberInsertable(Object, String)
isMemberRemovable(Object, String)
isMemberInvocable(Object, String)
isMemberInternal(Object, String)
isMemberWritable(Object, String)
isMemberExisting(Object, String)
hasMemberReadSideEffects(Object, String)
hasMemberWriteSideEffects(Object, String)
陣列命名空間
isArrayElementReadable(Object, long)
isArrayElementModifiable(Object, long)
isArrayElementInsertable(Object, long)
isArrayElementRemovable(Object, long)
isArrayElementWritable(Object, long)
isArrayElementExisting(Object, long)
注意:陣列命名空間不再支援查詢讀取或寫入副作用。這些訊息可能會重新引入,但目前沒有使用案例。此外,陣列命名空間不允許調用。
移除 TO_NATIVE 的傳回類型 #
TO_NATIVE 訊息在 InteropLibrary 中重新命名為 toNative,不同之處在於它不再傳回值,而是執行原生轉換作為副作用 (如果接收者支援)。這允許訊息的呼叫者簡化其程式碼。未找到任何需要傳回不同值的 toNative
轉換案例。toNative
的預設行為已變更為不傳回任何值。
次要變更 #
以下訊息大多未變更。NEW
訊息已重新命名為 instantiate
,以與 isInstantiable
一致。
Message.IS_NULL -> InteropLibrary.isNull
Message.EXECUTE -> InteropLibrary.execute
Message.IS_INSTANTIABLE -> InteropLibrary.isInstantiable
Message.NEW -> InteropLibrary.instantiate
Message.IS_EXECUTABLE -> InteropLibrary.isExecutable
Message.EXECUTE -> InteropLibrary.execute
Message.IS_POINTER -> InteropLibrary.isPointer
Message.AS_POINTER -> InteropLibrary.asPointer
更強大的斷言 #
作為遷移的一部分,引入了許多新的斷言。具體的 pre-post 和不變條件在 Javadoc 中描述。與舊的 interop 節點不同,快取程式庫只有在作為 AST 的一部分採用時才能使用。
不再有未檢查/已檢查的例外 #
在 Interop 2.0 中,InteropException.raise
已被棄用。雖然可行,但將已檢查的例外重新拋出為未檢查的例外被認為是一種反模式。使用 Truffle Libraries 時,目標語言節點會直接插入到呼叫者的 AST 中,因此不再存在不支援已檢查例外的 CallTarget
限制。再加上 Truffle DSL 對已檢查例外的額外支援,應該不再需要使用 raise 方法。相反,為所有 interop 例外類型引入了一個新的 create 工廠方法。
計畫從 interop 例外中移除堆疊追蹤,以提高其效率,因為 interop 例外旨在始終立即捕獲,而不會重新拋出。這已延遲到可以移除相容性層為止。
遷移 #
由於使用了 Truffle Libraries 來進行 interop,大多數現有的 interop API 都必須棄用。以下 Interop 1.0 與 Interop 2.0 的比較旨在幫助遷移現有的 interop 用法。
快速路徑發送 Interop 訊息 #
這是在操作節點中嵌入的發送 interop 訊息的快速路徑方式。這是發送 interop 訊息最常見的方式。
Interop 1.0
@ImportStatic({Message.class, ForeignAccess.class})
abstract static class ForeignExecuteNode extends Node {
abstract Object execute(Object function, Object[] arguments);
@Specialization(guards = "sendIsExecutable(isExecutableNode, function)")
Object doDefault(TruffleObject function, Object[] arguments,
@Cached("IS_EXECUTABLE.createNode()") Node isExecutableNode,
@Cached("EXECUTE.createNode()") Node executeNode) {
try {
return ForeignAccess.sendExecute(executeNode, function, arguments);
} catch (UnsupportedTypeException | ArityException | UnsupportedMessageException e) {
// ... convert errors to guest language errors ...
}
}
}
Interop 2.0
abstract static class ForeignExecuteNode extends Node {
abstract Object execute(Object function, Object[] arguments);
@Specialization(guards = "functions.isExecutable(function)", limit = "2")
Object doDefault(Object function, Object[] arguments,
@CachedLibrary("function") InteropLibrary functions) {
try {
return functions.execute(function, arguments);
} catch (UnsupportedTypeException | ArityException | UnsupportedMessageException e) {
// ... convert errors to guest language errors ...
}
}
}
請注意以下差異
- 要調用訊息,我們調用
TruffleLibrary
上的實例方法,而不是調用ForeignAccess
上的靜態方法。 - 舊的 interop 需要為每個操作建立一個節點。在新版本中,只會建立一個專用的 interop 程式庫。
- 在舊的 API 中,我們需要為
TruffleObject
專門化接收者類型。新的 interop 程式庫可以使用任何 interop 值調用。預設情況下,對於不匯出 interop 程式庫的值,isExecutable
將返回false
。例如,現在可以使用裝箱的基本接收者值來調用程式庫。 - 在新的 interop 中,我們使用
@CachedLibrary
而不是在舊的 interop 中使用@Cached
。 - 新的
@CachedLibrary
注釋指定程式庫專門化的值。這允許 DSL 將程式庫實例專門化為該值。這又允許針對所有訊息調用執行一次接收者值的調度。在舊的 interop 版本中,節點無法專門化為值。因此,需要為每個 interop 訊息發送重複調度。 - 專用的程式庫實例需要為專門化方法指定一個
limit
。如果此限制溢位,將使用不執行任何分析/快取的程式庫的未快取版本。舊的 interop API 假設每個 interop 節點的專門化限制常數為8
。 - 新的 interop API 允許通過指定
@CachedLibrary(limit="2")
來使用程式庫的調度版本。這允許 interop 程式庫與任何值一起使用,但缺點是對於每個訊息調用複製內聯快取,就像舊的 interop API 一樣。因此,建議盡可能使用專用的程式庫。
慢速路徑發送 Interop 訊息 #
有時需要在沒有節點上下文的情況下從執行時間調用 interop 訊息。
Interop 1.0
ForeignAccess.sendRead(Message.READ.createNode(), object, "property")
Interop 2.0
InteropLibrary.getFactory().getUncached().read(object, "property");
請注意以下差異
- 舊的介面為每次調用分配一個節點。
- 新的程式庫使用程式庫的未快取版本,該版本不需要為每次調用進行任何分配或裝箱。
- 通過
InteropLibrary.getFactory().getUncached(object)
,可以查找程式庫的未快取和專門化版本。如果需要將多個未快取的 interop 訊息發送到同一個接收者,則可以使用此方法來避免重複的匯出查找。
自訂快速路徑發送 Interop 訊息 #
有時無法使用 Truffle DSL,並且需要手動編寫節點。這兩個 API 都允許你這樣做。
Interop 1.0
final class ForeignExecuteNode extends Node {
@Child private Node isExecutableNode = Message.IS_EXECUTABLE.createNode();
@Child private Node executeNode = Message.EXECUTE.createNode();
Object execute(Object function, Object[] arguments) {
if (function instanceof TruffleObject) {
TruffleObject tFunction = (TruffleObject) function;
if (ForeignAccess.sendIsExecutable(isExecutableNode, tFunction)) {
try {
return ForeignAccess.sendExecute(executeNode, tFunction, arguments);
} catch (UnsupportedTypeException | ArityException | UnsupportedMessageException e) {
// TODO handle errors
}
}
}
// throw user error
}
}
Interop 2.0
static final class ForeignExecuteNode extends Node {
@Child private InteropLibrary functions = InteropLibrary.getFactory().createDispatched(5);
Object execute(Object function, Object[] arguments) {
if (functions.isExecutable(function)) {
try {
return functions.execute(function, arguments);
} catch (UnsupportedTypeException | ArityException | UnsupportedMessageException e) {
// handle errors
return null;
}
}
// throw user error
}
}
請注意以下差異
- 新的 interop 通過
LibraryFactory<InteropLibrary>
建立節點,該實例可以通過InteropLibrary.getFactory()
訪問。舊的 interop 通過Message
實例建立調度節點。 - 可以為新的 interop 程式庫指定調度限制。舊的 interop API 始終假設常數限制為
8
。 - 對於新的 interop,我們不需要檢查類型
TruffleObject
,因為 Truffle Libraries 可以與任何接收者類型一起使用。對於非函式值,isExecutable
只會返回false
。
實作/匯出 Interop 訊息 #
要實作/匯出 interop 程式庫訊息,請參閱以下範例
Interop 1.0
@MessageResolution(receiverType = KeysArray.class)
final class KeysArray implements TruffleObject {
private final String[] keys;
KeysArray(String[] keys) {
this.keys = keys;
}
@Resolve(message = "HAS_SIZE")
abstract static class HasSize extends Node {
public Object access(KeysArray receiver) {
return true;
}
}
@Resolve(message = "GET_SIZE")
abstract static class GetSize extends Node {
public Object access(KeysArray receiver) {
return receiver.keys.length;
}
}
@Resolve(message = "READ")
abstract static class Read extends Node {
public Object access(KeysArray receiver, int index) {
try {
return receiver.keys[index];
} catch (IndexOutOfBoundsException e) {
CompilerDirectives.transferToInterpreter();
throw UnknownIdentifierException.raise(String.valueOf(index));
}
}
}
@Override
public ForeignAccess getForeignAccess() {
return KeysArrayForeign.ACCESS;
}
static boolean isInstance(TruffleObject array) {
return array instanceof KeysArray;
}
}
Interop 2.0
@ExportLibrary(InteropLibrary.class)
final class KeysArray implements TruffleObject {
private final String[] keys;
KeysArray(String[] keys) {
this.keys = keys;
}
@ExportMessage
boolean hasArrayElements() {
return true;
}
@ExportMessage
boolean isArrayElementReadable(long index) {
return index >= 0 && index < keys.length;
}
@ExportMessage
long getArraySize() {
return keys.length;
}
@ExportMessage
Object readArrayElement(long index) throws InvalidArrayIndexException {
if (!isArrayElementReadable(index) {
throw InvalidArrayIndexException.create(index);
}
return keys[(int) index];
}
}
請注意以下差異
- 我們使用 @ExportLibrary 而不是 @MessageResolution。
- 兩個版本都需要實作 TruffleObject。新的 interop API 僅需要 TruffleObject 類型以實現相容性。
- 使用 @ExportMessage 注釋而不是 @Resolve。後一個注釋可以從方法名稱推斷訊息的名稱。如果方法名稱不明確,例如匯出了多個程式庫時,則可以明確指定名稱和程式庫。
- 無需為匯出/解析指定類別。但是,如果匯出需要多個專門化,仍然可以這樣做。有關詳細資訊,請參閱 Truffle Library 教學。
- 例外現在作為已檢查的例外拋出。
- 不再需要實作 getForeignAccess()。該實作會自動發現接收者類型的實作。
- 不再需要實作
isInstance
。該實作現在從類別簽名派生。請注意,如果接收者類型宣告為 final,則檢查可能更有效率。對於非 final 接收者類型,建議將匯出的方法指定為final
。
與 DynamicObject 的整合 #
舊的 interop 允許通過 ObjectType.getForeignAccessFactory()
指定外部存取工廠。此方法現在已棄用,並且引入了一個新方法 ObjectType.dispatch()
。調度方法需要返回一個使用明確接收者匯出 InteropLibrary 的類別,而不是外部存取工廠。
Interop 1.0
public final class SLObjectType extends ObjectType {
public static final ObjectType SINGLETON = new SLObjectType();
private SLObjectType() {
}
public static boolean isInstance(TruffleObject obj) {
return SLContext.isSLObject(obj);
}
@Override
public ForeignAccess getForeignAccessFactory(DynamicObject obj) {
return SLObjectMessageResolutionForeign.ACCESS;
}
}
@MessageResolution(receiverType = SLObjectType.class)
public class SLObjectMessageResolution {
@Resolve(message = "WRITE")
public abstract static class SLForeignWriteNode extends Node {...}
@Resolve(message = "READ")
public abstract static class SLForeignReadNode extends Node {...}
...
Interop 2.0
@ExportLibrary(value = InteropLibrary.class, receiverType = DynamicObject.class)
public final class SLObjectType extends ObjectType {
public static final ObjectType SINGLETON = new SLObjectType();
private SLObjectType() {
}
@Override
public Class<?> dispatch() {
return SLObjectType.class;
}
@ExportMessage
static boolean hasMembers(DynamicObject receiver) {
return true;
}
@ExportMessage
static boolean removeMember(DynamicObject receiver, String member) throws UnknownIdentifierException {...}
// other exports omitted
}
請注意以下差異
- 物件類型可以重用為匯出類別。
- 不再需要指定 isInstance 方法。
- 新的 interop 需要為 DynamicObject 指定接收者類型。
擴充 Interop #
使用 Truffle 實作的語言很少需要擴充 interop,但它們可能需要擴充自己的語言特定協定。
Interop 1.0
- 新增名為
FooBar
的新 KnownMessage 子類別。 - 向
ForeignAccess
新增一個新方法sendFooBar
。 - 向
ForeignAccess.Factory
新增一個新方法:createFooBar
。 - 修改 interop 注釋處理器以產生
createFooBar
的程式碼。
Interop 2.0
- 在
InteropLibrary
中新增一個新方法fooBar
。其他一切都會自動完成。