Truffle Interop 2.0

このドキュメントは、ゲスト言語およびツールの実装者を対象としています。先に進む前に、まずTruffleライブラリのチュートリアルをお読みになることをお薦めします。

使用する意義

Truffleバージョン1.0 RC15では、Truffleライブラリと呼ばれる新しいAPIが導入されました。Truffleライブラリを使用すると、プロファイリング/キャッシングをサポートする多相性を使用できます。Interop 2.0では、相互運用性プロトコルにTruffleライブラリを使用することが計画されています。現在の相互運用性APIは成熟しており、十分にテストされており、すでに言語およびツールで採用されています。

現在の相互運用性APIが変更され、Interop 2.0が導入された理由のリストを次に示します:

互換性

Interop 1.0から2.0への変更は、互換性のある方法で行われました。したがって、古い相互運用性は引き続き機能し、採用は増分的に行うことができます。つまり、一方の言語が古い相互運用性APIを使用して引き続きコールし、もう一方の言語が新しい相互運用性APIをすでに採用している場合、互換性ブリッジがAPIをマップします。この仕組みに興味がある場合は、古い相互運用性に対する新しい相互運用性コールについてDefaultTruffleObjectExportsクラスを確認してください。新しい相互運用性に対する古い相互運用性コールについてはLegacyToLibraryNodeです。互換性ブリッジを使用すると、パフォーマンスが低下する可能性があることに注意してください。そのため、言語は可能なかぎり早く移行する必要があります。

相互運用性プロトコルの変更点

Interop 2.0には、多くのプロトコルの変更が伴います。この項では、これらの変更理由について説明します。詳細なリファレンス・ドキュメントは、『InteropLibrary Javadoc』を参照してください。ノート: 非推奨のすべてのAPIは、@deprecatedでタグ付けされたJavadoc内の移行パスを記述します。

IS_BOXEDおよびUNBOXを明示的な型に置換

IS_BOXED/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は、レシーバの型BooleanByteShortIntegerLongFloatDoubleCharacterおよびStringのデフォルトの実装を指定します。この設計は、Javaプリミティブ型が直接使用されなくなったため、大きい数値やカスタム文字列の抽象化などの新しい値をサポートするように拡張できます。相互運用性のプリミティブ型のセットは将来変更される可能性があるため、特殊化でプリミティブ型を直接使用することはお薦めしません。かわりに、常に相互運用性ライブラリを使用して特定の型をチェックします。たとえば、instanceof IntegerのかわりにfitsInIntを使用します。

新しいメッセージを使用して、元の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メッセージは、isInstantiableと一致するようにinstantiateに名前が変更されました。

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からのチェック済の例外の追加サポートとともに、呼出しメソッドを使用する必要がなくなりました。かわりに、すべての相互運用性例外タイプに新しいファクトリ作成メソッドが導入されました。

相互運用性例外は常に即座に捕捉され、再スローされないようにすることを目的としているため、効率を向上させるために相互運用性例外からスタック・トレースを削除することが計画されています。これは、互換性レイヤーを削除できるようになるまで延期されました。

移行

相互運用性に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 ...
        }
    }
}

次の相違点に注意してください。

相互運用性メッセージを送信するスローパス

ノードのコンテキストを使用せずに、ランタイムから相互運用性メッセージをコールする必要がある場合があります:

Interop 1.0:

ForeignAccess.sendRead(Message.READ.createNode(), object, "property")

Interop 2.0:

InteropLibrary.getFactory().getUncached().read(object, "property");

次の相違点に注意してください。

相互運用性メッセージを送信するカスタム・ファストパス

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 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];
    }
}

次の相違点に注意してください。

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
 }

次の相違点に注意してください。

相互運用性の拡張

Truffleで実装された言語は相互運用性を拡張する必要はほとんどありませんが、独自の言語固有のプロトコルを拡張する必要がある場合があります:

Interop 1.0:

Interop 2.0: