言語の埋込み
- 依存性の設定
- ポリグロット・アプリケーションのコンパイルおよび実行
- ゲスト言語関数のJava値としての定義
- Javaからゲスト言語への直接アクセス
- ゲスト言語からJavaへのアクセス
- ゲスト言語からのJava型のルックアップ
- ポリグロット・プロキシを使用して計算された配列
- ホスト・アクセス
- ランタイム最適化のサポート
- ポリグロット・アプリケーションからのネイティブ実行ファイルのビルド
- 複数のコンテキストにわたるコード・キャッシング
- ポリグロット分離
- Javaでのゲスト言語の埋込み
- 多数の言語用のシェルのビルド
- 実行リスナーを使用したステップ実行
- ヒープ・サイズの設定
- JSR-223 ScriptEngineとの互換性
GraalVMポリグロットAPIを使用すると、JVMベースのホスト・アプリケーションにゲスト言語のコードを埋め込んで実行できます。
この項では、GraalVMで実行され、ゲスト言語を直接コールするホスト・アプリケーションをJavaで作成する方法を学習します。各コード例の下にあるタブを使用して、JavaScript、R、RubyおよびPythonのいずれかを選択できます。
ノート: ポリグロット埋込みの使用方法の説明は、GraalVM for JDK 21 (23.1.1)リリースで改訂されました。古いバージョンのGraalVMをまだ使用している場合は、正しいバージョンのドキュメントが表示されていることを確認してください。変更の詳細は、リリース・ノートを参照してください。
依存性の設定
GraalVMポリグロットAPIバージョン23.1.0以降、必要なアーティファクトはすべてMaven Centralから直接ダウンロードできます。埋込みに関連するすべてのアーティファクトは、Maven依存関係グループorg.graalvm.polyglot
にあります。実行可能な完全な例は、GitHubのポリグロット埋込みのデモンストレーションを参照してください。
プロジェクトに配置できるMaven依存関係設定の例を次に示します。
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>polyglot</artifactId>
<version>23.1.1</version>
</dependency>
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<!-- Select language: js, ruby, python, java, llvm, wasm, languages-->
<artifactId>js</artifactId>
<version>23.1.1</version>
<type>pom</type>
</dependency>
<!-- add additional languages if needed -->
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<!-- Select tools: profiler, inspect, coverage, dap, tools -->
<artifactId>tools</artifactId>
<version>23.1.1</version>
<type>pom</type>
</dependency>
<!-- add specific tools if needed -->
言語およびツールの依存関係では、GraalVM Free Terms and Conditions (GFTC)ライセンスを使用します。かわりにコミュニティ・ライセンス・バージョンを使用するには、各アーティファクト(js-community
など)に-community
接尾辞を追加します。ポリグロット分離アーティファクトにアクセスするには、かわりに-isolate
接尾辞(js-isolate
など)を使用します。
アーティファクトpolyglot
およびtools
には、使用可能なすべての言語とツールが依存関係として含まれています。このアーティファクトは、メジャー・リリース間で増減する場合があります。本番デプロイメントに必要な言語のみを選択することをお薦めします。
pom
タイプは、言語またはツールの依存関係に必須です。
また、Javaモジュールを使用する場合、module-info.javaファイルにはorg.graalvm.polyglot
が必要です。
module com.mycompany.app {
requires org.graalvm.polyglot;
}
Truffleランタイム最適化を使用して構成を実行できるかどうかは、使用するGraalVM JDKによって異なります。詳細は、「ランタイム・コンパイル」の項を参照してください。
可能な場合には必ず、モジュールとモジュールパスを使用して、ポリグロット埋込みを構成することをお薦めします。かわりにクラスパスからポリグロットを使用すると、そのクラスパス上のすべてのライブラリで、安全でないAPIへのアクセスが可能になることに注意してください。まだアプリケーションをモジュール化していない場合は、クラスとモジュールパスをハイブリッドで使用できます。たとえば:
$JAVA_HOME/bin/java -classpath=lib --module-path=lib/polyglot --add-modules=org.graalvm.polyglot ...
この例では、lib/polyglot
フォルダに、ポリグロットと言語のjarがすべて含まれている必要があります。クラスパスからポリグロット・クラスにアクセスするには、--add-modules=org.graalvm.polyglot
JVMオプションも指定する必要があります。クラスパス上のnative-imageポリグロット・モジュールを使用している場合は、自動的にそのモジュールパスにアップグレードされます。
Maven Assemblyプラグインを使用するなどして、ポリグロット・ライブラリから単一のuber Jarを作成することは可能ではありますが、お薦めはしません。また、Uber jarは、ネイティブイメージの作成との組合せではサポートされません。
ポリグロット・アプリケーションのコンパイルおよび実行
GraalVMでは、Truffle言語実装フレームワークで実装された任意の言語で記述されたポリグロット・アプリケーションを実行できます。これらの言語は、以降、ゲスト言語と呼びます。
この項のステップを実行して、GraalVMで実行され、プログラミング言語の相互運用性を示すサンプルのポリグロット・アプリケーションを作成します。
-
Mavenを使用して新しいJavaプロジェクトを作成します。
- polyglot-embedding-demoリポジトリをクローニングします。
git clone https://github.com/graalvm/polyglot-embedding-demo.git
-
コード例を「メイン・クラス」に挿入します。
-
GraalVM JDKを指すように
JAVA_HOME
環境変数を設定して、GraalVMをダウンロードして設定します。 mvn package exec:exec
を実行して、サンプル・コードをビルドおよび実行します。
これで、GraalVMで実行されるJavaホスト・アプリケーションとゲスト言語コードで構成されたポリグロット・アプリケーションが作成されました。このアプリケーションを他のコード例で使用して、ポリグロット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
コールは、結果をJavaint
に変換します。- 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
は、3つのメンバー(id
という数値、text
という文字列およびarr
という配列)を含むオブジェクトです。- 最初のアサーションは、戻り値にメンバーを含めることができることを検証し、これは値がオブジェクトのような構造であることを示します。
id
変数は、結果のオブジェクトからid
という名前のメンバーを読み取ることによって初期化されます。結果は、asInt()
を使用してJavaint
に変換されます。- 次のアサーションは、結果が
42
という値であることを検証します。 text
変数は、メンバーtext
の値を使用して初期化され、これもasString()
を使用してJavaString
に変換されます。- 次のアサーションは、結果値がJava
String
"42"
と等しいことを検証します。 - 次に、配列を保持する
arr
メンバーが読み取られます。 - 配列は、
hasArrayElements
に対してtrue
を返します。Rの配列インスタンスは、メンバーと配列要素を同時に持つことができます。 - 次のアサーションは、配列のサイズが3に等しいことを検証します。ポリグロットAPIは大きい配列をサポートしているため、配列長は
long
型になります。 - 最後に、インデックス
1
の配列要素が42
と等しいことを検証します。インデックスが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
には、4つのpublicフィールドid
、text
、arr
およびret42
があります。フィールドは、常に42
のint
値を戻す42
、"42"
、new int[]{1, 42, 3}
およびラムダ() -> 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
と比較されます。 - ゲスト言語スクリプトの実行後、検証が実行され、スクリプトが結果として
true
のboolean
値を返すことが確認されます。
ゲスト言語からの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
という名前の変数に格納します。 - staticメソッド
BigDecimal.valueOf(long)
が起動され、値10
を使用して新しいBigDecimal
が作成されます。static Javaメソッドをルックアップする以外に、戻されたJava型を直接インスタンス化する(たとえば、JavaScriptではnew
キーワードを使用して)こともできます。 20
を指定したpow
インスタンス・メソッドを起動して10^20
を計算するために、新しい小数が使用されます。- スクリプトの結果は、
asHostObject()
をコールしてホスト・オブジェクトに変換されます。戻り値は、自動的にBigDecimal
型にキャストされます。 - 結果の10進文字列は、
"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
が戻されます。- メイン・メソッドで、新しいポリグロット実行コンテキストを作成します。
ComputedArray
クラスの新しいインスタンスがarr
という名前を使用してエクスポートされます。- ゲスト言語スクリプトによって
arr
シンボルがインポートされ、これにより、エクスポートされたプロキシが戻されます。 - 2番目の要素と
1000000000
番目の要素がアクセスされ、合計されてから戻されます。Rなどの1ベースの言語の配列インデックスは、プロキシ配列の0ベースのインデックスに変換されることに注意してください。 - 言語スクリプトの結果は、long値として戻されて検証されます。
ポリグロット・プロキシ・インタフェースの詳細は、ポリグロットAPIのJavaDocを参照してください。
ホスト・アクセス
ポリグロットAPIでは、デフォルトで、ファイルI/Oなどの特定の重要な機能へのアクセスが制限されます。これらの制限は、allowAllAccess
をtrue
に設定することで完全に解除できます。
ノート: 現在、アクセス制限はJavaScriptでのみサポートされています。
ホスト機能へのアクセスの制御
ホストへのゲスト・アプリケーションのアクセスを制限することが望ましい場合があります。たとえば、System.exit
をコールするJavaメソッドが公開されている場合、ゲスト・アプリケーションはホスト・プロセスを終了できます。メソッドが誤って公開されることを回避するために、ホスト・アクセスはデフォルトでは許可されておらず、すべてのpublicメソッドまたはフィールドには明示的に@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
メソッドは、最初にデフォルト構成で新しいポリグロット・コンテキストを作成し、@HostAccess.Export
注釈が付けられたメソッドを除いてホスト・アクセスを禁止します。- 新しい
Services
インスタンスが作成され、グローバル変数services
としてコンテキストに配置されます。 - 最初に評価されるスクリプトは、servicesオブジェクトを使用して新しい従業員を作成し、その名前を戻します。
- 戻された名前は、予期された名前
John Doe
と等しいとアサートされます。 - servicesオブジェクトで
exitVM
メソッドをコールする2番目のスクリプトが評価されます。これは、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
を使用して、ホスト型へのホスト・ルックアップを許可およびカスタマイズします。ゲスト・アプリケーションが、ルックアップ述語によって許可されるホスト・アプリケーション・クラスの検索を許可します。たとえば、ArrayListがclassFilter
によって許可リストに登録され、ホスト・アクセス・ポリシーcontext.eval("js", "var array = Java.type('java.util.ArrayList')")
によってアクセスが許可されている場合、JavascriptコンテキストでJava ArrayListを作成できますallowHostClassLoading
を使用して、ホスト・クラスのロードを許可します。ホスト・アクセス・ポリシーによってクラスへのアクセス権が付与されている場合にのみ、クラスにアクセスできます。allowCreateThread
を使用して、スレッドの作成を許可します。allowNativeAccess
を使用して、ネイティブAPIへのアクセスを許可します。allowIO
を使用してIOへのアクセスを許可し、fileSystem
を使用してプロキシ・ファイル・アクセスを許可します。
ノート: クラスのロード、ネイティブAPIまたはホストI/Oへのアクセス権を付与すると、これらの権限を使用して他のアクセス制限をバイパスできるため、実質的にすべてのアクセス権が付与されます。
ランタイム最適化のサポート
ポリグロット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
を使用するように参照する必要があります。コンパイラのjarは、モジュールまたはクラス・パスに配置しない必要があります。MavenまたはGradleを使用した構成例は、ポリグロット埋込みデモンストレーションを参照してください。
フォールバック・エンジンへの切替え
たとえば、簡単なスクリプトのみを実行したり、リソースに制約のあるシステムで実行したりする必要が生じた場合、ランタイム最適化を行わずにフォールバック・エンジンに切り替えることができます。ポリグロット・バージョン23.1以降、フォールバック・エンジンは、クラスまたはモジュール・パスからtruffle-runtime
およびtruffle-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://www.graalvm.org/latest/reference-manual/embed-languages/.
To disable this warning use the '--engine.WarnInterpreterOnly=false' option or the '-Dpolyglot.engine.WarnInterpreterOnly=false' system property.
このメッセージは、追加ステップとして示されたオプションを使用して無効にできます。
これらの依存関係を削除すると、ネイティブ・イメージ・ビルドのフォールバック・エンジンに自動的に切り替わります。
ポリグロット・アプリケーションからのネイティブ実行ファイルのビルド
GraalVM for JDK 21上のポリグロット・バージョン23.1では、ネイティブ・イメージを使用して埋込みポリグロット言語ランタイムでイメージを構築するために特別な構成は必要ありません。他のJava依存関係と同様に、ネイティブ実行可能ファイルを構築する場合、ポリグロット言語JARファイルはクラスまたはモジュール・パスにある必要があります。MavenまたはGradleネイティブ・イメージ・プラグインを使用して、native-image
ビルドを構成することをお薦めします。ネイティブ・イメージのMavenおよびGradle構成の例は、ポリグロット埋込みデモンストレーション・リポジトリにあります。
Mavenプロファイル構成の例は次のとおりです。
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.9.25</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=path/to/resources
オプションを使用してルックアップの場所をカスタマイズできます。リソースの作成を無効にするには、-H:-CopyLanguageResources
ビルド時オプションを使用できます。一部の言語では、リソース・フォルダがないと実行がサポートされない場合があります。
ポリグロット・バージョン23.1では、-Dorg.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);
}
}
}
次のコードをreflect.json
にコピーします:
[
{ "name": "AccessJavaFromJS$MyClass", "allPublicFields": true },
{ "name": "java.util.concurrent.Callable", "allPublicMethods": true }
]
これで、ホスト・アクセスをサポートするネイティブ実行可能ファイルを作成し、追加の-H:ReflectionConfigurationFiles=reflect.json
ビルド時オプションを追加できます。
複数のコンテキストにわたるコード・キャッシング
GraalVMポリグロットAPIでは、複数のコンテキストにわたるコード・キャッシングが可能です。コード・キャッシングを使用すると、コンパイルされたコードを再利用でき、ソースの解析を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"のソース・オブジェクトを作成します。明示的なSource
オブジェクトを使用して、"js"言語(JavaScriptの言語識別子)でコード・キャッシュがコンテキスト間でガベージ・コレクションされないようにします。Context.newBuilder().engine(engine).build()
では、明示的なエンジンが割り当てられた新しいコンテキストを構築します。1つのエンジンに関連付けられているすべてのコンテキストでコードが共有されます。context.eval(source).asInt()
は、ソースを評価し、結果をValue
インスタンスとして戻します。
重要: 実行中のコンテキスト間でキャッシュされたソースのコード・キャッシュを存続させるには、アプリケーションでSource
オブジェクトが継続的に参照されていることを確認する必要があります。ポリグロット・ランタイムは、次のGCサイクルで参照されなくなったソースのキャッシュされたコードを収集できます。
コード・キャッシュの管理
コード・キャッシュのデータは、Engine
インスタンスの一部として格納されます。2つの別々のエンジン・インスタンス間でコード共有が発生することはありません。したがって、グローバル・コード・キャッシュが必要な場合は、シングルトンEngine
インスタンスを使用することをお薦めします。コンテキストとは対照的に、エンジンは常に複数のスレッドで共有できます。コンテキストを複数のスレッド間で共有できるかどうかは、使用される言語によって異なります。
コード・キャッシュをパージする明示的な方法はありません。ガベージ・コレクタは、次の収集でこれを自動的に行います。エンジンのコード・キャッシュは、引き続きエンジンへの強い参照があり、エンジンがクローズされていないかぎり収集されません。また、関連付けられたコードが収集されないように、Source
インスタンスを存続させる必要があります。ソース・インスタンスが参照されなくなったが、エンジンがまだ参照されている場合、ソース・オブジェクトに関連付けられたコード・キャッシュはGCによって収集される可能性があります。したがって、Source
をキャッシュされたままにするかぎり、Source
オブジェクトへの強い参照を維持することをお薦めします。
要約すると、コード・キャッシュは、Engine
およびSource
オブジェクトへの強い参照を維持および保持することで制御できます。
ポリグロット分離
Oracle GraalVMでは、専用のネイティブ・イメージ分離で実行するようにポリグロット・エンジンを構成できます。このモードのポリグロット・エンジンは、専用のガベージ・コレクタおよびJITコンパイラを使用して、VMレベルのフォルト・ドメイン内で実行されます。ポリグロット分離は、ポリグロット・サンドボックス化に役立ちます。分離された言語の実行は、HotSpotおよびネイティブ・イメージ・ホスト仮想マシンで動作します。
ポリグロット分離として使用される言語は、Maven Centralから-isolate
接尾辞を使用してダウンロードできます。たとえば、分離されたJavaScriptへの依存関係を構成するには、次のようなMaven依存関係を追加します。
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>polyglot</artifactId>
<version>23.1.0</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>js-isolate</artifactId>
<version>23.1.0</version>
<type>pom</type>
</dependency>
ダウンロードされた依存関係はプラットフォームに依存せず、プラットフォームごとにネイティブイメージが含まれています。将来のリリースでは、個々のプラットフォームのポリグロット分離ネイティブ・イメージのダウンロードをサポートする予定です。
ポリグロットAPIでの分離使用を有効にするには、構築時に--engine.SpawnIsolate=true
オプションをEngine
またはContext
に渡す必要があります。オプションengine.SpawnIsolate
は、Oracle GraalVM以外のJDKで使用されている場合は使用できません。
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
オプションを使用して、ハードウェア・レベルでポリグロット分離のヒープを分離できます。このオプションを使用してエンジンを作成すると、分離されたエンジンのヒープに専用の保護キーが割り当てられます。GraalVMによって、エンジンのヒープへのアクセスが有効になるのは、ポリグロット分離のコードの実行時のみです。
Javaでのゲスト言語の埋込み
GraalVM Polyglot APIは、Javaの相互運用性を利用して、ゲスト言語内から使用できます。これは、スクリプトを親コンテキストから分離して実行する必要がある場合に役立ちます。ホスト言語としてのJavaでは、Context.eval(Source)
へのコールはValue
のインスタンスを返しますが、このコードをゲスト言語の一部として実行しているため、かわりに言語固有の相互運用性APIを使用できます。そのため、言語の標準値など、言語内に作成されたコンテキストによって返される値を使用できます。次の例では、value.getMember("data")
ではなくvalue.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スニペットを評価します。- 最初のJSスクリプト行
inner = Java.type('org.graalvm.polyglot.Context').create()
は、Javaホスト・タイプのコンテキストを検索し、権限のない新しい内部コンテキスト・インスタンスを作成します(デフォルト)。 inner.eval('js', '({data:42})');
は、内部コンテキストでJavaScriptコード({data:42})
を評価し、結果を格納します。"value.data"
この行は、内部コンテキストの結果からメンバーdata
を読み取ります。この結果は、内部コンテキストがまだクローズされていない間のみ読み取ることができることに注意してください。context.eval("js", "c.close()")
このスニペットは、内部コンテキストを閉じます。内部コンテキストは手動でクローズする必要があり、親コンテキストで自動的にはクローズされません。- 最後に、この例では
Valid true
がコンソールに出力されることが予想されます。
多数の言語用のシェルのビルド
GraalVMポリグロット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ポリグロットAPIでは、ユーザーはExecutionListenerクラスを使用してゲスト言語の実行をインストゥルメントできます。たとえば、ゲスト言語プログラムのすべての文に対して起動される実行リスナーをアタッチできます。実行リスナーはポリグロットの埋込み機能用の単純な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実装は提供されません。ポリグロットAPIは、Truffleの機能をより詳細に制御できるため、設定の多くを直接制御し、GraalVMのより詳細なセキュリティ設定の恩恵を受けるために、org.graalvm.polyglot.Context
インタフェースを使用することを強くお薦めします。
ただし、ScriptEngine APIを使用して統合される他のスクリプト言語の代替としてTruffle言語を簡単に評価するには、次の単一ファイル・スクリプト・エンジンを提供します。このファイルは、ソース・ツリーにドロップして、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
をクローズすることです。クローズするには、ScriptEngine
にclose()
メソッドがないため、((AutoCloseable) scriptEngine).close();
を使用します。
Graal.jsは、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.Collections;
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.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 {
private final ScriptEngineFactory factory;
private PolyglotContext defaultContext;
PolyglotEngine(ScriptEngineFactory factory) {
this.factory = factory;
this.defaultContext = new PolyglotContext(factory);
}
@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);
} 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;
}
}
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);
for (Entry<String, Object> entry : getBindings(ScriptContext.GLOBAL_SCOPE).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);
}
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