Apache Calcite で Java のコードを生成する際に使っている linq4j の代わりに Truffle が使えないか調べてみたところ、truffle-sql というプロジェクトがあり、それを fork して Apache Arrow のデータを SQL で問い合わせる仕組みを作り始めました。

そこで、自分で定義したクラスのインスタンスを Truffle の中で生成し、Truffle の外に返す際にエラーが出てうまく取り出せなかったので、調べてみました。

Code

事象を確認する為に、小さなコードを書きました。まず、Truffle の外に返したいクラスは以下です。

 1package net.wrap_trap.tos;
 2
 3public class TransferObject {
 4
 5  private int result;
 6
 7  public TransferObject(int result) {
 8    this.result = result;
 9  }
10
11  @SuppressWarnings("unused")
12  public int result() {
13    return this.result;
14  }
15}

Truffle 側で処理する為に、構文が一切無い TruffleLanguage を作成します。

 1package net.wrap_trap.tos;
 2
 3import com.oracle.truffle.api.CallTarget;
 4import com.oracle.truffle.api.Truffle;
 5import com.oracle.truffle.api.TruffleLanguage;
 6import com.oracle.truffle.api.nodes.ExecutableNode;
 7
 8@TruffleLanguage.Registration(id="tso", name = "TruffleSampleLanguage", version = "0.1", mimeType = TruffleSampleLanguage.MIME_TYPE)
 9public class TruffleSampleLanguage extends TruffleLanguage<TruffleSampleLanguage.TruffleSampleContext> {
10  public static final String MIME_TYPE = "application/x-truffle-sample";
11
12  @Override
13  protected TruffleSampleContext createContext(TruffleLanguage.Env env) {
14    return new TruffleSampleContext();
15  }
16
17  @Override
18  protected ExecutableNode parse(TruffleLanguage.InlineParsingRequest request) throws Exception {
19    throw new UnsupportedOperationException();
20  }
21
22  @Override
23  protected Object findExportedSymbol(TruffleSampleContext context, String globalName, boolean onlyExplicit) {
24    return null;
25  }
26
27  @Override
28  protected Object getLanguageGlobal(TruffleSampleContext context) {
29    return context;
30  }
31
32  @Override
33  protected boolean isObjectOfLanguage(Object object) {
34    return false;
35  }
36
37  @Override
38  protected CallTarget parse(TruffleLanguage.ParsingRequest request) throws Exception {
39    TruffleSampleRootNode root = new TruffleSampleRootNode(this);
40    return Truffle.getRuntime().createCallTarget(root);
41  }
42
43  static class TruffleSampleContext {}
44}

構文の RootNode のみを作成します。実行すると TransferObject のインスタンスを返します。

 1package net.wrap_trap.tos;
 2
 3import com.oracle.truffle.api.frame.FrameDescriptor;
 4import com.oracle.truffle.api.frame.VirtualFrame;
 5import com.oracle.truffle.api.nodes.RootNode;
 6
 7public class TruffleSampleRootNode extends RootNode {
 8
 9  public TruffleSampleRootNode(TruffleSampleLanguage language) {
10    super(language, new FrameDescriptor());
11  }
12
13  @Override
14  public Object execute(VirtualFrame frame) {
15    return new TransferObject(1);
16  }
17}

この Language の eval を呼び出すテストコードは以下です。

 1package net.wrap_trap.tos;
 2
 3import org.graalvm.polyglot.Context;
 4import org.graalvm.polyglot.Value;
 5import org.junit.Test;
 6import static org.hamcrest.CoreMatchers.notNullValue;
 7import static org.junit.Assert.assertThat;
 8
 9public class TruffleBoundaryTest {
10
11  @Test
12  public void truffleBoundaryTest() {
13    Context context = Context.newBuilder("tso").build();
14    Value value = context.eval("tso", "");
15    assertThat(value, notNullValue());
16    assertThat(value.as(TransferObject.class), notNullValue());
17  }
18}

Boundary

前のセクションで「Truffle の中」「Truffle の外」と書きましたが、graal/truffle ではこれらを Host と Guest として区別しています。

Truffle Boundary

eval の実行結果を、自分で作成したクラスのオブジェクトを使って Host 側に返そうとするとエラーになります。

 1org.graalvm.polyglot.PolyglotException: java.lang.AssertionError: Not a valid guest value: net.wrap_trap.tos.TransferObject@5cc7c2a6. Only interop values are allowed to be exported.
 2
 3	at com.oracle.truffle.polyglot.PolyglotLanguageContext.lookupValueCache(PolyglotLanguageContext.java:770)
 4	at com.oracle.truffle.polyglot.PolyglotLanguageContext.asValue(PolyglotLanguageContext.java:764)
 5	at com.oracle.truffle.polyglot.PolyglotContextImpl.eval(PolyglotContextImpl.java:824)
 6	at org.graalvm.polyglot.Context.eval(Context.java:344)
 7	at org.graalvm.polyglot.Context.eval(Context.java:370)
 8	at net.wrap_trap.tos.TruffleBoundaryTest.truffleBoundaryTest(TruffleBoundaryTest.java:14)
 9(...snip...)
10Original Internal Error: 
11java.lang.AssertionError: Not a valid guest value: net.wrap_trap.tos.TransferObject@5cc7c2a6. Only interop values are allowed to be exported.
12	at com.oracle.truffle.polyglot.PolyglotLanguageContext.lookupValueCache(PolyglotLanguageContext.java:770)
13	at com.oracle.truffle.polyglot.PolyglotLanguageContext.asValue(PolyglotLanguageContext.java:764)
14(...snip...)
15Caused by: Attached Guest Language Frames (0)

TruffleObject

stacktrace と Exception の内容から、eval した結果が guest value として正しくない、と読めます。eval した結果に任意のクラスを使う場合は、そのクラスに TruffleObject を implements する必要があります。

public class TransferObject implements TruffleObject {

これで eval の結果を任意のクラスで返すことができる…とはなりません。次に、以下のようなエラーが発生します。

 1java.lang.ClassCastException: Cannot convert 'net.wrap_trap.tos.TransferObject@3a883ce7'(language: TruffleSampleLanguage, type: Unknown) to Java type 'net.wrap_trap.tos.TransferObject': Unsupported target type.
 2
 3	at com.oracle.truffle.polyglot.HostInteropErrors.newClassCastException(HostInteropErrors.java:195)
 4	at com.oracle.truffle.polyglot.HostInteropErrors.cannotConvert(HostInteropErrors.java:74)
 5	at com.oracle.truffle.polyglot.ToHostNode.asJavaObject(ToHostNode.java:539)
 6	at com.oracle.truffle.polyglot.ToHostNode.convertImpl(ToHostNode.java:210)
 7	at com.oracle.truffle.polyglot.ToHostNode.doCached(ToHostNode.java:106)
 8	at com.oracle.truffle.polyglot.ToHostNodeGen.executeAndSpecialize(ToHostNodeGen.java:115)
 9	at com.oracle.truffle.polyglot.ToHostNodeGen.execute(ToHostNodeGen.java:66)
10	at com.oracle.truffle.polyglot.PolyglotValue$InteropCodeCache$AsClassLiteralNode.executeImpl(PolyglotValue.java:1126)
11	at com.oracle.truffle.polyglot.HostToGuestRootNode.execute(HostToGuestRootNode.java:98)
12	at com.oracle.truffle.api.impl.DefaultCallTarget.call(DefaultCallTarget.java:102)
13	at com.oracle.truffle.api.impl.DefaultCallTarget$2.call(DefaultCallTarget.java:130)
14	at com.oracle.truffle.polyglot.PolyglotValue$InteropValue.as(PolyglotValue.java:2285)
15	at org.graalvm.polyglot.Value.as(Value.java:973)
16	at net.wrap_trap.tos.TruffleBoundaryTest.truffleBoundaryTest(TruffleBoundaryTest.java:20)
17(...snip...)

この Exception は、eval した時ではなく、任意のクラスのオブジェクトを Host 側で受け取る為の型変換 Value#as(Class) を呼び出した時に発生しています。色々試してみましたが、ここで任意の型に直接変換することはできませんでした。変換できる型は、JavaDoc に記載されています。

Interop Library

Truffle には 言語間のメッセージプロトコルのやり取りをサポートする Interop Library という仕組みがあります。このライブラリを使い、TransferObject を Host 側で Map として見えるようにしてみます。

 1package net.wrap_trap.tos;
 2
 3import com.oracle.truffle.api.CompilerDirectives;
 4import com.oracle.truffle.api.interop.InteropLibrary;
 5import com.oracle.truffle.api.interop.TruffleObject;
 6import com.oracle.truffle.api.interop.UnknownIdentifierException;
 7import com.oracle.truffle.api.interop.UnsupportedMessageException;
 8import com.oracle.truffle.api.library.ExportLibrary;
 9import com.oracle.truffle.api.library.ExportMessage;
10
11@SuppressWarnings({"static-method", "unused"})
12@ExportLibrary(InteropLibrary.class)
13public final class TransferObject implements TruffleObject {
14
15  private int result;
16
17  public TransferObject(int result) {
18    this.result = result;
19  }
20
21  @ExportMessage
22  boolean hasMembers() {
23    return true;
24  }
25
26  @ExportMessage
27  @CompilerDirectives.TruffleBoundary
28  Object readMember(String member) throws UnsupportedMessageException, UnknownIdentifierException {
29    if ((member != null) && (member.equals("result"))) {
30      return this.result;
31    }
32    throw new UnsupportedOperationException();
33  }
34
35  @ExportMessage
36  @CompilerDirectives.TruffleBoundary
37  Object getMembers(boolean includeInternal) throws UnsupportedMessageException {
38    throw new UnsupportedOperationException();
39  }
40
41  @ExportMessage
42  @CompilerDirectives.TruffleBoundary
43  Object writeMember(String member, Object value) throws UnsupportedMessageException, UnknownIdentifierException {
44    throw new UnsupportedOperationException();
45  }
46
47  @ExportMessage(name = "isMemberModifiable")
48  @ExportMessage(name = "isMemberReadable")
49  @CompilerDirectives.TruffleBoundary
50  boolean isMemberReadable(String member) {
51    return member.contains(member);
52  }
53
54  @ExportMessage
55  @CompilerDirectives.TruffleBoundary
56  boolean isMemberInsertable(String member) {
57    return !member.contains(member);
58  }
59}

こうすることにより、eval で Value を取得して Map に変換することで TransferObject が保持している値を取り出すことができます。

1  @Test
2  public void truffleBoundaryTest() {
3    Context context = Context.newBuilder("tso").build();
4    Value value = context.eval("tso", "");
5    assertThat(value, notNullValue());
6    Map<String, Object> map = value.as(Map.class);
7    assertThat(map.get("result"), is(1));
8  }

同じように Array として見えるようすることもできます。詳細は Truffle のテストコード を参照してください。

Conclusion

Truffle の eval の結果を Host から取り出す方法について記載しました。Truffle の記事は2016年あたりのものが多く、最新の API に対して参考にできるものが少ない印象です。

ただ、graalvm の Slack は質問に対して reply を返しているので、Web で見つからなければ Slack を検索し、それでも解決しない場合は Slack で質問してみるのも良さそうです。あとは Truffle のテストコードと、SimpleLanguage というリファレンス実装が参考になりました。

Apache Arrow のデータに対して SQL で問い合わせるコードは、以下に置いています。projection しかできませんが…

https://github.com/masayuki038/truffle-arrow