- 适用于 JDK 24 的 GraalVM(最新)
- 适用于 JDK 25 的 GraalVM(早期访问)
- 适用于 JDK 21 的 GraalVM
- 适用于 JDK 17 的 GraalVM
- 存档
- 开发构建
- Truffle 语言实现框架
- Truffle 分支插桩
- 动态对象模型
- 静态对象模型
- 解释器代码的主机优化
- Truffle 函数内联方法
- 分析 Truffle 解释器
- Truffle 互操作 2.0
- 语言实现
- 使用 Truffle 实现新语言
- Truffle 语言和工具迁移到 Java 模块
- Truffle 原生函数接口
- 优化 Truffle 解释器
- 选项
- 栈上替换
- Truffle 字符串指南
- 特化直方图
- 测试 DSL 特化
- 基于多语言 API 的 TCK
- Truffle 编译队列方法
- Truffle 库指南
- Truffle AOT 概述
- Truffle AOT 编译
- 辅助引擎缓存
- Truffle 语言安全点教程
- 单态化
- 拆分算法
- 单态化用例
- 向运行时报告多态特化
Truffle 互操作 2.0
本文档面向客座语言和工具的实现者。建议在继续之前,先阅读Truffle 库教程。
动机 #
在 Truffle 1.0 RC15 版本中,引入了一个名为“Truffle Libraries”的新 API。Truffle Libraries 允许用户使用多态性,并支持性能分析/缓存。Interop 2.0 计划使用 Truffle Libraries 作为互操作协议。当前的互操作 API 已经成熟且经过充分测试,并已被各种语言和工具采用。
以下是当前互操作 API 发生变化以及引入 Interop 2.0 的原因列表
- 占用空间:在当前的互操作 API 中,每次消息发送都通过一个 `CallTarget`,并且参数被装箱到 `Object[]` 中。这使得当前的互操作对于解释器调用效率低下,并且需要额外的内存。Truffle Libraries 使用简单的节点和类型专门化的调用签名,无需参数数组装箱或调用目标。
- 无缓存分派:无法从慢路径执行当前互操作消息而无需分配临时节点。Truffle Libraries 会自动为每个导出的消息生成一个无缓存版本。这使得可以在慢路径/运行时使用互操作消息,而无需分配任何临时数据结构。
- 复用多次消息分派:在当前的互操作中,对导出消息的分派会针对发送的每条消息重复进行。如果需要发送多条消息并且接收方类型变为多态,则会产生不良代码。互操作库实例可以针对输入值进行专门化。这允许用户只进行一次分派,然后调用多条消息而无需重复分派。这在多态情况下会产生更高效的代码。
- 支持默认实现:当前的互操作只能用于 `TruffleObject` 的实现。Truffle Libraries 可以与任何接收方类型一起使用。例如,可以在原始数字上调用 `isExecutable` 消息,它只会返回 `false`。
- 易错性:消息解析存在一些常见问题,Truffle Libraries 试图通过使其不可能发生来避免这些问题,例如混淆接收方类型或实现错误的类型检查。Truffle Libraries 的新断言功能允许指定消息特定的断言,从而可以验证不变量、前置条件和后置条件。
- 文档冗余:当前的互操作在 `Message` 常量和 `ForeignAccess` 静态访问器方法中记录消息。这导致了大部分冗余的文档。使用 Truffle 互操作,文档只有一个地方,即库类中的实例方法。
- 通用性:Truffle Libraries 可以用于语言表示抽象,因为它现在在内存消耗和解释器性能方面足够高效。由于这个问题,当前的互操作 API 无法实际地以这种方式使用。
- 解决协议问题:当前的互操作 API 存在一些设计问题,Interop 2.0 试图解决这些问题(详见后文)。
兼容性 #
从 Interop 1.0 到 2.0 的更改是以兼容的方式完成的。因此,旧的互操作应继续工作,并且可以逐步采用。这意味着如果一种语言仍在使用旧的互操作 API 进行调用,而另一种语言已经采用了新的互操作 API,则兼容性桥接将映射这些 API。如果您对它的工作原理感到好奇,可以查找 `DefaultTruffleObjectExports` 类以了解新互操作调用到旧互操作的映射,以及 `LegacyToLibraryNode` 以了解旧互操作调用到新互操作的映射。请注意,使用兼容性桥接可能会导致性能下降。因此,语言应尽早迁移。
互操作协议更改 #
Interop 2.0 带来了许多协议更改。本节旨在提供这些更改的理由。有关详细的参考文档,请参阅InteropLibrary Javadoc。注意:每个已弃用的 API 都在 Javadoc 中通过 `@deprecated` 标签描述了其迁移路径。
用显式类型替换 `IS_BOXED` 和 `UNBOX` #
`IS_BOXED/UNBOX` 设计存在一些问题
- 为了确定一个值是否是特定类型(例如 `String`),需要先对该值进行解箱(unbox)。解箱可能是一个开销大的操作,仅仅为了检查值的类型就导致代码效率低下。
- 旧的 API 不能用于未实现 `TruffleObject` 的值。因此,原始数字的处理需要与 `TruffleObject` 的情况分开,使得 `UNBOX` 设计对于复用现有代码变得必要。Truffle Libraries 支持原始接收方类型。
- `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 原始类型,这种设计可以扩展以支持大数字或自定义 `String` 抽象等新值。不再建议在专门化中直接使用原始类型,因为互操作原始类型的集合将来可能会改变。相反,始终使用互操作库来检查特定类型,例如,使用 `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` 的解释发生了变化,当与数字一起使用时表示数组访问,当与字符串一起使用时表示对象成员访问。`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
更强的断言 #
作为迁移的一部分,引入了许多新断言。具体的条件、前置和后置不变量在 Javadoc 中有描述。与旧的互操作节点不同,缓存库只能在作为 AST 的一部分被采用时使用。
不再有未检查/已检查异常 #
随着 Interop 2.0 的推出,`InteropException.raise` 已被弃用。虽然可能,但将已检查异常重新抛出为未检查异常被认为是一种反模式。使用 Truffle Libraries,目标语言节点直接插入到调用者的 AST 中,因此不再有限制已检查异常的 `CallTarget`。再加上 Truffle DSL 对已检查异常的额外支持,不再需要使用 `raise` 方法。取而代之的是,为所有互操作异常类型引入了一个新的 `create` 工厂方法。
计划从互操作异常中移除堆栈跟踪,以提高其效率,因为互操作异常旨在始终立即捕获且从不重新抛出。这已被推迟,直到兼容性层可以移除为止。
迁移 #
随着 Truffle Libraries 用于互操作,大多数现有互操作 API 都必须弃用。以下 Interop 1.0 与 Interop 2.0 的比较旨在帮助迁移现有的互操作使用。
快速路径发送互操作消息 #
这是在操作节点中嵌入互操作消息的快速路径方式。这是发送互操作消息最常见的方式。
互操作 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 ...
}
}
}
互操作 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` 上的静态方法。
- 旧的互操作要求为每个操作创建一个节点。在新版本中,只创建一个专门的互操作库。
- 在旧 API 中,我们需要为 `TruffleObject` 专门化接收方类型。新的互操作库可以与任何互操作值一起调用。默认情况下,对于未导出互操作库的值,`isExecutable` 将返回 `false`。例如,现在可以使用装箱的原始接收方值调用该库。
- 在旧的互操作中,我们使用 `@Cached`,而在新的互操作中,我们使用 `@CachedLibrary`。
- 新的 `@CachedLibrary` 注解指定了库专门化的值。这允许 DSL 将库实例专门化为该值。这再次允许对接收方值进行一次分派,用于所有消息调用。在旧的互操作版本中,节点无法专门化为值。因此,对于每次互操作消息发送,分派都需要重复进行。
- 专门的库实例需要为专门化方法指定一个 `limit`。如果此限制溢出,将使用库的无缓存版本,该版本不执行任何性能分析/缓存。旧的互操作 API 假定每个互操作节点的固定专门化限制为 `8`。
- 新的互操作 API 允许通过指定 `@CachedLibrary(limit="2")` 来使用库的分派版本。这允许互操作库与任何值一起使用,但它的缺点是为每次消息调用复制内联缓存,就像旧的互操作 API 一样。因此,建议尽可能使用专门的库。
慢路径发送互操作消息 #
有时需要从运行时调用互操作消息,而无需节点的上下文
互操作 1.0
ForeignAccess.sendRead(Message.READ.createNode(), object, "property")
互操作 2.0
InteropLibrary.getFactory().getUncached().read(object, "property");
请注意以下差异
- 旧接口为每次调用分配一个节点。
- 新库使用库的无缓存版本,该版本不需要为每次调用进行任何分配或装箱。
- 通过 `InteropLibrary.getFactory().getUncached(object)` 可以查找库的无缓存和专门化版本。如果需要向同一接收方发送多个无缓存的互操作消息,这可以避免重复的导出查找。
自定义快速路径发送互操作消息 #
有时不能使用 Truffle DSL,并且需要手动编写节点。两个 API 都允许这样做
互操作 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
}
}
互操作 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
}
}
请注意以下差异
- 新的互操作通过 `InteropLibrary.getFactory()` 可访问的 `LibraryFactory
` 创建节点。旧的互操作通过 `Message` 实例创建分派节点。 - 可以为新的互操作库指定分派限制。旧的互操作 API 始终假定固定限制为 `8`。
- 对于新的互操作,我们不需要检查 `TruffleObject` 类型,因为 Truffle Libraries 可以与任何接收方类型一起使用。对于非函数值,`isExecutable` 将只返回 `false`。
实现/导出互操作消息 #
要实现/导出互操作库消息,请参阅以下示例
互操作 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;
}
}
互操作 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`。新的互操作 API 仅出于兼容性原因才需要 `TruffleObject` 类型。
- 使用 `@ExportMessage` 注解代替 `@Resolve`。后者可以从方法名称推断消息的名称。如果方法名称不明确,例如导出多个库时,则可以显式指定名称和库。
- 无需为导出/解析指定类。但是,如果导出需要多个专门化,仍然可以这样做。有关详细信息,请参阅 Truffle 库教程。
- 异常现在作为已检查异常抛出。
- 不再需要实现 `getForeignAccess()`。实现会自动发现接收方类型的实现。
- 不再需要实现 `isInstance`。实现现在从类签名派生。请注意,如果接收方类型声明为 `final`,则检查会更高效。对于非 `final` 接收方类型,建议将导出的方法指定为 `final`。
与 `DynamicObject` 集成 #
旧的互操作允许通过 `ObjectType.getForeignAccessFactory()` 指定一个外部访问工厂。此方法现已弃用,并引入了一个新方法 `ObjectType.dispatch()`。分派方法不再需要返回外部访问工厂,而是需要返回一个显式接收方并导出 `InteropLibrary` 的类
互操作 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 {...}
...
互操作 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` 方法。
- 新的互操作要求将接收方类型指定为 `DynamicObject`。
扩展互操作 #
使用 Truffle 实现的语言很少需要扩展互操作,但它们可能需要扩展自己的特定语言协议
互操作 1.0
- 添加名为 `FooBar` 的新 `KnownMessage` 子类。
- 向 `ForeignAccess` 添加新方法 `sendFooBar`。
- 向 `ForeignAccess.Factory` 添加新方法 `createFooBar`。
- 修改互操作注解处理器以生成 `createFooBar` 的代码。
互操作 2.0
- 在 `InteropLibrary` 中添加新方法 `fooBar`。其他所有操作都会自动完成。