Truffleとは?GraalVMで自作言語を実装する仕組みとPolyglot API

Truffleは、自作言語のインタプリタをJavaのクラスとして書くだけで、GraalVM上で機械語まで最適化して実行させるためのフレームワークです。公式リポジトリはTruffleを「自己書き換え型の抽象構文木(AST)のインタプリタとしてプログラミング言語の実装を構築するためのオープンソースライブラリ」と定義しており、開発と保守はOracleとヨハネス・ケプラー大学リンツのシステムソフトウェア研究所が担っています。ここでは言語を作る側のTruffleLanguageと、Javaアプリから他言語を呼ぶ側のPolyglot APIを、2026年8月時点の実際のAPIとバージョンで分けて整理します。

まとめ

  • Truffleは言語実装用のライブラリ、GraalコンパイラはそのASTを部分評価して機械語へ落とすJITコンパイラで、両者は役割が違います。
  • 言語を作る側はTruffleLanguageを継承し、演算ノードにTruffle DSLの@Specializationを付けて型ごとの高速路を宣言します。
  • Javaから他言語を呼ぶだけなら言語実装の知識は不要で、org.graalvm.polyglotContextValueだけで完結します。
  • 2026年8月時点の最新は GraalVM Community 25 Innovation 2(graal 25.2.4 / jdk 25.0.4、2026年7月28日公開)で、Maven Centralのtruffle-apiも25.2.4です。
  • 素のOracle JDK 25やOpenJDK 25では、Polyglot isolateを使わない限り実行時コンパイルが効かず、公式表でいうfallback runtime(インタプリタのみ)に落ちます。ここが採用可否の分岐点です。
  • 検索で混ざるEthereum開発ツールのTruffle Suiteは無関係の別製品で、2023年9月にサンセットが発表済みです。

以降は、守備範囲・実装コード・高速化の原理・Polyglot APIの書き方・採用の判断基準の順に見ていきます。

Truffleが担う範囲とGraalコンパイラとの役割分担

自己書き換えASTインタプリタという公式の定義

Truffleが提供するのは、構文木のノードを表すJavaクラス群と、そのノードが実行時に自分自身を書き換えるための仕組みです。言語実装者はレキサとパーサで構文木を組み立て、各ノードのexecute相当のメソッドに意味論を書きます。ここまでは普通のASTインタプリタと変わりません。

違いは、実行中に観測した値の型に応じてノードが別の実装へ差し替わる点です。整数どうしの加算しか現れない箇所はlong専用のノードに固定され、後から文字列が来た時点でより一般的なノードへ書き換わります。この差し替えをフレームワークが自動生成するため、実装者は型ごとの分岐を手書きしません。結果としてGraalコンパイラは、型が実質的に確定した後の安定したASTを見てコンパイルできます。

Graalコンパイラ自体は言語に依存しない汎用のJITコンパイラです。Truffleのインタプリタをコンパイルするときにだけ、AST形状を定数として畳み込む部分評価(partial evaluation)のフェーズが加わり、公式のコンパイルログもTruffle tier(主に部分評価)とGraal tierに分かれて出力されます。GraalVMという配布物、Graalコンパイラという最適化器、Truffleという言語実装ライブラリは、それぞれ別の層に属する存在です。GraalVMそのものの構成と特徴は別記事で扱っているため、本記事では言語実装とPolyglotに絞ります。

Ethereum向けTruffle Suiteとの取り違え回避

「truffle framework」で検索すると、Solidityのスマートコントラクトを開発するTruffle Suiteの情報が混ざります。名前が同じだけで、開発元も対象も無関係です。判別に迷ったら、Java・GraalVM・ASTという語が周辺にあればこちらのTruffle、SolidityやGanacheという語があればEthereum側と考えて構いません。

なおEthereum側のTruffle Suiteは、Consensysが2023年9月21日にTruffleとGanacheのサンセットを発表しています。同社の告知では発表から90日間サポートを継続し、2023年12月20日以降はコードベースを公開アーカイブとして残すとされ、移行先としてHardhatとの連携が案内されました。

TruffleLanguage継承から始める言語実装の最小構成

@TruffleLanguage.Registrationによる言語登録

言語のエントリポイントはTruffleLanguageのサブクラスです。クラスに@TruffleLanguage.Registrationを付けると、その言語IDとMIMEタイプがPolyglotのランタイムに登録され、context.eval("sl", ...)のように外部から呼べるようになります。公式のデモ言語SimpleLanguageから要点だけを抜き出すと次の形です。

@TruffleLanguage.Registration(
        id = "sl",
        name = "SL",
        defaultMimeType = "application/x-sl",
        characterMimeTypes = "application/x-sl",
        contextPolicy = ContextPolicy.SHARED)
public final class SLLanguage extends TruffleLanguage<SLContext> {

    @Override
    protected SLContext createContext(Env env) {
        return new SLContext(this, env);
    }

    @Override
    protected CallTarget parse(ParsingRequest request) throws Exception {
        Source source = request.getSource();
        RootNode root = parseProgram(source);
        return root.getCallTarget();
    }
}

実装者が必ず書くのはcreateContextparseの2つです。createContextはグローバル環境や組み込み関数レジストリなど言語ごとの状態を作ります。parseはソースを受け取り、実行可能なCallTargetを返す入口で、上のparseProgramは各言語が自前で用意するパーサ呼び出しに相当する部分です(SimpleLanguageの実ソースではSLNodeParser.parseSLを呼び、戻り値をSLEvalRootNodeで包んでいます)。

contextPolicySHAREDを指定すると、解析済みのコードを複数コンテキストで共有できる前提条件が整います。ただし公式ドキュメントは「既定ではコードは単一コンテキスト内でのみキャッシュされ、複数コンテキスト間でキャッシュするには明示的なエンジンの指定が必要」と述べており、実際に共有させるにはContext.newBuilder().engine(engine)で各コンテキストへ同じEngineを渡してください。共有の範囲を決めるのはエンジンのインスタンスです。

Truffle DSLの@Specializationが担う型特殊化

自己書き換えの中身は、ノードクラスに付けたアノテーションから自動生成されます。SimpleLanguageの加算ノードは、longで足せる間はMath.addExactを使い、桁あふれの例外が出た瞬間に多倍長の実装へ切り替わります。

@NodeInfo(shortName = "+")
public abstract class SLAddNode extends SLBinaryNode {

    @Specialization(rewriteOn = ArithmeticException.class)
    public static long doLong(long left, long right) {
        return Math.addExact(left, right);
    }

    @Specialization(replaces = "doLong")
    @TruffleBoundary
    public static SLBigInteger doSLBigInteger(SLBigInteger left, SLBigInteger right) {
        return new SLBigInteger(left.getValue().add(right.getValue()));
    }
}

読みどころは2つの属性です。rewriteOnは「この例外が出たらこの特殊化を捨てて次へ移る」という宣言で、桁あふれ検査を例外機構に任せられます。もうひとつのreplacesは「この特殊化が選ばれたら指定した特殊化はもう使わない」という指定であり、両方が有効なまま残って分岐が増えるのを防ぐ役割です。抽象クラスとして書いたこのノードから、Truffle DSLのアノテーションプロセッサがSLAddNodeGenという具象クラスとファクトリを生成します。実装者が書くのは型ごとのメソッド本体だけで、状態遷移のコードは手書きしません。

部分評価がインタプリタを機械語へ変える流れ

Truffleの高速化は、生成したコードを速くする話ではなく、インタプリタそのものを定数畳み込みの対象にする話です。ある関数が繰り返し呼ばれてホットになると、Graalコンパイラはその関数のASTを「決まった構造の定数」として扱い、インタプリタのディスパッチ処理をAST形状に沿って展開します。これが部分評価で、結果として残るのはノードを辿るループではなく、その関数だけに特化した直線的な機械語です。

先の加算ノードでいえば、longの特殊化に落ち着いた箇所は最終的にaddExactとオーバーフロー判定だけになります。型チェックはデオプティマイズ用のガードに畳まれ、通常経路の分岐としては残りません。公式のソースコメントも、この高速路が「2命令の機械語にコンパイルされる」と説明しています。

逆に言えば、部分評価が効くのは特殊化が安定した後です。同じ場所に何十種類もの型が流れ込む多態的なコードは特殊化が収束せず、汎用の遅い実装に落ちたままコンパイルされます。プロファイリングで遅い箇所を見つけたら、まず特殊化が発散していないかを疑ってください。ホットな箇所だけを実行時に機械語へ落とす発想自体はJVMのJITと同じで、前提となるJava仮想マシン(JVM)の基本構造を押さえておくと理解が早くなります。異なるのは、最適化の対象がバイトコードではなく「インタプリタとASTの組み合わせ」である点です。

ASTインタプリタとBytecode DSLの使い分け

Truffle=ASTインタプリタという理解は、2026年時点では半分だけ正しい状態になりました。同じTruffle上に、バイトコードインタプリタを自動生成するBytecode DSLが追加されているためです。graal 25.2.4時点の公式ドキュメントは、これを明確に実験的機能(experimental feature)と位置づけ、リリース間でAPIが変わりうると注意書きしています。

公式が挙げる導入動機は、ピーク性能ではなく2つの弱点の解消です。ひとつはメモリ使用量で、ASTは実行前にノードと@Cachedの状態を丸ごと確保する必要があり、数回しか実行されない初期化コードでは割に合いません。もうひとつはコンパイル前の実行速度です。ASTのexecute呼び出し点は多態になりやすく、ホストのJVMがインライン展開できません。バイトコード表現ならどちらも改善できる、という整理になっています。

@GenerateBytecode(languageClass = MyLanguage.class)
public abstract class SampleInterpreter extends RootNode
        implements BytecodeRootNode {

    protected SampleInterpreter(MyLanguage language, FrameDescriptor frameDescriptor) {
        super(language, frameDescriptor);
    }

    @Operation
    public static final class Add {
        @Specialization
        public static int doInts(int a, int b) {
            return a + b;
        }
    }
}

書き味はASTとほとんど変わらず、@Operationの中身は同じ@Specializationです。判断としては、起動時間や常駐メモリが問題にならない範囲で言語を試作するならASTのままで十分でしょう。短命なコードを大量に評価する用途や、ノード確保のコストが実測で見えている場合にだけBytecode DSLを検討し、実験的機能である以上バージョン更新時の追従コストを見込んでおいてください。安定性を優先する本番言語で、性能上の必然性なくBytecode DSLへ乗り換えるのは勧められません。

ポリグロットの語義とPolyglot APIによる言語間呼び出し

多言語話者を指す一般語との違い

ポリグロット(polyglot)は本来「複数の言語を操る人」を指す一般語で、日常の文脈では多言語話者の意味で使われます。ソフトウェア開発ではこれが転用され、ひとつのアプリケーションを複数のプログラミング言語で構成することを指します。GraalVMが「ポリグロット」を名乗るのは、言語ごとに別プロセスを立ててAPIで繋ぐのではなく、同じランタイム上で複数言語を動かし、オブジェクトを直接渡し合えるためです。

この受け渡しを支えているのがcom.oracle.truffle.api.interopのInteropLibraryです。各言語は「この値は数値として読めるか」「関数として呼べるか」といった問い合わせに答える実装を持ち、相手の言語の内部表現を知らなくても操作できます。先に挙げたSimpleLanguageの加算ノードにも、他言語から来た値に対してfitsInBigIntegerで問い合わせてから加算する特殊化が用意されています。

ContextとValueでJavaScriptをJavaから呼ぶ手順

Javaアプリから他言語を呼ぶだけなら、TruffleのAPIは一切触りません。org.graalvm.polyglotContextを開き、evalの戻り値であるValueを通して操作します。

try (Context context = Context.create()) {
    Value function = context.eval("js", "x => x + 1");
    int result = function.execute(41).asInt();
    System.out.println(result);
}

Contextはゲスト言語の実行状態を保持する単位で、closeまで確実に解放するためtry-with-resourcesで囲みます。既定ではホスト側のクラスへのアクセスは遮断されており、Java側のオブジェクトを渡したい場合はContext.newBuilderで許可範囲を明示してください。信頼できないスクリプトを評価する用途では、この既定の遮断状態を崩さない設計が前提になります。実行単位を分離して隔離する考え方は、V8のIsolateとContextの分離と対比すると掴みやすい部分です。

言語をMaven Central依存として追加する構成

ここが古い解説と最も食い違う点です。かつては言語ごとにgu installでGraalVMへコンポーネントを追加していましたが、Polyglot API 23.1.0以降は必要な成果物をすべてMaven Centralから取得します。groupIdはorg.graalvm.polyglotで、API本体に加えて使う言語を依存として並べます。

<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>polyglot</artifactId>
    <version>25.2.4</version>
</dependency>
<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>js</artifactId>
    <version>25.2.4</version>
    <type>pom</type>
</dependency>

artifactIdに指定できる言語はjs・ruby・python・java・llvm・wasm、およびまとめて入れるlanguagesです。ここでいうjavaは、Truffle上でJavaバイトコードを実行するJava on Truffle(Espresso)を指します。言語とツールの依存では<type>pom</type>の指定が必須で、これを落とすと解決に失敗します。またこれらの言語成果物はGraalVM Free Terms and Conditions(GFTC)ライセンスで提供されるため、ライセンス条件を避けたい場合はartifactIdに-communityを付けたコミュニティ版を選んでください。

Javaモジュールを使う構成では、module-info.javarequires org.graalvm.polyglot;を追加します。公式ドキュメントはモジュールパス上での埋め込みを推奨しており、クラスパスに置く構成より依存境界を明示できる点が理由です。

標準JDK(OpenJDK 25)でTruffleを動かすときの実行時最適化の制約

依存をMavenで足せるようになったことで、「GraalVMを入れなくても普通のJDKでGraalJSを動かせる」と説明されることがあります。動くのは事実ですが、速度は同じではありません。公式ドキュメントの対応表は、実行環境ごとに最適化ランタイムが使えるかを次のように分けています。

実行環境 最適化ランタイム フォールバックランタイム
Oracle GraalVM 25 対応(追加のコンパイラ最適化あり) 対応
GraalVM Community Edition 25 対応(追加設定不要) 対応
Oracle JDK 25 / OpenJDK 25 Polyglot isolate のみ 対応
JDK 21 系ランタイム Polyglot isolate のみ 対応

公式の説明によれば、フォールバックランタイムとはゲストコードを「機械語への実行時コンパイルを伴わないインタプリタのみのモード」で実行する状態を指します。素のOpenJDK 25でPolyglot依存を足しただけの構成は、原則としてここに落ちます。Polyglot isolateを使えば最適化されますが、その場合ゲスト言語はネイティブイメージとして隔離環境で動く形になり、ホストのJavaアプリ側はフォールバックランタイムのままです。

isolateを使う場合の依存は通常の言語成果物とは別で、Oracle GraalVMでは-isolate、GraalVM Community Edition 25.1以降では-isolate-communityを付けたartifactId(js-isolatejs-isolate-community)を指定します。

スクリプトの評価が処理時間の主要部分を占めるなら、実行基盤はGraalVMの配布物に揃えてください。設定ファイルの式評価やテンプレート展開のように呼び出し回数が少ない用途なら、標準JDKとMaven依存の手軽さを取ってフォールバックランタイムのまま運用しても実害は出にくい範囲です。判断材料は言語の種類ではなく、ゲストコードの実行時間比率になります。

Truffle採用の判断基準

Truffleが明確に有利なのは、独自のDSLや設定言語を自前のインタプリタで持っており、その実行速度が業務のボトルネックになっている場合です。既存のASTインタプリタをTruffleのノードへ移し替えれば、言語処理系で最も重い最適化コンパイラの開発を丸ごと外したまま、部分評価の恩恵を受けられます。

一方で、次の場合は採用を見送るべきです。第一に、言語処理系ではなく単に他言語のライブラリを呼びたいだけのケース。それはPolyglot APIの範囲で足り、TruffleLanguageを書く理由はありません。第二に、実行基盤をGraalVMに固定できないケース。前節の対応表のとおり、標準JDK上ではフォールバックランタイムに落ちるため、Truffleで作った言語の性能上の売りが消えます。第三に、既存の成熟した処理系がある言語を移植し直すケースです。Truffleへの移植は言語仕様の再実装であり、互換性の検証コストが性能改善を上回りがちです。

導入を決めたら、依存はorg.graalvm.truffle:truffle-apiと、@Specializationからコードを生成するtruffle-dsl-processor(MavenのannotationProcessorPathsに指定)から入ります。2026年8月時点の最新はいずれも25.2.4で、Maven Centralのメタデータも2026年7月28日更新です。GraalVMは2025年9月から「GraalVM 25」のような表記に変わり、25.1以降は月次のイノベーションリリースへ移行しました。Critical Patch Updateは1月・4月・7月・10月の第3火曜で、CPUと重なる月のフィーチャーリリースはテスト期間を取るためその翌週火曜にずれます(2026年7月はCPUが21日、25.2.4が28日)。CPUのない月は通常どおり月次で、公式カレンダー上は25.3.4が2026年8月25日に予定されています。実験的APIを使う場合、この頻度で追従判断が発生する前提で計画してください。

よくある質問

ポリグロットとはどういう意味ですか

語源としては複数の言語を話す人を指す一般語です。ソフトウェア開発では、ひとつのアプリケーションを複数のプログラミング言語で構成することを指します。GraalVMの文脈ではさらに限定的で、同一ランタイム上で複数の言語を動かし、値やオブジェクトをプロセス間通信なしで直接受け渡せる状態を指す用語です。GraalVMではこれをorg.graalvm.polyglotのContext経由で扱い、言語ごとの内部表現の差はInteropLibraryが吸収します。

GraalVMを入れずにOpenJDKでTruffle言語を動かせますか

動作はしますが、実行時コンパイルは既定では効きません。公式の対応表ではOracle JDK 25とOpenJDK 25の最適化ランタイムは「Polyglot isolate のみ」とされ、通常の構成ではフォールバックランタイム、つまり機械語への実行時コンパイルを行わないインタプリタのみのモードになります。動作確認や呼び出し回数の少ない用途なら問題ありませんが、ゲストコードの実行時間が支配的なワークロードではGraalVMの配布物を使ってください。

Javaから他言語を呼ぶのにTruffleの知識は必要ですか

不要です。必要なのはorg.graalvm.polyglotのContextとValueの2つだけで、TruffleLanguageや@Specializationを書く場面はありません。Mavenにpolyglot本体と使う言語の依存を追加し、Contextを開いてevalするところから始められます。TruffleのAPIが要るのは、自分で新しい言語やDSLの処理系を実装する側に回ったときだけです。

Bytecode DSLはASTインタプリタから乗り換えるべきですか

graal 25.2.4時点でBytecode DSLは実験的機能で、公式もリリース間でAPIが変わりうると明記しています。既存のAST実装がメモリ使用量や起動時間で困っていないなら、乗り換える理由はありません。検討に値するのは、短命なコードを大量に解析してノード確保のコストが実測で見えている場合や、常駐メモリの上限が厳しい環境です。移行しても@Specializationの書き方は変わらないため、必要が生じてから着手しても手戻りは限定的です。

GraalVMでRustのコードを動かせますか

Rust専用のゲスト言語は用意されていません。GraalVMにはLLVMビットコードを実行するLLVMランタイムがあり、公式が対象として挙げているのはC/C++やFortranなど、LLVMビットコードへ変換できる言語です。Rustも同じ経路に乗せる余地はありますが、公式ドキュメントが明示的にサポートを表明している言語には含まれていません。加えてMaven Centralのorg.graalvm.polyglot:llvmは2026年8月時点で25.0.1が最新で、JavaScriptなど他言語の月次リリース線には追随していない点も確認しておいてください。

関連記事

資料請求

RELATED POSTS 関連記事