言語の埋め込み

GraalVM Polyglot APIを使用すると、ゲスト言語のコードをJavaホストアプリケーションに埋め込んで実行できます。

このセクションでは、GraalVMで実行され、ゲスト言語を直接呼び出すJavaでホストアプリケーションを作成する方法を学習します。各コード例の以下のタブを使用して、JavaScript、R、Ruby、Pythonから選択できます。

注:多言語埋め込みの使用に関する説明は、JDK 21およびPolyglot APIバージョン23.1.0向けのGraalVMで改訂されました。23.1.0より前のPolyglot APIバージョンを使用している場合は、正しいバージョンのドキュメントが表示されていることを確認してください。変更の詳細については、リリースノートを参照してください。

依存関係の設定 #

Polyglot APIバージョン23.1.0以降、必要なすべてのアーティファクトをMaven Centralから直接ダウンロードできます。埋め込み関連のアーティファクトは、Maven依存関係グループorg.graalvm.polyglotにあります。実行可能な完全な例のについては、GitHubの多言語埋め込みのデモを参照してください。

次のMaven依存関係の設定をプロジェクトに入れられます

<dependency> 
	<groupId>org.graalvm.polyglot</groupId> 
	<artifactId>polyglot</artifactId> 
	<version>${graalvm.polyglot.version}</version>
</dependency>
<dependency> 
	<groupId>org.graalvm.polyglot</groupId>
	<!-- Select a language: js, ruby, python, java, llvm, wasm, languages-->
	<artifactId>js</artifactId> 
	<version>${graalvm.polyglot.version}</version>
	<type>pom</type>
</dependency>
<!-- Add additional languages if needed -->
<dependency> 
	<groupId>org.graalvm.polyglot</groupId> 
    <!-- Select a tool: profiler, inspect, coverage, dap, tools -->
	<artifactId>profiler</artifactId> 
	<version>${graalvm.polyglot.version}</version>
	<type>pom</type>
</dependency>

pomタイプは言語またはツール依存関係の要件です。

言語とツールの依存関係は、GraalVM Free Terms and Conditions (GFTC)ライセンスを使用します。代わりにコミュニティライセンスバージョンを使用するには、各アーティファクト(例:js-community)に-communityサフィックスを追加します。多言語分離アーティファクトにアクセスするには、代わりに-isolateサフィックスを使用します(例:js-isolate)。

アーティファクトlanguagestoolsには、利用可能なすべての言語とツールが依存関係として含まれます。このアーティファクトは、メジャーリリース間で増減する場合があります。本番環境への導入には必要な言語のみを選択することをお勧めします。

さらに、Java モジュールを使用する場合、module-info.java ファイルで org.graalvm.polyglot が必要になります。

module com.mycompany.app {
  requires org.graalvm.polyglot;
}

構成が Truffle ランタイムの最適化で実行できるかどうかは、使用する GraalVM JDK によって異なります。詳細については、ランタイムコンパイルセクションを参照してください。

可能な限り、モジュールとモジュールパスを使用してポリグロット埋め込みを構成することをお勧めします。代わりにクラスパスから org.graalvm.polyglot を使用すると、クラスパス上のすべてのライブラリの安全でない API へのアクセスが可能になることに注意してください。アプリケーションがまだモジュール化されていない場合、クラスパスとモジュールパスのハイブリッド使用が可能です。例えば、

$java -classpath=lib --module-path=lib/polyglot --add-modules=org.graalvm.polyglot ...

この例では、lib/polyglot ディレクトリにポリグロットとすべての言語の JAR ファイルが含まれている必要があります。クラスパスからポリグロットクラスにアクセスするには、--add-modules=org.graalvm.polyglot JVM オプションも指定する必要があります。GraalVM ネイティブイメージを使用している場合、クラスパス上のポリグロットモジュールはモジュールパスに自動的にアップグレードされます。

ポリグロットライブラリから単一の UBER JAR ファイル(例:Maven Assembly プラグインを使用して)を作成することはサポートされていますが、お勧めしません。また、GraalVM ネイティブイメージを使用してネイティブバイナリを作成する場合、UBER JAR ファイルはサポートされていません。

ポリグロットアプリケーションのコンパイルと実行 #

GraalVM は、Truffle 言語実装フレームワークで実装された任意の言語で記述されたポリグロットアプリケーションを実行できます。これらの言語は以下で ゲスト言語 と呼ばれます。

このセクションの手順を完了して、GraalVM で実行され、プログラミング言語の相互運用性を示すサンプルポリグロットアプリケーションを作成します。

  1. Maven を使用して新しい Java プロジェクトを作成します。

  2. リポジトリ polyglot-embedding-demo をクローンします。
     git clone https://github.com/graalvm/polyglot-embedding-demo.git
    
  3. サンプルコードを メインクラス に挿入します。

  4. Maven pom.xml 依存関係の構成を更新して、前のセクションで説明されているように実行する言語を含めます。

  5. GraalVM をダウンロードしてインストールするには、JAVA_HOME 環境変数の値を GraalVM JDK の場所に設定します。

  6. mvn package exec:exec を実行してサンプルコードをビルドして実行します。

これで、Java ホストアプリケーションとゲスト言語コードで構成されるポリグロットアプリケーションが GraalVM で実行されています。このアプリケーションは、他のコード例で使用して、GraalVM ポリグロット API の高度な機能を実証できます。

ゲスト言語関数を Java 値として定義 #

ポリグロットアプリケーションを使用すると、あるプログラミング言語の値を取得して他の言語で使用できます。

このセクションのコード例をポリグロットアプリケーションで使用して、ポリグロット API が JavaScript、R、Ruby、Python 関数を Java 値として返す方法を示します。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class function_js {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value function = context.eval("js", "x => x+1");
    assert function.canExecute();
    int x = function.execute(41).asInt();
    assert x == 42;
}
// END-SNIPPET
    }
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class function_R {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.newBuilder()
                           .allowAllAccess(true)
                       .build()) {
    Value function = context.eval("R", "function(x) x + 1");
    assert function.canExecute();
    int x = function.execute(41).asInt();
    assert x == 42;
}
// END-SNIPPET
    }
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class function_ruby {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value function = context.eval("ruby", "proc { |x| x + 1 }");
    assert function.canExecute();
    int x = function.execute(41).asInt();
    assert x == 42;
}
 // END-SNIPPET
    }
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class function_python {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value function = context.eval("python", "lambda x: x + 1");
    assert function.canExecute();
    int x = function.execute(41).asInt();
    assert x == 42;
}
 // END-SNIPPET
    }
}

  

 このコードでは

  • Value function は関数を表す Java 値です。
  • eval 呼び出しはスクリプトを解析してゲスト言語関数を返します。
  • 最初の評価は、コードスニペットによって返された値を実行できることを確認します。
  • execute 呼び出しは、引数 41 を渡して関数を呼び出します。
  • asInt 呼び出しは結果を Java int に変換します。
  • 2 番目の評価は、結果が予想どおりに 1 だけ増分されたことを検証します。

Javaからゲスト言語に直接アクセス#

ポリグロットアプリケーションは大抵の言語タイプに簡単にアクセスでき、関数のみに限定されません。Javaなどのホスト言語は、ポリグロットアプリケーションに埋め込まれたゲスト言語値に直接アクセスできます。

このセクションのコード例をポリグロットアプリケーションで使用して、ポリグロットAPIがオブジェクト、数値、文字列、配列にアクセスする方法を示します。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class access_js_from_java {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value result = context.eval("js", 
                    "({ "                   +
                        "id   : 42, "       +
                        "text : '42', "     +
                        "arr  : [1,42,3] "  +
                    "})");
    assert result.hasMembers();

    int id = result.getMember("id").asInt();
    assert id == 42;

    String text = result.getMember("text").asString();
    assert text.equals("42");

    Value array = result.getMember("arr");
    assert array.hasArrayElements();
    assert array.getArraySize() == 3;
    assert array.getArrayElement(1).asInt() == 42;
}
// END-SNIPPET
    }
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class access_R_from_java {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.newBuilder()
                           .allowAllAccess(true)
                       .build()) {
    Value result = context.eval("R", 
                    "list("                +
                        "id   = 42, "      +
                        "text = '42', "    +
                        "arr  = c(1,42,3)" +
                    ")");
    assert result.hasMembers();
    
    int id = result.getMember("id").asInt();
    assert id == 42;
    
    String text = result.getMember("text").asString();
    assert text.equals("42");
    
    Value array = result.getMember("arr");
    assert array.hasArrayElements();
    assert array.getArraySize() == 3;
    assert array.getArrayElement(1).asInt() == 42;
}
// END-SNIPPET
    }
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class access_ruby_from_java {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value result = context.eval("ruby", 
                    "o = Struct.new(:id, :text, :arr).new(" +
                        "42, "       +
                        "'42', "     +
                        "[1,42,3] "  +
                    ")");
    assert result.hasMembers();
    
    int id = result.getMember("id").asInt();
    assert id == 42;
    
    String text = result.getMember("text").asString();
    assert text.equals("42");
    
    Value array = result.getMember("arr");
    assert array.hasArrayElements();
    assert array.getArraySize() == 3;
    assert array.getArrayElement(1).asInt() == 42;
}
// END-SNIPPET
    }
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;

public class access_python_from_java {
    public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.create()) {
    Value result = context.eval("python", 
                    "type('obj', (object,), {" +
                        "'id'  : 42, "         +
                        "'text': '42', "       +
                        "'arr' : [1,42,3]"     +
                    "})()");
    assert result.hasMembers();
    
    int id = result.getMember("id").asInt();
    assert id == 42;
    
    String text = result.getMember("text").asString();
    assert text.equals("42");
    
    Value array = result.getMember("arr");
    assert array.hasArrayElements();
    assert array.getArraySize() == 3;
    assert array.getArrayElement(1).asInt() == 42;
}
// END-SNIPPET
    }
}

  

 このコードでは

  • Value resultは、idという数値、textという文字列、arrという配列の3つのメンバーを含むオブジェクトです。
  • 最初のassertは、戻り値にメンバーが含まれることができることを検証します。これは、値がオブジェクトのような構造であることを示します。
  • id変数は、結果オブジェクトからidという名前のメンバーを読み込むことで初期化されます。次に、asInt()を使用してこの結果をJavaintに変換します。
  • 次のassertは、resultに42の値が含まれていることを検証します。
  • text変数は、メンバーtextの値を使用して初期化されます。これもasString()を使用してJavaStringに変換されます。
  • 次のassertは、result値がJavaString"42"と等しいことを検証します。
  • 次に、配列を保有するarrメンバーが読み取られます。
  • 配列はhasArrayElementsに対してtrueを返します。R配列インスタンスには、メンバーと配列要素を同時に含むことができます。
  • 次のassertは、配列のサイズが3であることを検証します。ポリグロットAPIは大きな配列をサポートするため、配列長はlong型です。
  • 最後に、インデックス1での配列要素が42であることを検証します。ポリグロット値を使用する配列のインデックス付けは、常に0から始まります。インデックスが1から始まるRなどの言語でも同じです。

ゲスト言語からJavaにアクセス#

ポリグロットアプリケーションは、ゲスト言語とホスト言語間の双方向アクセスを提供します。その結果、Javaオブジェクトをゲスト言語に渡すことができます。

ポリグロットAPIはデフォルトで安全であるため、デフォルトの構成ではアクセスは制限されています。ゲスト言語がJavaオブジェクトのパブリックメソッドまたはフィールドにアクセスすることを許可するには、コンテキストの作成時にallowAllAccess(true)を明示的に指定する必要があります。このモードでは、ゲスト言語コードはホストJavaコードがアクセスできるあらゆるリソースにアクセスできます。

このセクションのコード例をポリグロットアプリケーションで使用して、ゲスト言語が原始的なJava値、オブジェクト、配列、機能インターフェースにアクセスする方法を示します。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import java.util.concurrent.Callable;
import org.graalvm.polyglot.*;

public class access_java_from_js {

// BEGIN-SNIPPET
public static class MyClass {
    public int               id    = 42;
    public String            text  = "42";
    public int[]             arr   = new int[]{1, 42, 3};
    public Callable<Integer> ret42 = () -> 42;
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        context.getBindings("js").putMember("javaObj", new MyClass());
        boolean valid = context.eval("js",
               "    javaObj.id         == 42"          +
               " && javaObj.text       == '42'"        +
               " && javaObj.arr[1]     == 42"          +
               " && javaObj.ret42()    == 42")
           .asBoolean();
        assert valid == true;
    }
}
// END-SNIPPET
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import java.util.concurrent.Callable;
import org.graalvm.polyglot.*;

public class access_java_from_R {

// BEGIN-SNIPPET
public static class MyClass {
    public int               id    = 42;
    public String            text  = "42";
    public int[]             arr   = new int[]{1, 42, 3};
    public Callable<Integer> ret42 = () -> 42;
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        context.getBindings("R").putMember("javaObj", new MyClass());
        boolean valid = context.eval("R",
               "    javaObj$id         == 42"   +
               " && javaObj$text       == '42'" +
               " && javaObj$arr[[2]]   == 42"   +
               " && javaObj$ret42()    == 42")
           .asBoolean();
        assert valid == true;
    }
}
// END-SNIPPET
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import java.util.concurrent.Callable;
import org.graalvm.polyglot.*;

public class access_java_from_ruby {

// BEGIN-SNIPPET
public static class MyClass {
    public int               id    = 42;
    public String            text  = "42";
    public int[]             arr   = new int[]{1, 42, 3};
    public Callable<Integer> ret42 = () -> 42;
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        context.getPolyglotBindings().putMember("javaObj", new MyClass());
        boolean valid = context.eval("ruby",
               "javaObj = Polyglot.import('javaObj')\n" +
               "    javaObj[:id]         == 42"         +
               " && javaObj[:text]       == '42'"       +
               " && javaObj[:arr][1]     == 42"         +
               " && javaObj[:ret42].call == 42")
           .asBoolean();
        assert valid == true;
    }
}
// END-SNIPPET
}
 
  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import java.util.concurrent.Callable;
import org.graalvm.polyglot.*;

public class access_java_from_python {

// BEGIN-SNIPPET
public static class MyClass {
    public int               id    = 42;
    public String            text  = "42";
    public int[]             arr   = new int[]{1, 42, 3};
    public Callable<Integer> ret42 = () -> 42;
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        context.getPolyglotBindings().putMember("javaObj", new MyClass());
        boolean valid = context.eval("python",
               "import polyglot \n"                            +
               "javaObj =  polyglot.import_value('javaObj')\n" +
               "javaObj.id                   == 42"            +
               " and javaObj.text            == '42'"          +
               " and javaObj.arr[1]          == 42"            +
               " and javaObj.ret42() == 42")
           .asBoolean();
        assert valid == true;
    }
}
// END-SNIPPET
}

  

 このコードでは

  • JavaクラスMyClassには、idtextarrret42の4つのパブリックフィールドがあります。これらのフィールドには、42"42"new int[]{1, 42, 3}、常に42int値を返すラムダ() -> 42が初期化されています。
  • JavaクラスMyClassはインスタンス化され、ホスト言語とゲスト言語がシンボルを交換できるように名前javaObjでポリグロットスコープにエクスポートされます。
  • javaObjシンボルをインポートして、javaObjという名前のローカル変数に割り当てるゲスト言語スクリプトが評価されます。変数との競合を避けるため、ポリグロットスコープ内のすべての値を、言語の最上位スコープで明示的にインポートおよびエクスポートする必要があります。
  • 次の2行は、Javaオブジェクトの内容が数値42と文字列'42'と比較されて検証されます。
  • 3回目の検証は、2番目の配列位置を読み取って数値42と比較します。配列に0ベースまたは1ベースのインデックスのどちらを使用するかは、ゲスト言語によって異なります。言語に関係なく、arrフィールドに格納されたJava配列には、変換された0ベースのインデックスを使用して常にアクセスされます。たとえば、R言語では配列は1ベースなので、2番目の配列要素にはインデックス2を使用してアクセスできます。JavaScriptおよびRuby言語では、2番目の配列要素はインデックス1にあります。すべての言語例で、Java配列は同じインデックス1を使用して読み取られます。
  • 最後の行では、ret42フィールドに含まれるJavaラムダを呼び出し、結果を数値42と比較します。
  • ゲスト言語スクリプトの実行後、スクリプトがtrueboolean値を結果として返すことを確認するために検証が行われます。

ゲスト言語からJavaタイプを検索する#

ゲスト言語にJavaオブジェクトを渡すことに加えて、ゲスト言語でJavaタイプの検索を許可することもできます。

ポリグロットアプリケーションでこのセクションのコード例を使用して、ゲスト言語がJavaタイプをどのように検索してインスタンス化するかを示します。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;

public class lookup_java_from_js {


public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.newBuilder()
                           .allowAllAccess(true)
                       .build()) {
    java.math.BigDecimal v = context.eval("js",
            "var BigDecimal = Java.type('java.math.BigDecimal');" +
            "BigDecimal.valueOf(10).pow(20)")
        .asHostObject();
    assert v.toString().equals("100000000000000000000");
}
// END-SNIPPET
}
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;

public class lookup_java_from_R {


public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.newBuilder()
                           .allowAllAccess(true)
                       .build()) {
    java.math.BigDecimal v = context.eval("R",
            "BigDecimal = java.type('java.math.BigDecimal');\n" + 
            "BigDecimal$valueOf(10)$pow(20)")
        .asHostObject();
    assert v.toString().equals("100000000000000000000");
}
// END-SNIPPET
}
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;

public class lookup_java_from_ruby {

public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.newBuilder()
                           .allowAllAccess(true)
                       .build()) {
    java.math.BigDecimal v = context.eval("ruby",
            "BigDecimal = Java.type('java.math.BigDecimal')\n" + 
            "BigDecimal.valueOf(10).pow(20)")
        .asHostObject();
    assert v.toString().equals("100000000000000000000");
}
// END-SNIPPET
}
}
 
  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;

public class lookup_java_from_python {


public static void main(String[] args) {
// BEGIN-SNIPPET
try (Context context = Context.newBuilder()
                           .allowAllAccess(true)
                       .build()) {
    java.math.BigDecimal v = context.eval("python",
            "import java\n" +
            "BigDecimal = java.type('java.math.BigDecimal')\n" + 
            "BigDecimal.valueOf(10).pow(20)")
        .asHostObject();
    assert v.toString().equals("100000000000000000000");
}
// END-SNIPPET
}
}

  

 このコードでは

  • すべてのアクセスを有効にして新しいコンテキストが作成されます(allowAllAccess(true))。
  • ゲスト言語スクリプトが評価されます。
  • スクリプトはJavaタイプjava.math.BigDecimalを検索し、それをBigDecimalという名前の変数に格納します。
  • 静的メソッドBigDecimal.valueOf(long)が呼び出されて、値10を持つ新しいBigDecimalが作成されます。静的Javaメソッドを検索することに加えて、直接返されたJavaタイプをインスタンス化することもできます(たとえば、JavaScriptでは「new」キーワードを使用)。
  • 新しい小数は、10^20を計算する20のインスタンスメソッドpowを呼び出すために使用されます。
  • スクリプトの結果は、asHostObject()を呼び出すことによってホストオブジェクトに変換されます。戻り値は、自動的にBigDecimalタイプにキャストされます。
  • 結果の小数文字列が"100000000000000000000"に等しいことがアサートされます。

ポリグロットプロキシを使用した計算配列#

ポリグロットAPIにはポリグロットプロキシインターフェイスが含まれており、オブジェクト、配列、ネイティブオブジェクト、またはプリミティブなどのゲスト言語タイプを模倣して、Java相互運用性をカスタマイズできます。

ポリグロットアプリケーションでこのセクションのコード例を使用すると、値を遅延計算する配列を実装する方法がわかります。

注:ポリグロットAPIは、JVMまたはネイティブイメージ上でポリグロットプロキシをサポートします。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.*;
import org.graalvm.polyglot.proxy.*;

public class proxy_js {

// BEGIN-SNIPPET
static class ComputedArray implements ProxyArray {
    public Object get(long index) {
        return index * 2;
    }
    public void set(long index, Value value) {
        throw new UnsupportedOperationException();
    }
    public long getSize() {
        return Long.MAX_VALUE;
    }
}

public static void main(String[] args) {
    try (Context context = Context.create()) {
        ComputedArray arr = new ComputedArray();
        context.getBindings("js").putMember("arr", arr);
        long result = context.eval("js",
                    "arr[1] + arr[1000000000]")
                .asLong();
        assert result == 2000000002L;
    }
}
// END-SNIPPET
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.proxy.ProxyArray;

public class proxy_R {

// BEGIN-SNIPPET
static class ComputedArray implements ProxyArray {
    public Object get(long index) {
        return index * 2;
    }
    public void set(long index, Value value) {
        throw new UnsupportedOperationException();
    }
    public long getSize() {
        return Long.MAX_VALUE;
    }
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        ComputedArray arr = new ComputedArray();
        context.getPolyglotBindings().putMember("arr", arr);
        long result = context.eval("R",
               "arr <- import('arr');" +
               "arr[2] + arr[1000000001]")
           .asLong();
        assert result == 2000000002L;
    }
}
// END-SNIPPET
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.proxy.ProxyArray;

public class proxy_ruby {

// BEGIN-SNIPPET
static class ComputedArray implements ProxyArray {
    public Object get(long index) {
        return index * 2;
    }
    public void set(long index, Value value) {
        throw new UnsupportedOperationException();
    }
    public long getSize() {
        return Long.MAX_VALUE;
    }
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        ComputedArray arr = new ComputedArray();
        context.getPolyglotBindings().putMember("arr", arr);
        long result = context.eval("ruby",
               "arr = Polyglot.import('arr') \n" +
               "arr[1] + arr[1000000000]")
           .asLong();
        assert result == 2000000002L;
    }
}
// END-SNIPPET
}

  
  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.proxy.ProxyArray;

public class proxy_python {

// BEGIN-SNIPPET
static class ComputedArray implements ProxyArray {
    public Object get(long index) {
        return index * 2;
    }
    public void set(long index, Value value) {
        throw new UnsupportedOperationException();
    }
    public long getSize() {
        return Long.MAX_VALUE;
    }
}

public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        ComputedArray arr = new ComputedArray();
        context.getPolyglotBindings().putMember("arr", arr);
        long result = context.eval("python",
               "import polyglot\n" +
               "arr = polyglot.import_value('arr') \n" +
               "arr[1] + arr[1000000000]")
           .asLong();
        assert result == 2000000002L;
    }
}
// END-SNIPPET
}

  

 このコードでは

  • JavaクラスComputedArrayはプロキシインターフェイスProxyArrayを実装するため、ゲスト言語はJavaクラスインスタンスを配列と同様に扱います。
  • ComputedArray配列はメソッドgetをオーバーライドし、算術式を使用して値を計算します。
  • 配列プロキシは書き込みアクセスをサポートしません。このため、setの実装時にUnsupportedOperationExceptionスローされます。
  • getSizeの実装は長さにLong.MAX_VALUEを返します。
  • メインメソッドは、新しいポリグロット実行コンテキストを作成します。
  • 次に、arrという名前を使用してComputedArrayクラスの新しいインスタンスがエクスポートされます。
  • ゲスト言語スクリプトはエクスポートされたプロキシを返すarrシンボルをインポートします。
  • 2番目の要素と1000000000番目の要素にアクセスしてそれらを合計してから、返します。Rなどの1ベースの言語からの配列インデックスは、プロキシ配列の0ベースのインデックスに変換されることに注意してください。
  • 言語スクリプトの結果は長い値として返され、検証されます。

ポリグロットプロキシインターフェイスの詳細については、ポリグロットAPI JavaDocを参照してください。

ホストアクセス#

Polyglot APIはデフォルトで、ファイルI/Oなどの特定の重要な機能へのアクセスを制限します。allowAllAccesstrueに設定することで、これらの制限を完全に解除できます。

注意: アクセス制限は現在、JavaScriptでのみサポートされています。

ホスト関数のアクセスを制御 #

ゲストアプリケーションがホストにアクセスできるようにすることもあります。たとえば、System.exitを呼び出すJavaメソッドが公開されている場合、ゲストアプリケーションはホストプロセスを終了できます。誤ってメソッドが公開されないようにするには、デフォルトではホストへのアクセスは許可されておらず、すべてのパブリックメソッドまたはフィールドに明示的に@HostAccess.Exportアノテーションを付ける必要があります。

  // COMPILE-CMD: javac {file}
// RUN-CMD: java -ea {file}
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.PolyglotException;

public class explicit_access_java_from_js {

static
// BEGIN-SNIPPET
public class Employee {
    private final String name;
    Employee(String name) {this.name = name;}

    @HostAccess.Export
    public String getName() {
        return name;
    }
}
//END-SNIPPET
static
//BEGIN-SNIPPET
public class Services {
    @HostAccess.Export
    public Employee createEmployee(String name) {
        return new Employee(name);
    }
    
    public void exitVM() {
        System.exit(1);
    }
}

public static void main(String[] args) {
    try (Context context = Context.create()) {
        Services services = new Services();
        context.getBindings("js").putMember("services", services);
        String name = context.eval("js",
                "let emp = services.createEmployee('John Doe');" + 
                "emp.getName()").asString();
        assert name.equals("John Doe");
        
        try {
            context.eval("js", "services.exitVM()");
            assert false;
        } catch (PolyglotException e) {
            assert e.getMessage().endsWith(
                    "Unknown identifier: exitVM");
        }
    }
}
// END-SNIPPET
}

  

 このコードでは

  • クラスEmployeeは、String型のフィールドnameと共に宣言されています。getNameメソッドへのアクセスは、メソッドに@HostAccess.Exportアノテーションを付加することで明示的に許可されています。
  • Servicesクラスは、createEmployeeおよびexitVMという2つのメソッドを公開しています。createEmployeeメソッドは、従業員の名前を引数として受け取り、新しいEmployeeインスタンスを作成します。createEmployeeメソッドには@HostAccess.Exportアノテーションが付けられており、ゲストアプリケーションからアクセスできるようになっています。exitVMメソッドは明示的には公開されていないためアクセスできません。
  • mainメソッドは、まず、デフォルトの設定で新しいPolyglotコンテキストを作成し、@HostAccess.Exportアノテーションが付けられたメソッドを除き、ホストへのアクセスを禁止します。
  • 新しいServicesインスタンスが作成され、グローバル変数servicesとしてコンテキストに配置されます。
  • 最初に評価されたスクリプトは、サービスオブジェクトを使用して新しい従業員を作成し、その名前を返します。
  • 返された名前は、期待される名前John Doeと等しいことがアサートされています。
  • 2番目のスクリプトが評価され、サービスオブジェクトのexitVMメソッドが呼び出されます。exitVMメソッドはゲストアプリケーションに公開されていないため、PolyglotExceptionで失敗します。

ホストへのアクセスは、カスタムHostAccessポリシーを作成することで完全にカスタマイズできます。

コールバックパラメータスコープの制御 #

デフォルトでは、Valueは対応するContextと同じ期間存続します。ただし、このデフォルトの動作を変更し、値をスコープにバインドして、実行がスコープから外れると値が無効になるようにすることが必要になる場合があります。このようなスコープの例としては、ゲストからホストへのコールバックがあり、Valueをコールバックパラメータとして渡すことができます。コールバックパラメータの渡しがデフォルトのHostAccess.EXPLICITでどのように機能するかをすでに上で見てきました。

public class Services {
    Value lastResult;

    @HostAccess.Export
    public void callback(Value result) {
        this.lastResult = result;
    }

    String getResult() {
        return this.lastResult.asString();
    }
}

public static void main(String[] args) {
    Services s = new Services()
    try (Context context = Context.newBuilder().allowHostAccess(HostAccess.EXPLICIT).build()) {
        context.getBindings("js").putMember("services", s);
        context.eval("js", "services.callback('Hello from JS');");
        System.out.println(s.getResult());
    }
}

この例では、lastResultはホストに格納されたゲストからの値への参照を維持し、callback()のスコープが終わった後もアクセス可能です。

ただし、これは常に望まれることではなく、値を存続させることでリソースが不要にブロックされたり、一時的な値の動作が正しく反映されなくなったりする場合があります。そのような場合には、HostAccess.SCOPEDを使用できます。これにより、すべてのコールバックのデフォルトの動作が変更され、コールバックパラメータとして渡される値はコールバックの実行中のみ有効になります。

上記のコードをHostAccess.SCOPEDで動作させるには、コールバックパラメータとして渡される個々の値をピン留めして、コールバックが完了するまで有効期間を延長できます。

public class Services {
    Value lastResult;

    @HostAccess.Export
    void callback(Value result, Value notneeded) {
        this.lastResult = result;
        this.lastResult.pin();
    }

    String getResult() {
        return this.lastResult.asString();
    }
}

public static void main(String[] args) {
    Services s = new Services()
    try (Context context = Context.newBuilder().allowHostAccess(HostAccess.SCOPED).build()) {
        context.getBindings("js").putMember("services", s);
        context.eval("js", "services.callback('Hello from JS', 'foobar');");
        System.out.println(services.getResult());
    }
}

あるいは、コールバックメソッド全体が@HostAccess.DisableMethodScopeアノテーションで注釈付けされている場合は、スコープからオプトアウトして、コールバックのすべてのパラメータに通常のセマンティクスを維持できます。

public class Services {
    Value lastResult;
    Value metaInfo;

    @HostAccess.Export
    @HostAccess.DisableMethodScope
    void callback(Value result, Value metaInfo) {
        this.lastResult = result;
        this.metaInfo = metaInfo;
    }

    String getResult() {
        return this.lastResult.asString() + this.metaInfo.asString();
    }
}

public static void main(String[] args) {
    Services s = new Services()
    try (Context context = Context.newBuilder().allowHostAccess(HostAccess.SCOPED).build()) {
        context.getBindings("js").putMember("services", s);
        context.eval("js", "services.callback('Hello from JS', 'foobar');");
        System.out.println(services.getResult());
    }
}

アクセス権限の設定 #

ゲストアプリケーションのきめ細かなアクセス権限を設定できます。設定は新しいコンテクストを構築するときに Context.Builder クラスを使用して提供できます。次のアクセスパラメータを設定できます。

  • allowPolyglotAccess を使用して他の言語へのアクセスを許可します。
  • allowHostAccess を使用してホストオブジェクトへのアクセスを許可してカスタマイズできます。
  • allowHostClassLookup を使用してホストタイプへのホストルックアップを許可してカスタマイズできます。これにより、ゲストアプリケーションはルックアップ述語によって許可されたホストアプリケーションクラスを検索できます。たとえば、JavaScriptコンテキストは、ArrayListが classFilter によって許可され、ホストアクセスポリシーによってアクセスが許可されている場合、Java ArrayListを作成できます。 context.eval("js", "var array = Java.type('java.util.ArrayList')")
  • allowHostClassLoading を使用して、ホストクラスのロードを許可します。クラスは、ホストアクセスポリシーによってアクセスが許可されている場合にのみアクセスできます。
  • allowCreateThread を使用してスレッドの作成を許可します。
  • allowNativeAccess を使用してネイティブAPIへのアクセスを許可します。
  • allowIO を使用してIOへのアクセスを許可し、fileSystem を使用してファイルアクセスをプロキシします。

注意: クラスのロード、ネイティブAPI、またはホストI/Oへのアクセス権を付与すると、実際にはすべてのアクセス権を付与することになります。これらの権限を使用すると、他のアクセス制限をバイパスできます。

ランタイムオプティマイゼーションサポート #

Polyglot Truffleランタイムは、さまざまな実行時オプティマイゼーションのサポートを備えたいくつかのホスト仮想マシンで使用できます。ゲストアプリケーションコードの実行時オプティマイゼーションは、組み込みゲストアプリケーションを効率的に実行するために不可欠です。この表は、Javaランタイムが現在提供している最適化レベルを示しています。

Javaランタイム ランタイムオプティマイゼーションレベル
Oracle GraalVM コンパイラパスを追加して最適化
GraalVM Community Edition 最適化
Oracle JDK 実験的なVMオプションを介して有効化する場合に最適化
OpenJDK 実験的なVMオプションを介して有効化する場合に最適化
JVMCI機能を持たないJDK ランタイム最適化なし(インタプリタのみ)

説明 #

  • 最適化: 実行されたゲストアプリケーションコードは、ランタイムに非常に効率的なマシンコードとしてコンパイルして実行できます。
  • コンパイラパスを追加して最適化: Oracle GraalVMは、ランタイムコンパイル中に実行される追加の最適化を実装しています。たとえば、より高度なインラインヒューリスティックを使用します。これにより、通常、ランタイムのパフォーマンスとメモリの消費が向上します。
  • 実験的なVMオプションを介して有効化する場合に最適化: 最適化はデフォルトでは有効になっておらず、-XX:+EnableJVMCI仮想マシンオプションを使用して有効にする必要があります。さらに、コンパイルをサポートするために、GraalコンパイラをJARファイルとしてダウンロードし、--upgrade-module-path に配置する必要があります。このモードでは、コンパイラはJavaアプリケーションとして実行され、ホストアプリケーションの実行パフォーマンスに悪影響を与える場合があります。
  • ランタイム最適化なし: ランタイム最適化がない場合、または JVMCI が有効になっていない場合、ゲストアプリケーションのコードはインタプリタモードのみで実行されます。
  • JVMCI: ほとんどの Java ランタイムでサポートされる Java-レベル JVM コンパイラインターフェイス を参照します。

Oracle JDK および OpenJDK のランタイム最適化を既定で有効にするプロジェクトが作成されました。詳細については、プロジェクト Galahad を参照してください。

OpenJDK および Oracle JDK で最適化を有効にする #

OpenJDK など、既定でランタイム最適化が有効になっている JDK ランタイム上で実行する場合、次のような警告が表示されることがあります

[engine] WARNING: The polyglot engine uses a fallback runtime that does not support runtime compilation to machine code.
Execution without runtime compilation will negatively impact the guest application performance.

これは、ゲストアプリケーションがランタイム最適化なしで実行されていることを示します。この警告は、--engine.WarnInterpreterOnly=false オプションまたは -Dpolyglot.engine.WarnInterpreterOnly=false システムプロパティを使用して抑制できます。さらに、compiler.jar ファイルとその依存関係は Maven Central からダウンロードして、--upgrade-module-path オプションを使用して参照する必要があります。compiler.jarモジュールまたはクラスパスに配置しないでください。Maven または Gradle を使用したサンプル設定については、ポリグロット埋め込みデモンストレーション を参照してください。

フォールバックエンジンに切り替える #

たとえば、簡単なスクリプトのみを実行する場合やリソース制約のあるシステムで実行する場合など、必要に応じて、ランタイム最適化なしでフォールバックエンジンに切り替えることができます。Polyglot バージョン 23.1 以降、フォールバックエンジンはクラスまたはモジュールパスから truffle-runtimetruffle-enterprise モジュールを削除することでアクティブ化できます。

Maven では次のように実現できます。

<dependencies>
  <dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>js</artifactId>
    <version>$graalvm-version</version>
    <exclusions>
      <exclusion>
        <groupId>org.graalvm.truffle</groupId>
        <artifactId>truffle-runtime</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.graalvm.truffle</groupId>
        <artifactId>truffle-enterprise</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
</dependencies>

-community 依存関係のみを使用する場合、truffle-enterprise の除外ルールは不要です。truffle-enterprise が除外されているため、フォールバックエンジンはサンドボックスの制限やポリグロットアイソレートなどの高度な拡張機能をサポートしません。2 つの依存関係が他の場所に含まれていないことを mvn dependency:tree で二重確認するとよいでしょう。

ランタイムが正常に除外された場合、次のログメッセージが表示されます。

[engine] WARNING: The polyglot engine uses a fallback runtime that does not support runtime compilation to native code.
Execution without runtime compilation will negatively impact the guest application performance.
The following cause was found: No optimizing Truffle runtime found on the module or class path.
For more information see: https://graalvm.dokyumento.jp/latest/reference-manual/embed-languages/.
To disable this warning use the '--engine.WarnInterpreterOnly=false' option or the '-Dpolyglot.engine.WarnInterpreterOnly=false' system property.

このメッセージは、追加の手順として示されているオプションを使用して無効にすることができます。

これらの依存関係を削除すると、Native Image ビルドでもフォールバックエンジンに自動的に切り替わります。

ポリグロットアプリケーションからネイティブ実行ファイルをビルドする #

JDK 21 以降の GraalVM 上の Polyglot バージョン 23.1 では、埋め込みポリグロット言語ランタイムでイメージをビルドするために ネイティブイメージ を使用する際に特別な設定は必要ありません。他の Java 依存関係と同様に、ネイティブ実行ファイルをビルドするときはポリグロット言語 JAR ファイルがクラスまたはモジュールパス上にある必要があります。native-image ビルドを設定するには、Maven または Gradle ネイティブイメージプラグインを使用することをお勧めします。ネイティブイメージ用のサンプルの Maven と Gradle 設定は、ポリグロット埋め込みデモンストレーションリポジトリ にあります。

Maven プロファイル設定の例を次に示します。

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <version>0.10.1</version>
                    <extensions>true</extensions>
                    <executions>
                        <execution>
                            <id>build-native</id>
                            <goals>
                                <goal>compile-no-fork</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                    </executions>
                    <configuration>
                        <imageName>${project.artifactId}</imageName>
                        <mainClass>org.example.embedding.Main</mainClass>
                        <buildArgs>
                            <buildArg>--no-fallback</buildArg>
                            <buildArg>-J-Xmx20g</buildArg>
                        </buildArgs>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

上記の構成でネイティブ実行ファイルをビルドするには、次を実行します。

mvn -Pnative package

ポリグロットアプリケーションからネイティブの実行可能ファイルをビルドする場合、たとえば、Pythonを埋め込むJavaホストアプリケーションの場合、必要なすべてのファイルを含む「./resources」ディレクトリが初期設定で作成されます。初期設定では、言語ランタイムはビルドされたネイティブの実行可能ファイルまたはライブラリイメージを基準にリソースディレクトリを検索します。実行時に検索する場所をカスタマイズするには、「-Dpolyglot.engine.resourcePath=resourceへのパス」オプションを使用できます。リソースの作成を無効にするには、ビルド時のオプション「-H:-CopyLanguageResources」を使用できます。リソースディレクトリなしで実行できない言語がある可能性があることに注意してください。

ポリグロットバージョン23.1では、org.graalvm.homeなどの、言語ホームオプションは使用されなくなり、リソースディレクトリオプションに置き換えられました。言語ホームオプションは互換性の理由から機能し続けますが、今後のリリースで削除される可能性があります。

ネイティブホストリフレクションの構成 #

ゲストアプリケーションからホストJavaコードにアクセスするには、Javaリフレクションの機能が必要です。リフレクションをネイティブ実行可能ファイル内で使用する場合、リフレクション設定ファイルが必要です。

この例では、JavaScriptを使用してネイティブ実行可能ファイルのホストアクセスを示します。次のコードをAccessJavaFromJS.javaという新しいファイルにコピーします。

import org.graalvm.polyglot.*;
import org.graalvm.polyglot.proxy.*;
import java.util.concurrent.*;

public class AccessJavaFromJS {

    public static class MyClass {
        public int               id    = 42;
        public String            text  = "42";
        public int[]             arr   = new int[]{1, 42, 3};
        public Callable<Integer> ret42 = () -> 42;
    }

    public static void main(String[] args) {
        try (Context context = Context.newBuilder()
                                   .allowAllAccess(true)
                               .build()) {
            context.getBindings("js").putMember("javaObj", new MyClass());
            boolean valid = context.eval("js",
                   "    javaObj.id         == 42"          +
                   " && javaObj.text       == '42'"        +
                   " && javaObj.arr[1]     == 42"          +
                   " && javaObj.ret42()    == 42")
               .asBoolean();
            System.out.println("Valid " + valid);
        }
    }
}

次のコードをreachability-metadata.jsonにコピーします。

{
  "reflection": [
     { "type": "AccessJavaFromJS$MyClass", "allPublicFields": true },
     { "type": "java.util.concurrent.Callable", "allPublicMethods": true }
  ]
}

これで、reachability-metadata.jsonをプロジェクトのMETA-INF/native-image/<group-id>/に追加できます。

複数のコンテキストでのコードキャッシング #

GraalVMポリグロットAPIを使用すると、複数のコンテキストでコードキャッシングできます。コードキャッシングを使用すると、コンパイル済みのコードを再利用でき、ソースは1回だけ解析できます。コードキャッシングを使用すると、多くの場合、アプリケーションのメモリ使用量とウォームアップ時間を削減できます。

初期設定では、コードは1つのコンテキストインスタンス内にのみキャッシュされます。複数のコンテキスト間でコードキャッシングを有効にするには、明示的にエンジンを指定する必要があります。エンジンは、コンテキストビルダーを使用してコンテキストを作成するときに指定されます。エンジンのインスタンスはコード共有の範囲を決定します。コードは、1つのエンジンインスタンスに関連付けられたコンテキスト間でのみ共有されます。

すべてのソースは初期設定でキャッシュされます。キャッシュはcached(boolean cached)を「false」に設定することで明示的に無効にできます。キャッシングは無効にすると、ソースが1回のみ評価されることがわかっている場合に役立ちます。

たとえば、次のコードスニペットを検討します。

public class Main {
    public static void main(String[] args) {
        try (Engine engine = Engine.create()) {
            Source source = Source.create("js", "21 + 21");
            try (Context context = Context.newBuilder()
                .engine(engine)
                .build()) {
                    int v = context.eval(source).asInt();
                    assert v == 42;
            }
            try (Context context = Context.newBuilder()
                .engine(engine)
                .build()) {
                    int v = context.eval(source).asInt();
                    assert v == 42;
            }
        }
    }
}

このコードでは

  • import org.graalvm.polyglot.* はポリグロットAPIのベースAPIをインポートします。
  • Engine.create()は初期設定で新しいエンジンインスタンスを作成します。
  • Source.create()は「21 + 21」という式のソースオブジェクトを作成します。JavaScriptの言語IDである「js」言語の、明示的なSourceオブジェクトを使用します。コードキャッシュがコンテキスト間でガベージコレクションされないようにします。
  • Context.newBuilder().engine(engine).build()は、明示的にエンジンが割り当てられた新しいコンテキストを構築します。エンジンに関連付けられたすべてのコンテキストはコードを共有します。
  • context.eval(source).asInt()はソースを評価し、結果をValueインスタンスとして返します。

重要:実行するコンテキスト間でキャッシュされたソースのコードキャッシュを有効にするために、アプリケーションはSourceオブジェクトが継続的に参照されるようにする必要があります。ポリグロットランタイムは、次のGCサイクルで参照されなくなったソースのキャッシュされたコードを収集する場合があります。

コードキャッシュの管理 #

コードキャッシュに関するデータは、Engine インスタンスの一部として格納されます。2 つの別々のエンジンインスタンス間でコードを共有することはありません。そのため、グローバルコードキャッシュが必要な場合は、シングルトン Engine インスタンスを使用することをお勧めします。コンテキストとは異なり、エンジンは常に複数のスレッド間で共有できます。コンテキストが複数のスレッド間で共有できるかどうかは、使用する言語によって異なります。

コードキャッシュをパージするための明示的なメソッドはありません。次のコレクションでガベージコレクタが自動的にこれを行うことを期待しています。エンジンのコードキャッシュは、エンジンがまだ強く参照されており、閉じられていない限り、コレクションされません。また、関連付けられたコードがコレクションされないようにするには、Source インスタンスをアクティブにしておく必要があります。ソースインスタンスへの参照がなくなってもエンジンへの参照が残っている場合、ソースオブジェクトに関連付けられたコードキャッシュは GC によってコレクションされることがあります。したがって、Source はキャッシュされたままにする必要があるかぎり、Source オブジェクトへの強い参照を維持することをお勧めします。

つまり、コードキャッシュは、Engine オブジェクトと Source オブジェクトへの強い参照を保持して維持することで制御できます。

ポリグロットアイソレート #

Oracle GraalVM では、ポリグロットエンジンを専用のネイティブイメージアイソレートで実行するように構成できます。このモードのポリグロットエンジンは、VM レベルのフォールトドメイン内で、専用のガベージコレクタと JIT コンパイラを使用して実行されます。ポリグロットアイソレートは サンドボックス化 に役立ちます。アイソレートで言語を実行することは、HotSpot とネイティブイメージホスト仮想マシンで機能します。

ポリグロットアイソレートとして使用される言語は、-isolate サフィックスを使用して Maven Central からダウンロードできます。たとえば、分離された JavaScript に依存することは、次のような Maven 依存関係を追加することで構成できます。

<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>polyglot</artifactId>
    <version>24.0.0</version>
    <type>jar</type>
</dependency>
<dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>js-isolate</artifactId>
    <version>24.0.0</version>
    <type>pom</type>
</dependency>

ダウンロードされた依存関係はプラットフォームに依存せず、各プラットフォーム用のネイティブイメージが含まれています。今後のリリースで、個々のプラットフォーム用のポリグロットアイソレートネイティブイメージのダウンロードをサポートする予定です。

ポリグロット API を使用してアイソレートの使用を有効にするには、--engine.SpawnIsolate=true オプションを構築時に Engine または Context に渡す必要があります。他の JDK で使用する場合は、engine.SpawnIsolate オプションは使用できない場合があります。

import org.graalvm.polyglot.*;

public class PolyglotIsolate {
	public static void main(String[] args) {
		try (Context context = Context.newBuilder("js")
			  .allowHostAccess(HostAccess.SCOPED)
			  .option("engine.SpawnIsolate", "true").build()) {
			  
			Value function = context.eval("js", "x => x+1");
			assert function.canExecute();
			int x = function.execute(41).asInt();
			assert x == 42;
		}
	}
}

現在、ポリグロットアイソレートとして使用できる言語は次のとおりです。

言語 入手可能元
JavaScript (js-isolate) 23.1

今後のバージョンで、より多くの言語をサポートする予定です。

前の例では、HostAccess.SCOPED を使用してスコープ付き参照を有効にしています。これは、ホスト GC とゲスト GC が互いに関係がないため、オブジェクト間の循環参照は自動的に解決できないため、必要です。したがって、循環参照を完全に回避するため、ホストコールバックに スコープ付きパラメータ を使用することを強くお勧めします。

複数のコンテキストは、共有エンジン を使用して、同じ分離されたエンジンで生成できます。

public class PolyglotIsolateMultipleContexts {
    public static void main(String[] args) {
        try (Engine engine = Engine.newBuilder("js")
                .option("engine.SpawnIsolate", "true").build()) {
            Source source = Source.create("js", "21 + 21");
            try (Context context = Context.newBuilder()
                .engine(engine)
                .build()) {
                    int v = context.eval(source).asInt();
                    assert v == 42;
            }
            try (Context context = Context.newBuilder()
                .engine(engine)
                .build()) {
                    int v = context.eval(source).asInt();
                    assert v == 42;
            }
        }
    }
}

ネイティブイメージランタイムオプションの渡す #

アイソレートで実行されているエンジンは、--engine.IsolateOption.<option> をエンジンビルダーに渡すことで、ネイティブイメージランタイムオプション を利用できます。たとえば、--engine.IsolateOption.MaxHeapSize=128m を使用してアイソレートの最大ヒープサイズを設定することで、エンジンによって使用される最大ヒープメモリを制限できます。

import org.graalvm.polyglot.*;

public class PolyglotIsolateMaxHeap {
  public static void main(String[] args) {
    try {
      Context context = Context.newBuilder("js")
        .allowHostAccess(HostAccess.SCOPED)
        .option("engine.SpawnIsolate", "true")
        .option("engine.IsolateOption.MaxHeapSize", "64m").build()
      context.eval("js", "var a = [];while (true) {a.push('foobar');}");
    } catch (PolyglotException ex) {
      if (ex.isResourceExhausted()) {
        System.out.println("Resource exhausted");
      }
    }
  }
}

最大ヒープサイズを超えると、コンテキストが自動的に閉じられ、PolyglotException が発生します。

ホストコールバックスタックヘッドルームを確保する #

ポリグロットアイソレートでは、--engine.HostCallStackHeadRoom はホストコールバックを実行する際の最小スタックスペースを確保します。使用可能なスタックサイズが指定されたしきい値を下回ると、ホストコールバックは失敗します。

メモリー保護 #

メモリー保護キーをサポートする Linux 環境では、--engine.MemoryProtection=true オプションを使用して、Polyglot アイソレートのヒープをハードウェアレベルで分離できます。このオプションを使用してエンジンが作成されると、隔離されたエンジンのヒープに専用の保護キーが割り当てられます。GraalVM は、Polyglot アイソレートのコードを実行するときにのみ、エンジンのヒープへのアクセスを許可します。

ゲスト言語を Java に埋め込む #

GraalVM Polyglot API は、Java 相互運用を使用してゲスト言語内から使用できます。これは、スクリプトを親コンテキストから分離して実行する必要がある場合に役立ちます。ホスト言語としての Java では、Context.eval(Source) への呼び出しは Value のインスタンスを返しますが、このコードをゲスト言語の一部として実行するため、代わりに言語固有の相互運用 API を使用できます。そのため、言語内で作成されたコンテキストによって返された値を、言語の通常の値のように使用できます。次の例では、value.data の代わりに、value.getMember("data") とはっきりと書くことができます。外部の値との相互運用方法については、各言語のドキュメントを参照してください。複数のコンテキスト間での値の共有に関する詳細情報は、こちら で見つけることができます。

たとえば、次のコードスニペットを検討します。

import org.graalvm.polyglot.*;

public class Main {
    public static void main(String[] args) {
        try (Context outer = Context.newBuilder()
                                   .allowAllAccess(true)
                               .build()) {
            outer.eval("js", "inner = Java.type('org.graalvm.polyglot.Context').create()");
            outer.eval("js", "value = inner.eval('js', '({data:42})')");
            int result = outer.eval("js", "value.data").asInt();
            outer.eval("js", "inner.close()");

            System.out.println("Valid " + (result == 42));
        }
    }
}

このコードでは

  • Context.newBuilder().allowAllAccess(true).build() はあらゆる特権を持つ新しい外部コンテキストを構築します。
  • outer.eval は外部コンテキストで JavaScript スニペットを評価します。
  • inner = Java.type('org.graalvm.polyglot.Context').create() 最初の JS スクリプト行は Java ホストのタイプ Context を検索し、特権のない (既定) 新しい内部コンテキストインスタンスを作成します。
  • inner.eval('js', '({data:42})'); は内部コンテキストで JavaScript コード ({data:42}) を評価し、結果を格納して返します。
  • "value.data" この行は内部コンテキストの結果からメンバー data を読み込みます。この結果は、内部コンテキストがまだ閉じられていない限り、読み取ることができるということに注意してください。
  • context.eval("js", "c.close()") このスニペットは内部コンテキストを閉じます。内部コンテキストは手動で閉じる必要があり、親コンテキストで自動的に閉じられることはありません。
  • 最後に、この例では「有効、true」がコンソールに表示されることが想定されます。

多言語のシェルを作成する #

GraalVM Polyglot API を使用すると、わずか数行のコードで、GraalVM がサポートするあらゆるゲスト言語と統合するアプリケーションを構築できます。

このシェルの実装は、特定のゲスト言語には依存しません。

BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
PrintStream output = System.out;
Context context = Context.newBuilder().allowAllAccess(true).build();
Set<String> languages = context.getEngine().getLanguages().keySet();
output.println("Shell for " + languages + ":");
String language = languages.iterator().next();
for (;;) {
    try {
        output.print(language + "> ");
        String line = input.readLine();
        if (line == null) {
            break;
        } else if (languages.contains(line)) {
            language = line;
            continue;
        }
        Source source = Source.newBuilder(language, line, "<shell>")
                        .interactive(true).buildLiteral();
        context.eval(source);
    } catch (PolyglotException t) {
        if(t.isExit()) {
            break;
        }
        t.printStackTrace();
    }
}

実行リスナーを使用してステップスルー #

GraalVM Polyglot API ではユーザーは ExecutionListener クラス を介してゲスト言語の実行をインストゥルメント化できます。たとえば、ゲスト言語プログラムのあらゆる文に対して呼び出される実行リスナーをアタッチできます。実行リスナーは、Polyglot エンベダー用のシンプルな API として設計されており、たとえばプログラムをステップスルーするような場合に便利です。

import org.graalvm.polyglot.*;
import org.graalvm.polyglot.management.*;

public class ExecutionListenerTest {
    public static void main(String[] args) {
        try (Context context = Context.create("js")) {
            ExecutionListener listener = ExecutionListener.newBuilder()
                      .onEnter((e) -> System.out.println(
                              e.getLocation().getCharacters()))
                      .statements(true)
                      .attach(context.getEngine());
            context.eval("js", "for (var i = 0; i < 2; i++);");
            listener.close();
        }
    }
}

このコードでは

  • Context.create() 呼び出しはゲスト言語用の新しいコンテキストを作成します。
  • ExecutionListeners.newBuilder() を呼び出して、実行リスナービルダーを作成します。
  • onEnter イベントを、要素の実行が入力されて消費されるときに通知するように設定します。少なくとも 1 つのイベントコンシューマーと 1 つのフィルター済みソース要素を有効にする必要があります。
  • リスナーのアタッチを完了するには、attach() を呼び出す必要があります。
  • statements(true) は実行リスナーを文だけにフィルターします。
  • context.eval() 呼び出しは指定されたゲスト言語コードのスニペットを評価します。
  • listener.close() はリスナーを早く閉じますが、実行リスナーはエンジンで自動的に閉じられます。

JSR-223 ScriptEngine との互換性 #

Truffle言語実装フレームワークは、JSR-223 ScriptEngine実装は提供しておりません。Polyglot APIはTruffle機能に対するよりきめ細かい制御を行います。多くの設定を直接管理し、GraalVMのよりきめ細かいセキュリティ設定を活用するため、org.graalvm.polyglot.Contextインターフェースを使用することを強く推奨します。

ただし、ScriptEngine APIを使用して統合された他のスクリプト言語の代わりにTruffle言語を簡単に評価するために、1つのファイルのスクリプトエンジンを次に示します。このファイルをソースツリーにドロップして、ScriptEngine API経由でTruffle言語を評価するために直接使用できます。プロジェクトに合わせるために適応する行はわずか2行です。

public final class CHANGE_NAME_EngineFactory implements ScriptEngineFactory {
    private static final String LANGUAGE_ID = "<<INSERT LANGUAGE ID HERE>>";

クラスを希望どおりに名前変更し、LANGUAGE_IDを希望するTruffle言語(例: GraalPyの場合は「python」、TruffleRubyの場合は「ruby」)に変更します。使用するには、選択したクラス名をリソース内のMETA-INF/services/javax.script.ScriptEngineFactoryファイルに含めます。これにより、デフォルトのjavax.script.ScriptEngineManagerが言語を自動的に検出できるようになります。または、javax.script.ScriptEngineManager#registerEngineNameを介してファクトリを登録したり、インスタンス化して直接使用することができます。

最善の方法は、ファイナライザーに頼るのではなく、ScriptEngineを使用しなくなったときにそれを閉じることです。ScriptEngineclose()メソッドがないため、それを閉じるには((AutoCloseable) scriptEngine).close();を使用します。

GraalJSは、JDK 11で推奨廃止されたNashorn JavaScriptエンジンから移行するユーザー向けにScriptEngine実装を提供することに注意してください。したがって、このメソッドは不要です。

展開して、1つのファイルでTruffle言語のScriptEngineFactory実装を確認しましょう。

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;

import org.graalvm.home.Version;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.Language;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;

public final class CHANGE_NAME_EngineFactory implements ScriptEngineFactory {
    private static final String LANGUAGE_ID = "<>";

    /***********************************************************/
    /* Everything below is generic and does not need to change */
    /***********************************************************/

    private final Engine polyglotEngine = Engine.newBuilder().build();
    private final Language language = polyglotEngine.getLanguages().get(LANGUAGE_ID);

    @Override
    public String getEngineName() {
        return language.getImplementationName();
    }

    @Override
    public String getEngineVersion() {
        return Version.getCurrent().toString();
    }

    @Override
    public List getExtensions() {
        return List.of(LANGUAGE_ID);
    }

    @Override
    public List getMimeTypes() {
        return List.copyOf(language.getMimeTypes());
    }

    @Override
    public List getNames() {
        return List.of(language.getName(), LANGUAGE_ID, language.getImplementationName());
    }

    @Override
    public String getLanguageName() {
        return language.getName();
    }

    @Override
    public String getLanguageVersion() {
        return language.getVersion();
    }

    @Override
    public Object getParameter(final String key) {
        switch (key) {
            case ScriptEngine.ENGINE:
                return getEngineName();
            case ScriptEngine.ENGINE_VERSION:
                return getEngineVersion();
            case ScriptEngine.LANGUAGE:
                return getLanguageName();
            case ScriptEngine.LANGUAGE_VERSION:
                return getLanguageVersion();
            case ScriptEngine.NAME:
                return LANGUAGE_ID;
        }
        return null;
    }

    @Override
    public String getMethodCallSyntax(final String obj, final String m, final String... args) {
        throw new UnsupportedOperationException("Unimplemented method 'getMethodCallSyntax'");
    }

    @Override
    public String getOutputStatement(final String toDisplay) {
        throw new UnsupportedOperationException("Unimplemented method 'getOutputStatement'");
    }

    @Override
    public String getProgram(final String... statements) {
        throw new UnsupportedOperationException("Unimplemented method 'getProgram'");
    }

    @Override
    public ScriptEngine getScriptEngine() {
        return new PolyglotEngine(this);
    }

    private static final class PolyglotEngine implements ScriptEngine, Compilable, Invocable, AutoCloseable {
        private final ScriptEngineFactory factory;
        private PolyglotContext defaultContext;

        PolyglotEngine(ScriptEngineFactory factory) {
            this.factory = factory;
            this.defaultContext = new PolyglotContext(factory);
        }

        @Override
        public void close() {
            defaultContext.getContext().close();
        }

        @Override
        public CompiledScript compile(String script) throws ScriptException {
            Source src = Source.create(LANGUAGE_ID, script);
            try {
                defaultContext.getContext().parse(src); // only for the side-effect of validating the source
            } catch (PolyglotException e) {
                throw new ScriptException(e);
            }
            return new PolyglotCompiledScript(src, this);
        }

        @Override
        public CompiledScript compile(Reader script) throws ScriptException {
            Source src;
            try {
                src = Source.newBuilder(LANGUAGE_ID, script, "sourcefromreader").build();
                defaultContext.getContext().parse(src); // only for the side-effect of validating the source
            } catch (PolyglotException | IOException e) {
                throw new ScriptException(e);
            }
            return new PolyglotCompiledScript(src, this);
        }

        @Override
        public Object eval(String script, ScriptContext context) throws ScriptException {
            if (context instanceof PolyglotContext) {
                PolyglotContext c = (PolyglotContext) context;
                try {
                    return c.getContext().eval(LANGUAGE_ID, script).as(Object.class);
                } catch (PolyglotException e) {
                    throw new ScriptException(e);
                }
            } else {
                throw new ClassCastException("invalid context");
            }
        }

        @Override
        public Object eval(Reader reader, ScriptContext context) throws ScriptException {
            Source src;
            try {
                src = Source.newBuilder(LANGUAGE_ID, reader, "sourcefromreader").build();
            } catch (IOException e) {
                throw new ScriptException(e);
            }
            if (context instanceof PolyglotContext) {
                PolyglotContext c = (PolyglotContext) context;
                try {
                    return c.getContext().eval(src).as(Object.class);
                } catch (PolyglotException e) {
                    throw new ScriptException(e);
                }
            } else {
                throw new ScriptException("invalid context");
            }
        }

        @Override
        public Object eval(String script) throws ScriptException {
            return eval(script, defaultContext);
        }

        @Override
        public Object eval(Reader reader) throws ScriptException {
            return eval(reader, defaultContext);
        }

        @Override
        public Object eval(String script, Bindings n) throws ScriptException {
            throw new UnsupportedOperationException("Bindings for Polyglot language cannot be created explicitly");
        }

        @Override
        public Object eval(Reader reader, Bindings n) throws ScriptException {
            throw new UnsupportedOperationException("Bindings for Polyglot language cannot be created explicitly");
        }

        @Override
        public void put(String key, Object value) {
            defaultContext.getBindings(ScriptContext.ENGINE_SCOPE).put(key, value);
        }

        @Override
        public Object get(String key) {
            return defaultContext.getBindings(ScriptContext.ENGINE_SCOPE).get(key);
        }

        @Override
        public Bindings getBindings(int scope) {
            return defaultContext.getBindings(scope);
        }

        @Override
        public void setBindings(Bindings bindings, int scope) {
            defaultContext.setBindings(bindings, scope);
        }

        @Override
        public Bindings createBindings() {
            throw new UnsupportedOperationException("Bindings for Polyglot language cannot be created explicitly");
        }

        @Override
        public ScriptContext getContext() {
            return defaultContext;
        }

        @Override
        public void setContext(ScriptContext context) {
            throw new UnsupportedOperationException("The context of a Polyglot ScriptEngine cannot be modified.");
        }

        @Override
        public ScriptEngineFactory getFactory() {
            return factory;
        }

        @Override
        public Object invokeMethod(Object thiz, String name, Object... args)
                throws ScriptException, NoSuchMethodException {
            try {
                Value receiver = defaultContext.getContext().asValue(thiz);
                if (receiver.canInvokeMember(name)) {
                    return receiver.invokeMember(name, args).as(Object.class);
                } else {
                    throw new NoSuchMethodException(name);
                }
            } catch (PolyglotException e) {
                throw new ScriptException(e);
            }
        }

        @Override
        public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {
            throw new UnsupportedOperationException();
        }

        @Override
        public  T getInterface(Class interfaceClass) {
            throw new UnsupportedOperationException();
        }

        @Override
        public  T getInterface(Object thiz, Class interfaceClass) {
            return defaultContext.getContext().asValue(thiz).as(interfaceClass);
        }
    }

    private static final class PolyglotContext implements ScriptContext {
        private Context context;
        private final ScriptEngineFactory factory;
        private final PolyglotReader in;
        private final PolyglotWriter out;
        private final PolyglotWriter err;
        private Bindings globalBindings;

        PolyglotContext(ScriptEngineFactory factory) {
            this.factory = factory;
            this.in = new PolyglotReader(new InputStreamReader(System.in));
            this.out = new PolyglotWriter(new OutputStreamWriter(System.out));
            this.err = new PolyglotWriter(new OutputStreamWriter(System.err));
        }

        Context getContext() {
            if (context == null) {
                Context.Builder builder = Context.newBuilder(LANGUAGE_ID)
                        .in(this.in)
                        .out(this.out)
                        .err(this.err)
                        .allowAllAccess(true);
                Bindings globalBindings = getBindings(ScriptContext.GLOBAL_SCOPE);
                if (globalBindings != null) {
                    for (Entry<String, Object> entry : globalBindings.entrySet()) {
                        Object value = entry.getValue();
                        if (value instanceof String) {
                            builder.option(entry.getKey(), (String) value);
                        }
                    }
                }
                context = builder.build();
            }
            return context;
        }

        @Override
        public void setBindings(Bindings bindings, int scope) {
            if (scope == ScriptContext.GLOBAL_SCOPE) {
                if (context == null) {
                    globalBindings = bindings;
                } else {
                    throw new UnsupportedOperationException(
                            "Global bindings for Polyglot language can only be set before the context is initialized.");
                }
            } else {
                throw new UnsupportedOperationException("Bindings objects for Polyglot language is final.");
            }
        }

        @Override
        public Bindings getBindings(int scope) {
            if (scope == ScriptContext.ENGINE_SCOPE) {
                return new PolyglotBindings(getContext().getBindings(LANGUAGE_ID));
            } else if (scope == ScriptContext.GLOBAL_SCOPE) {
                return globalBindings;
            } else {
                return null;
            }
        }

        @Override
        public void setAttribute(String name, Object value, int scope) {
            if (scope == ScriptContext.ENGINE_SCOPE) {
                getBindings(scope).put(name, value);
            } else if (scope == ScriptContext.GLOBAL_SCOPE) {
                if (context == null) {
                    globalBindings.put(name, value);
                } else {
                    throw new IllegalStateException("Cannot modify global bindings after context creation.");
                }
            }
        }

        @Override
        public Object getAttribute(String name, int scope) {
            if (scope == ScriptContext.ENGINE_SCOPE) {
                return getBindings(scope).get(name);
            } else if (scope == ScriptContext.GLOBAL_SCOPE) {
                return globalBindings.get(name);
            }
            return null;
        }

        @Override
        public Object removeAttribute(String name, int scope) {
            Object prev = getAttribute(name, scope);
            if (prev != null) {
                if (scope == ScriptContext.ENGINE_SCOPE) {
                    getBindings(scope).remove(name);
                } else if (scope == ScriptContext.GLOBAL_SCOPE) {
                    if (context == null) {
                        globalBindings.remove(name);
                    } else {
                        throw new IllegalStateException("Cannot modify global bindings after context creation.");
                    }
                }
            }
            return prev;
        }

        @Override
        public Object getAttribute(String name) {
            return getAttribute(name, ScriptContext.ENGINE_SCOPE);
        }

        @Override
        public int getAttributesScope(String name) {
            if (getAttribute(name, ScriptContext.ENGINE_SCOPE) != null) {
                return ScriptContext.ENGINE_SCOPE;
            } else if (getAttribute(name, ScriptContext.GLOBAL_SCOPE) != null) {
                return ScriptContext.GLOBAL_SCOPE;
            }
            return -1;
        }

        @Override
        public Writer getWriter() {
            return this.out.writer;
        }

        @Override
        public Writer getErrorWriter() {
            return this.err.writer;
        }

        @Override
        public void setWriter(Writer writer) {
            this.out.writer = writer;
        }

        @Override
        public void setErrorWriter(Writer writer) {
            this.err.writer = writer;
        }

        @Override
        public Reader getReader() {
            return this.in.reader;
        }

        @Override
        public void setReader(Reader reader) {
            this.in.reader = reader;
        }

        @Override
        public List getScopes() {
            return List.of(ScriptContext.ENGINE_SCOPE, ScriptContext.GLOBAL_SCOPE);
        }

        private static final class PolyglotReader extends InputStream {
            private volatile Reader reader;

            public PolyglotReader(InputStreamReader inputStreamReader) {
                this.reader = inputStreamReader;
            }

            @Override
            public int read() throws IOException {
                return reader.read();
            }
        }

        private static final class PolyglotWriter extends OutputStream {
            private volatile Writer writer;

            public PolyglotWriter(OutputStreamWriter outputStreamWriter) {
                this.writer = outputStreamWriter;
            }

            @Override
            public void write(int b) throws IOException {
                writer.write(b);
            }
        }
    }

    private static final class PolyglotCompiledScript extends CompiledScript {
        private final Source source;
        private final ScriptEngine engine;

        public PolyglotCompiledScript(Source src, ScriptEngine engine) {
            this.source = src;
            this.engine = engine;
        }

        @Override
        public Object eval(ScriptContext context) throws ScriptException {
            if (context instanceof PolyglotContext) {
                return ((PolyglotContext) context).getContext().eval(source).as(Object.class);
            }
            throw new UnsupportedOperationException(
                    "Polyglot CompiledScript instances can only be evaluated in Polyglot.");
        }

        @Override
        public ScriptEngine getEngine() {
            return engine;
        }
    }

    private static final class PolyglotBindings implements Bindings {
        private Value languageBindings;

        PolyglotBindings(Value languageBindings) {
            this.languageBindings = languageBindings;
        }

        @Override
        public int size() {
            return keySet().size();
        }

        @Override
        public boolean isEmpty() {
            return size() == 0;
        }

        @Override
        public boolean containsValue(Object value) {
            for (String s : keySet()) {
                if (get(s) == value) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public void clear() {
            for (String s : keySet()) {
                remove(s);
            }
        }

        @Override
        public Set keySet() {
            return languageBindings.getMemberKeys();
        }

        @Override
        public Collection values() {
            List values = new ArrayList<>();
            for (String s : keySet()) {
                values.add(get(s));
            }
            return values;
        }

        @Override
        public Set<Entry<String, Object>> entrySet() {
            Set<Entry<String, Object>> values = new HashSet<>();
            for (String s : keySet()) {
                values.add(new Entry<String, Object>() {
                    @Override
                    public String getKey() {
                        return s;
                    }

                    @Override
                    public Object getValue() {
                        return get(s);
                    }

                    @Override
                    public Object setValue(Object value) {
                        return put(s, value);
                    }
                });
            }
            return values;
        }

        @Override
        public Object put(String name, Object value) {
            Object previous = get(name);
            languageBindings.putMember(name, value);
            return previous;
        }

        @Override
        public void putAll(Map<? extends String, ? extends Object> toMerge) {
            for (Entry<? extends String, ? extends Object> e : toMerge.entrySet()) {
                put(e.getKey(), e.getValue());
            }
        }

        @Override
        public boolean containsKey(Object key) {
            if (key instanceof String) {
                return languageBindings.hasMember((String) key);
            } else {
                return false;
            }
        }

        @Override
        public Object get(Object key) {
            if (key instanceof String) {
                Value value = languageBindings.getMember((String) key);
                if (value != null) {
                    return value.as(Object.class);
                }
            }
            return null;
        }

        @Override
        public Object remove(Object key) {
            Object prev = get(key);
            if (prev != null) {
                languageBindings.removeMember((String) key);
                return prev;
            } else {
                return null;
            }
        }
    }
}
</code></pre>
</details>

私たちとつながってください