よくある質問
次に、GraalVMで実行されているJavaScriptに関して最もよくある質問とその回答を示します。
互換性
GraalVMはJavaScript言語と互換性がありますか。
GraalVMはECMAScript 2022仕様と互換性があり、さらに2023ドラフト仕様に沿って開発されています。GraalVMのJavaScriptランタイムの互換性は、Kangax ECMAScript互換性表などの外部ソースによって検証されています。
GraalVM JavaScriptは、ECMAScriptの公式テスト・スイートであるtest262だけでなく、V8とNashornによって公開されたテスト、Node.jsユニット・テスト、GraalVM独自のユニット・テストなど、一連のテスト・エンジンに対してテストされています。
GraalVMでサポートされているJavaScript APIのリファレンスは、GRAAL.JS-APIを参照してください。
GraalVMは元のノード実装と互換性がありますか。
GraalVMに基づくNode.jsは、(V8エンジンに基づく)元のNode.jsとほぼ互換性があります。これにより、npmベースの多数のモジュールがGraalVMと互換性を持つようになります。実際、テストした100k npmモジュールのうち、94%以上がすべてのテストに合格しています。それでも、いくつかの相違の要因を考慮する必要があります:
- 設定: GraalVMでは、ほとんどの場合、
node
実行可能ファイルやnpm
など、ノードの元の設定が模倣されます。ただし、すべてのコマンドライン・オプションがサポートされているわけではありません(またはまったく同じ動作をするわけではありません)。モジュールでは、v8.hファイルに対してネイティブ・モジュールを(再)コンパイルする必要がある場合があります。
GraalVM 21.1以降、Node.jsおよび関連するすべての実行可能ファイル(node
、npm
など)は、デフォルトでGraalVMバイナリに含まれていません。Node.jsサポートは別のコンポーネントにパッケージ化されています。これは、GraalVMアップデータで$JAVA_HOME/bin/gu install nodejs
を使用してインストールできます。
-
内部: GraalVMはJVM上に実装されるため、V8に基づくNode.jsとは異なる内部アーキテクチャを持ちます。これは、一部の内部メカニズムの動作が異なり、V8の動作を正確にレプリケートできないことを意味します。これはユーザー・コードにはほとんど影響しませんが、V8内部に応じて、ネイティブに実装されたモジュールに影響する可能性があります。
-
パフォーマンス: GraalVMはJVM上に実装されるため、パフォーマンス特性は元のネイティブ実装とは異なります。GraalVMのピーク・パフォーマンスは多くのベンチマークでV8と一致しますが、通常はピークに達するまでに時間がかかります(ウォームアップと呼ばれます)。(ピーク)パフォーマンスを測定するときは、必ずGraalVMコンパイラに対していくらか余分な時間を確保してください。
-
互換性: GraalVMでは、Node.jsコードとの互換性のチェックと維持に次のアプローチが使用されます:
- node-compat-table: node-compat-tableモジュールを使用してGraalVMと他のエンジンが比較され、Node.jsコードを破損させる可能性のある非互換性が明らかにされます。
- mochaを使用したモジュールの自動一括テスト: 大規模なモジュール・セットをテストするために、GraalVMはmochaテスト・フレームワークを使用する95kモジュールに対してテストされます。mochaを使用すると、テストの実行プロセスを自動化し、テスト結果を理解できます。
- 一般的なモジュールの手動テスト: npmモジュールの選択リストは手動テスト設定でテストされます。これらの関連性の高いモジュールは、より高度な方法でテストされます。
以前にNashornで実行していたアプリケーションはGraalVM JavaScriptでは動作しないのですか。
理由:
- GraalVM JavaScriptでは、ECMAScript仕様および競合するエンジン(Nashornを含む)との互換性確保が試みられます。場合によっては、要件が矛盾することもあり、その場合はECMAScriptが優先されます。また、セキュリティ上の理由などにより、GraalVM JavascriptでNashorn機能の正確なレプリケーションが意図的に行われない場合もあります。
解決策:
- 多くの場合、GraalVMのNashorn互換性モードを有効にすると、デフォルトでは無効になっている機能が有効になります。ただし、これはアプリケーションのセキュリティに悪影響を及ぼす可能性があります。詳細は、Nashorn移行ガイドを参照してください。
特定のアプリケーション:
- JSR 223 ScriptEngineでは、Nashorn互換性モードを使用するためにシステム・プロパティ
polyglot.js.nashorn-compat
をtrue
に設定する必要がある場合があります。 ant
では、ScriptEngineを介してGraalVM JavaScriptを使用する場合にANT_OPTS="-Dpolyglot.js.nashorn-compat=true" ant
を使用します。
array.map()
やfn.apply()
などの組込み関数は、JavaのProxyArray
などのJavaScript以外のオブジェクトでは使用できません
理由:
- JavaScriptに渡されるJavaオブジェクトは、対応するJavaScript要素に可能なかぎり近いものとして扱われます。たとえば、JavaScriptに渡されるJava配列は、可能なかぎりJavaScript Arrayエキゾチック・オブジェクト(JavaScript配列)のように扱われます。関数についても同様です。明らかな違いは、このようなオブジェクトのプロトタイプが
null
であることです。つまり、たとえば、length
を読み取ったり、JavaScriptコードでJava配列の値を読み取ったり書き込むことはできますが、Array.prototype
がデフォルトでは提供されていないことから、sort()
をコールすることはできません。
解決策:
- オブジェクトにはプロトタイプのメソッドが割り当てられていませんが、
Array.prototype.call.sort(myArray)
のように明示的にコールすることは可能です。 - オプション
js.foreign-object-prototype
が提供されています。有効にすると、JavaScript側のオブジェクトが最上位プロトタイプ(Array.prototype
、Function.prototype
、Object.prototype
など)のセットを取得し、結果として各型のネイティブJavaScriptオブジェクトと同様に動作するようになります。ここでは、通常のJavaScript優先順位ルールが適用されます。たとえば、独自のプロパティ(このケースではJavaオブジェクトのプロパティ)が優先され、プロトタイプのプロパティは非表示になります。
Array.prototype
などのJavaScript組込み関数は各Java型に対してコールできますが、これらの関数ではJavaScriptセマンティクスが要求されることに注意してください。そのため、たとえば、Javaでサポートされていない操作は失敗する可能性があります(通常はTypeError
: Message not supported
が表示されます)。Array.prototype.push
を例として考えます: 配列のサイズはJavaScriptでは大きくなりますが、Javaでは固定であるため、値のプッシュは意味的に不可能であり、失敗します。このような場合は、Javaオブジェクトをラップして、そのケースを明示的に処理できます。そのためには、インタフェースProxyObject
およびProxyArray
を使用します。
アプリケーションでのGraalVMの動作を検証するにはどうすればよいですか。
モジュールにテストが付属している場合は、それらをGraalVMで実行します。もちろん、テストされるのはアプリケーションのみであり、依存性はテストされません。互換性ツールを使用して、対象のモジュールがGraalVMでテストされているかどうか、またテストに合格したかどうかを確認できます。また、package-lock.json
またはpackage.json
ファイルをそのツールにアップロードすると、すべての依存性が一度に分析されます。
パフォーマンス
GraalVM JavaScriptでは別のエンジンよりもアプリケーションの速度が低下します
理由:
- ベンチマークでウォームアップが考慮されていることを確認してください。最初の数回の反復では、GraalVM JavaScriptはネイティブに実装されたエンジンよりも低速ですが、ピーク・パフォーマンスに到達すれば、この差異はなくなるはずです。
- GraalVM JavaScriptは、
native
(デフォルト)とJVM
の2種類のモードで出荷されます。デフォルトのnative
では高速起動が実現されますが、アプリケーションのウォームアップが完了すると、ピーク・パフォーマンスが低下する可能性があります。JVM
モードでは、アプリケーションの起動が数百ミリ秒遅くなる場合がありますが、通常、ピーク・パフォーマンスは向上します。 - コードを繰り返し実行するとき、新たに作成される
org.graalvm.polyglot.Context
を使用すると、実行されるコードは毎回同じですが時間がかかります。
解決策:
- ベンチマークで適切なウォームアップを使用し、アプリケーションがまだウォームアップしている最初の数回の反復を無視します。
--jvm
オプションを使用すると、起動速度は低下しますが、ピーク・パフォーマンスは向上します。- パフォーマンスを低下させる可能性のあるフラグが設定されていないことを再確認します(例:
-ea
/-esa
)。 - 問題を根本原因まで切り詰め、GraalVMチームが確認できるように問題提起します。
org.graalvm.polyglot.Context
でコードを実行するときは、1つのorg.graalvm.polyglot.Engine
オブジェクトを共有して、新たに作成される各Context
に渡すようにします。org.graalvm.polyglot.Source
オブジェクトを使用し、可能な場合はキャッシュします。これで、GraalVMではすでにコンパイルされたコードをコンテキスト間で共有できるようになり、パフォーマンスが向上します。詳細および例は、リファレンス・マニュアルの「複数のコンテキストにわたるコード・キャッシング」を参照してください。
最高のピーク・パフォーマンスを実現するにはどうすればよいですか。
次に示すいくつかのヒントに従えば、ピーク・パフォーマンスを分析して改善できます:
- ピーク・パフォーマンスを測定する際には、測定を開始する前に、GraalVMコンパイラによってすべてのホット・メソッドがコンパイルされるように十分な時間を確保してください。その際の便利なコマンドライン・オプションは
--engine.TraceCompilation=true
です。これを使用すると、(JavaScript)メソッドがコンパイルされるたびにメッセージが出力されます。出力が頻繁に続いている間は、測定をまだ開始しないでください。 - 可能な場合は、ネイティブ・イメージとJVMモードのパフォーマンスを比較してください。アプリケーションの特性によっては、いずれか一方が高いピーク・パフォーマンスを示す場合があります。
- ポリグロットAPIには、アプリケーションのパフォーマンスを検査するためのツールとオプションがいくつか用意されています:
--cpusampler
および--cputracer
を使用すると、アプリケーションの終了時に最上位ホット・メソッドのリストが出力されます。そのリストを使用して、アプリケーションで最も時間が費やされている場所を特定します。--experimental-options --memtracer
は、アプリケーションのメモリー割当てを把握する場合に役立ちます。詳細は、プロファイリング・コマンドライン・ツールのリファレンスを参照してください。
ネイティブ・イメージでのGraalVMのJavaScriptの実行は、JVMと比較してどのように違いますか。
基本的に、GraalVMのJavaScriptエンジンはプレーンJavaアプリケーションです。任意のJVM (JDK 11以上)で実行できますが、より適切な結果を得るには、GraalVMか、またはGraalVMコンパイラを使用した互換性のあるJVMCI対応のJDKにする必要があります。このモードでは、実行時にJavaScriptエンジンにJavaへの完全なアクセス権が付与されますが、他のJavaアプリケーションと同様に、実行時にJVMで最初(Just-in-Time)にJavaScriptエンジンをコンパイルする必要もあります。
ネイティブ・イメージで実行することは、JavaScriptエンジンがJDKなどのすべての依存性を含めてネイティブ実行可能ファイルにプリコンパイルされることを意味します。これにより、GraalVM自体を最初にコンパイルする必要がなくなり、JavaScriptコードのコンパイルをすぐに開始できるため、JavaScriptアプリケーションの起動が大幅に高速化されます。ただし、このモードでは、イメージ作成時に認識されているJavaクラスへのアクセス権のみがGraalVMに付与されます。この場合、JavaScriptとJavaの相互運用性機能は、実行時に動的なクラスのロードと任意のJavaコードの実行を必要とするため、このモードでは使用できないことに留意してください。
npmパッケージはグローバルにインストールできますか。
ノード・パッケージは、元のNode.js実装とGraalVMの両方で、npm
および-g
オプションを使用してグローバルにインストールできます。
元のNode.js実装には、バイナリおよびグローバルにインストールされたパッケージとそのコマンドライン・ツールを配置するための1つのメイン・フォルダ(NODE/bin
)がありますが、GraalVMには、メインのGRAALVM/bin
フォルダとは別に、言語ごとの個別フォルダ(GRAALVM/jre/languages/js/bin
など)がいくつかあります。npmパッケージをGraalVMにグローバルにインストールすると、コマンドライン・インタフェース・ツールなどの実行可能ファイルへのリンクがJavaScript固有のフォルダに配置されます。グローバルにインストールされたパッケージを正しく機能させるには、$PATH
にGRAALVM/jre/languages/js/bin
を追加する必要がある場合があります。
もう1つのオプションは、$PREFIX
環境変数を設定するか、npm install
の実行時に--prefix
オプションを指定することで、npm
のグローバル・インストール・フォルダを指定することです。
詳細は、「npm
パッケージのグローバル・インストール」を参照してください。
エラー
TypeError: Access to host class com.myexample.MyClass is not allowed or does not exist
理由:
js
またはnode
プロセスで認識されていないか、またはコードによるアクセスが許可されたクラスの中にないJavaクラスにアクセスしようとしています。
解決策:
- クラス名にタイプミスがないことを確認します。
- クラスがクラスパス上にあることを確認します。ランチャの
--vm.cp=<classpath>
オプションを使用します。 - クラスの
@HostAccess.Export
またはContext.Builder.allowHostAccess()
(あるいはその両方)を許容設定に設定して、クラスへのアクセスが許可されるようにします。JavaDoc of org.graalvm.polyglot.Contextを参照してください。
TypeError: UnsupportedTypeException
TypeError: execute on JavaObject[Main$$Lambda$63/1898325501@1be2019a (Main$$Lambda$63/1898325501)] failed due to: UnsupportedTypeException
理由:
- GraalVM JavaScriptでは、JavaScriptからのJavaのコール時に具体的なコールバック・タイプが許可されない場合があります。
Value
オブジェクトなどを必要とするJava関数は、それに起因して失敗し、引用符で囲まれたエラー・メッセージが表示されることがあります。
解決策:
- Javaコールバック・メソッドの署名を変更します。
ステータス:
- これは既知の制限であり、将来のバージョンで解決される予定です。
例:
import java.util.function.Function;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.HostAccess;
public class Minified {
public static void main(String ... args) {
//change signature to Function<Object, String> to make it work
Function<Value, String> javaCallback = (test) -> {
return "passed";
};
try(Context ctx = Context.newBuilder()
.allowHostAccess(HostAccess.ALL)
.build()) {
Value jsFn = ctx.eval("js", "f => function() { return f(arguments); }");
Value javaFn = jsFn.execute(javaCallback);
System.out.println("finished: "+javaFn.execute());
}
}
}
TypeError: Message not supported
TypeError: execute on JavaObject[Main$$Lambda$62/953082513@4c60d6e9 (Main$$Lambda$62/953082513)] failed due to: Message not supported.
理由:
- このオブジェクトでは処理されないポリグロット・オブジェクトに対して操作(メッセージ)を実行しようとしています。たとえば、実行不可能なオブジェクトに対して
Value.execute()
をコールしています。 - セキュリティ設定(
org.graalvm.polyglot.HostAccess
など)により、操作が妨げられる場合があります。
解決策:
- 該当するオブジェクト(型)でそれぞれのメッセージが処理されることを確認します。
- 具体的には、Java型に対して実行しようとしているJavaScript操作がJavaで意味的に可能であることを確認します。たとえば、JavaScriptの配列に値を
プッシュ
して自動的に配列を拡張することはできますが、Javaの配列は固定長であり、それらにプッシュしようとすると、Message not supported
というエラーが発生します。このような場合は、ProxyArray
などとしてJavaオブジェクトをラップできます。 - クラスの
@HostAccess.Export
またはContext.Builder.allowHostAccess()
(あるいはその両方)を許容設定に設定して、クラスへのアクセスが許可されるようにします。JavaDoc of org.graalvm.polyglot.Contextを参照してください。 - Javaラムダ式または関数型インタフェースをコールしようとしていますか。適切なメソッドに
@HostAccess.Export
の注釈を付けると、問題が発生する可能性があります。関数型インタフェースから参照されるメソッドに注釈を付けることはできますが、インタフェース自体(またはバックグラウンドで作成されたラムダ・クラス)には適切に注釈を付けられず、これらはエクスポートされたものとして認識されません。問題と実用的な解決策を取り上げた例は、後述の説明を参照してください。
特定のHostAccess
設定(HostAccess.EXPLICIT
など)を使用してMessage not supported
というエラーをトリガーする例:
{
...
//a JS function expecting a function as argument
Value jsFn = ...;
//called with a functional interface as argument
jsFn.execute((Function<Integer, Integer>)this::javaFn);
...
}
@Export
public Object javaFn(Object x) { ... }
@Export
public Callable<Integer> lambda42 = () -> 42;
前述の例では、メソッドjavaFn
には@Export
という注釈が付けられていますが、jsFn
に渡される関数型インタフェースには付けられていません。関数型インタフェースはjavaFn
を囲むラッパーのように動作し、結果として注釈が非表示になるためです。lambda42
にも適切に注釈が付けられていません。このパターンでは、生成されたラムダ・クラスの実行可能関数ではなく、フィールドlambda42
に注釈が付けられます。
@Export
という注釈を関数型インタフェースに追加するには、かわりに次のパターンを使用します:
import java.util.function.Function;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.HostAccess;
public class FAQ {
public static void main(String[] args) {
try(Context ctx = Context.newBuilder()
.allowHostAccess(HostAccess.EXPLICIT)
.build()) {
Value jsFn = ctx.eval("js", "f => function() { return f(arguments); }");
Value javaFn = jsFn.execute(new MyExportedFunction());
System.out.println("finished: " + javaFn.execute());
}
}
@FunctionalInterface
public static class MyExportedFunction implements Function<Object, String> {
@Override
@HostAccess.Export
public String apply(Object s) {
return "passed";
}
};
}
もう1つのオプションは、java.function.Function
のapply
メソッドへのアクセスを許可することです。ただし、これにより、このインタフェースのすべてのインスタンスにアクセス可能になることに注意してください。ほとんどの本番設定では、これは許容範囲を超えており、潜在的なセキュリティ・ホールを開くことになります。
HostAccess ha = HostAccess.newBuilder(HostAccess.EXPLICIT)
//warning: too permissive for use in production
.allowAccess(Function.class.getMethod("apply", Object.class))
.build();
警告: 実装は実行時コンパイルをサポートしていません。
次の警告が表示された場合は、GraalVM、またはGraalVMコンパイラを使用するJVMCI対応JVMで実行していません:
[engine] WARNING: The polyglot context is using an implementation that does not support runtime compilation.
The guest application code will therefore be executed in interpreted mode only.
Execution only in interpreted mode will strongly impact the guest application performance.
To disable this warning the '--engine.WarnInterpreterOnly=false' option or use the '-Dpolyglot.engine.WarnInterpreterOnly=false' system property.
これを解決するには、GraalVMを使用するか、互換性のあるJVMCI対応ストックJDKでGraalコンパイラを設定する手順についてストックJDKでのGraalVM JavaScriptの実行に関するガイドを参照してください。
ただし、これが意図的である場合は、コマンドラインまたはContext.Builder
を使用して前述のオプションを設定することで、警告を無効にして引き続き実行できます(パフォーマンスは低下します)。例:
try (Context ctx = Context.newBuilder("js")
.option("engine.WarnInterpreterOnly", "false")
.build()) {
ctx.eval("js", "console.log('Greetings!');");
}
明示的なポリグロット・エンジンを使用している場合は、Engine
にオプションを設定する必要があります。例:
try (Engine engine = Engine.newBuilder()
.option("engine.WarnInterpreterOnly", "false")
.build()) {
try (Context ctx = Context.newBuilder("js").engine(engine).build()) {
ctx.eval("js", "console.log('Greetings!');");
}
}