truffle/docs/InteropMigration.md
This document is targeted at guest language and tool implementers. It is recommended to read the Truffle Library Tutorial first, before proceeding.
In Truffle Version 1.0 RC15 a new API called "Truffle Libraries" was introduced. Truffle Libraries allow users to use polymorphism with support for profiling/caching. With Interop 2.0 it is planned to use Truffle Libraries for the interoperability protocol. The current interoperability APIs are mature and well-tested and already adopted by languages and tools.
Here is a list of arguments why current interoperability APIs were changed and Interop 2.0 was introduced:
CallTarget and the arguments are boxed into an Object[]. This makes current interop inefficient for interpreter calls and it requires additional memory. Truffle Libraries use simple nodes and type-specialized call signatures that do not require argument array boxing or call targets.TruffleObject. Truffle Libraries can be used with any receiver type. For example, it is possible to invoke the isExecutable message on primitive numbers and it just returns false.Message constant and in the ForeignAccess static accessor method. This leads to mostly redundant documentation. With Truffle interop, there is only one place for the documentation, which is the instance method in the library class.The change from interop 1.0 to 2.0 was done in a compatible way.
Therefore, the old interop should continue to work and adoption can be incremental.
This means that if one language still calls using the old interop API and the other language has already adopted the new interop API, a compatibility bridge will map the APIs.
If you are curious about how this works, look for the class DefaultTruffleObjectExports for new interop calls to old interop. And LegacyToLibraryNode for old interop calls to new interop. Note that using the compatibility bridge may cause performance regressions.
That is why languages should migrate as early as possible.
Interop 2.0 comes with many protocol changes. Th
is section is intended to provide rationales for these changes. For fully detailed reference documentation see the InteropLibrary Javadoc.
Note: Every deprecated API describes its migration path in the Javadoc tagged by @deprecated.
There are some problems with the IS_BOXED/UNBOX design:
TruffleObject. Therefore, the handling of primitive numbers needed to be separated from the TruffleObject case, making the UNBOX design necessary to reuse existing code. Truffle Libraries support primitive receiver types.The following new messages were introduced in InteropLibrary as a replacement:
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)
The InteropLibrary specifies default implementations for the receiver types Boolean, Byte, Short, Integer, Long, Float, Double, Character, and String.
This design is extendable to support new values like big numbers or a custom String abstraction as Java primitive types are no longer directly used.
It is no longer recommended to use primitive types in specializations directly, as the set of interop primitive types may change in the future.
Instead, always use the interop library to check for a particular type, e.g., use fitsInInt instead of instanceof Integer.
By using the new messages it is possible to emulate the original UNBOX message like this:
@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();
}
Note: It is not recommended to unbox all primitive types like this. Instead a language should only unbox to the primitive types it actually uses. Ideally an unbox operation is not needed and the interop library is directly used to implement the operation, like this:
@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);
}
The generic READ and WRITE messages were originally designed with primarily JavaScript use-cases in mind. With the adoption of interop by more languages, it became apparent that there is a need for explicit namespaces for arrays and object members. Over time, the interpretation of READ and WRITE was changed to represent array accesses when used with numbers and object member accesses when used with strings. The HAS_SIZE message was reinterpreted as whether the value contains array elements with additional guarantees, e.g., that array elements were iterable between index 0 and size.
For better interop between languages, there is a need for an explicit Hash/Map/Dictionary entry namespace. Originally it was intended to reuse the generic READ/WRITE namespace for this. For JavaScript, this was possible, as the dictionary and member namespaces were equivalent. Most languages, however, separate Map entries from Object members, which leads to ambiguous keys. It is not possible for the source language (the protocol implementer) to know how this conflict needs to be resolved. Instead, by having explicit namespaces we can let the target language (the protocol caller) decide how to resolve the ambiguity. For example, whether dictionary or member elements should take precedence can now be decided in the target language operation.
The following interop messages were changed:
READ, WRITE, REMOVE, HAS_SIZE, GET_SIZE, HAS_KEYS, KEYS
The updated protocol with separate member and array namespace in InteropLibrary looks like this:
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)
Array access messages no longer throw UnknownIdentifierException; they instead throw InvalidArrayIndexException.
This was a bug in the original design, where the accessed number needed to be converted to the identifier string in the UnknownIdentifierException.
In the previous section, we did not mention the KEY_INFO message. The KEY_INFO message was useful to query all properties of a member or array element. While this was a conveniently small API, it was often inefficient as it required the implementer to return all the key info properties. At the same time, it is rare that the caller really needed all key info properties. With Interop 2.0 we removed the KEY_INFO message. Instead, we introduced explicit messages for each namespace, to address this issue.
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)
Note: The array namespace no longer supports querying for read or write side-effects. These messages might be reintroduced but, at the moment, there was no use-case. Also, the array namespace does not allow invocations.
The TO_NATIVE message was renamed to toNative in the InteropLibrary with the difference that it no longer returns a value, but performs the native transition as a side-effect if supported by the receiver.
This allows the caller of the message to simplify their code. No cases the toNative transition required to return a different value were found.
The default behaviour of toNative was changed to not return any value.
The following messages were mostly unchanged. The NEW message was renamed to instantiate to be consistent with 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
Many new assertions were introduced as part of the migration. The concrete pre-post and invariant conditions are described in the Javadoc. Unlike the old interop nodes, cached libraries can only be used when adopted as part of the AST.
With Interop 2.0 InteropException.raise was deprecated.
While possible, it is considered an anti-pattern to rethrow checked exceptions as unchecked exceptions.
With Truffle Libraries the target language nodes are directly inserted into the AST of the caller so there is no longer a limiting CallTarget that does not support checked exceptions.
Together with additional support for checked Exceptions from Truffle DSL, it should no longer be necessary to use the raise methods.
Instead, a new create factory method was introduced for all interop exception types.
It is planned to remove stack traces from interop exceptions in order to improve their efficiency, as interop exceptions are intended to be always immediately caught and never be rethrown. This was deferred until the compatibility layer can be removed.
With the use of Truffle Libraries for interop, most existing interop APIs had to be deprecated. The following comparison of Interop 1.0 with Interop 2.0 is designed to help migrate existing uses of interop.
This is the fast-path way of sending interop messages embedded in an operation node. This is the most common way of sending interop messages.
@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 ...
}
}
}
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 ...
}
}
}
Note the following differences:
TruffleLibrary instead of calling a static method on ForeignAccess.TruffleObject. The new interop library can be invoked with any interop value. By default isExecutable will return false for values that don't export the interop library. E.g., it is now valid to call the library with boxed primitive receiver values.@Cached in the old interop, in the new interop we use @CachedLibrary.@CachedLibrary annotation specifies the value the library specializes on. This allows the DSL to specialize the library instance to that value. This again allows the dispatch on the receiver value to be performed once for all message invocations. In the old interop version, the nodes could not be specialized to values. Therefore the dispatch needed to be repeated for every interop message send.limit for the specialization method. If this limit overflows, the uncached version of the library will be used that does not perform any profiling/caching. The old interop API assumed a constant specialization limit of 8 per interop node.@CachedLibrary(limit="2") instead. This allows the interop library to be used with any value, but it has the disadvantage of duplicating the inline cache for every message invocation, like with the old interop API. It is therefore recommended to use specialized libraries whenever possible.It is sometimes necessary to call interop messages from the runtime without the context of a node:
ForeignAccess.sendRead(Message.READ.createNode(), object, "property")
InteropLibrary.getFactory().getUncached().read(object, "property");
Note the following differences:
InteropLibrary.getFactory().getUncached(object) an uncached and specialized version of a library can be looked up. This can be used to avoid repeated export lookups if multiple uncached interop messages need to be sent to the same receiver.Sometimes Truffle DSL cannot be used and the nodes need to be written manually. Both APIs allow you to do so:
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
}
}
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
}
}
Note the following differences:
LibraryFactory<InteropLibrary> accessible through InteropLibrary.getFactory(). The old interop creates dispatching nodes through the Message instance.8.TruffleObject as Truffle Libraries can be used with any receiver type. For non-function values, isExecutable will just return false.To implement/export interop library messages, see the following example:
@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;
}
}
@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];
}
}
Note the following differences:
isInstance. The implementation is now derived from the class signature. Note that the check can be more efficient if the receiver type is declared final. For non-final receiver types, it is recommended to specify exported methods as final.The old interop allowed for specifying a foreign access factory through ObjectType.getForeignAccessFactory(). This method is now deprecated and a new method, ObjectType.dispatch(), was introduced. Instead of a foreign access factory, the dispatch method needs to return a class that exports the InteropLibrary with an explicit receiver:
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 {...}
...
@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
}
Note the following differences:
The languages implemented with Truffle rarely need to extend interop, but they might need to extend their own language specific protocol:
FooBar.sendFooBar to ForeignAccess.ForeignAccess.Factory: createFooBar.createFooBar.fooBar in InteropLibrary. Everything else is done automatically.