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 として区別しています。
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 しかできませんが…