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