Javaの相互運用性

GraalVMにはJavaScript言語実行ランタイムが含まれており、Javaコードとの相互運用が可能です。このドキュメントでは、JavaScriptとJavaの相互運用性機能とその使用方法について説明します。

GraalVMパブリックAPIのリファレンスは、「JavaScriptの互換性」を参照してください。Javaホスト・アプリケーションからJavaScriptなどのゲスト言語と対話する方法については、埋込みリファレンスを参照してください。ポリグロット・プログラミング・ガイドは、その領域の追加のヘルプを提供できます。RhinoおよびNashornの移行ガイドも用意されています。

JavaScriptとNode.jsはどちらも、GraalVMの個別にインストール可能なコンポーネントです。GraalVMアップデータ・ツールguを使用してJavaScriptおよびNode.jsをインストールする方法の詳細は、READMEを参照してください。インストールが成功すると、$GRAALVM/binディレクトリにあるそれぞれのネイティブ・ランチャjsおよびnodeを使用できます。

他のビルドも可能ですが、次の各例では、この設定が使用されていることを前提としています。

Javaの相互運用性の有効化

GraalVMでは、jsおよびnodeランチャは、デフォルトで事前コンパイル済ネイティブ・モードで起動されます。このモードでは、Javaの相互運用性は使用できません。

Javaの相互運用性を有効にするには、ネイティブ・ランチャに--jvmオプションを渡す必要があります。これにより、GraalVM JavaScriptは従来のJVMで実行され、Javaの完全な相互運用性が実現されます。

クラスパス

Javaクラスをロードするには、それらをJavaクラスパスに含める必要があります。クラスパスは、--vm.classpath=<classpath>オプション(または短縮形の--vm.cp=<classpath>)を使用して指定できます:

node --jvm --vm.cp=/my/class/path
js --jvm --vm.cp=/my/class/path

Java.addToClasspath()メソッドを使用すると、実行時にプログラムでクラスパスに追加できます。

ポリグロット・コンテキスト

Java相互運用性サポートを使用してGraalVM JavaScriptを起動するには、ポリグロットContextを使用することをお薦めします。その際には、hostAccessオプション(アクセス許可に使用)とhostClassLookup述語(アクセスを許可するJavaクラスの定義に使用)に基づいて、新しいorg.graalvm.polyglot.Contextが構築されます:

Context context = Context.newBuilder("js")
    .allowHostAccess(HostAccess.ALL)
    //allows access to all Java classes
    .allowHostClassLookup(className -> true)
    .build();
context.eval("js", jsSourceCode);

Javaホスト・アプリケーションからJavaScriptなどのゲスト言語と対話する方法については、埋込みリファレンスを参照してください。ポリグロット・プログラミング・ガイドは、その領域のヘルプも提供できます。

ScriptEngine (JSR 223)

org.graalvm.polyglot.Contextは、GraalVMの言語およびツールとの相互運用性確保に推奨される実行方法です。また、GraalVMで実行されるJavaScriptはJSR 223と完全に互換性があり、ScriptEngine APIをサポートしています。内部的には、GraalVMのJavaScript ScriptEngineによってポリグロット・コンテキスト・インスタンスがラップされます:

ScriptEngine eng = new ScriptEngineManager()
    .getEngineByName("graal.js");
Object fn = eng.eval("(function() { return this; })");
Invocable inv = (Invocable) eng;
Object result = inv.invokeMethod(fn, "call", fn);

GraalVM JavaScriptでの使用方法の詳細は、ScriptEngineガイドを参照してください。

JavaScriptからのJavaへのアクセス

GraalVMには、JavaScriptからJavaへの相互運用性を実現する一連の機能が用意されています。Rhino、NashornおよびGraalVM JavaScriptの全体的な機能セットはほぼ同等ですが、厳密な構文およびセマンティクスの一部が異なります。

クラスへのアクセス

Javaクラスにアクセスできるように、GraalVM JavaScriptはJava.type(typeName)関数をサポートしています:

var FileClass = Java.type('java.io.File');

デフォルトでは、Javaクラスはグローバル変数に自動的にはマップされません。たとえば、GraalVM JavaScriptにはjavaグローバル・プロパティはありません。既存のコード・アクセス(java.io.Fileなど)は、Java.type(name)関数を使用するように書き換える必要があります:

//GraalVM JavaScript compliant syntax
var FileClass = Java.type("java.io.File");
//backwards-compatible syntax
var FileClass = java.io.File;

GraalVM JavaScriptには、互換性確保のためにPackagesjavaおよび類似のグローバル・プロパティが用意されています。ただし、次の2つの理由から、可能な場合は常にJava.typeを使用して目的のクラスに明示的にアクセスすることをお薦めします:

  1. これにより、各プロパティをクラスとして解決しようとするのではなく、1つのステップでクラスを解決できます。
  2. Java.typeは、クラスが見つからないか、アクセスできない場合に、未解決の名前をパッケージとしてサイレントに処理するのではなく、TypeErrorをただちにスローします。

js.java-package-globalsフラグを使用すると、Javaパッケージのグローバル・フィールドを非アクティブ化できます(フィールドの作成を回避するにはfalseを設定します。デフォルトはtrueです)。

Javaオブジェクトの構築

Javaオブジェクトは、JavaScriptのnewキーワードを使用して構成できます:

var FileClass = Java.type('java.io.File');
var file = new FileClass("myFile.md");

フィールドおよびメソッドへのアクセス

Javaクラスの静的フィールドまたはJavaオブジェクトのフィールドには、JavaScriptプロパティと同様にアクセスできます:

var JavaPI = Java.type('java.lang.Math').PI;

Javaメソッドは、JavaScript関数と同様にコールできます:

var file = new (Java.type('java.io.File'))("test.md");
var fileName = file.getName();

メソッドの引数の変換

JavaScriptは、double数値型で動作するように定義されています。GraalVM JavaScriptでは、パフォーマンス上の理由から、内部的に追加のJavaデータ型(int型など)を使用する場合があります。

Javaメソッドをコールする際には、値の変換が必要になることがあります。これは、Javaメソッドがlongパラメータを必要とし、GraalVM JavaScriptからintが渡された場合に起こります(型の拡大)。この変換によって非可逆変換が発生すると、TypeErrorがスローされます:

//Java
void longArg   (long arg1);
void doubleArg (double arg2);
void intArg    (int arg3);
//JavaScript
javaObject.longArg(1);     //widening, OK
javaObject.doubleArg(1);   //widening, OK
javaObject.intArg(1);      //match, OK

javaObject.longArg(1.1);   //lossy conversion, TypeError!
javaObject.doubleArg(1.1); //match, OK
javaObject.intArg(1.1);    //lossy conversion, TypeError!

引数値がパラメータ型に収まる必要があることに注意してください。この動作は、カスタムのターゲット・タイプ・マッピングを使用してオーバーライドできます。

メソッドの選択

Javaでは、引数の型によってメソッドをオーバーロードできます。JavaScriptからのJavaのコール時には、可能なかぎり狭く、実際の引数を損失なしで変換できる型を持つメソッドが選択されます:

//Java
void foo(int arg);
void foo(short arg);
void foo(double arg);
void foo(long arg);
//JavaScript
javaObject.foo(1);              // will call foo(short);
javaObject.foo(Math.pow(2,16)); // will call foo(int);
javaObject.foo(1.1);            // will call foo(double);
javaObject.foo(Math.pow(2,32)); // will call foo(long);

この動作をオーバーライドするには、javaObject['methodName(paramTypes)']構文を使用して明示的なメソッドのオーバーロードを選択できます。パラメータ型はスペースなしでカンマで区切る必要があります。オブジェクト型は完全修飾する必要があります(例: 'get(java.lang.String,java.lang.String[])')。これはNashornとは異なり、余分なスペースや単純な名前を使用できることに注意してください。前述の例では、可逆変換(foo(1))でfoo(short)に到達できたとしても、場合によっては常にfoo(long)をコールする必要があります。

javaObject['foo(int)'](1);
javaObject['foo(long)'](1);
javaObject['foo(double)'](1);

引数値は引き続きパラメータ型に収まる必要があることに注意してください。この動作は、カスタムのターゲット・タイプ・マッピングを使用してオーバーライドできます。

明示的なメソッドの選択は、メソッドのオーバーロードがあいまいで自動的に解決できない場合や、デフォルトの選択をオーバーライドする場合にも役立ちます:

//Java
void sort(List<Object> array, Comparator<Object> callback);
void sort(List<Integer> array, IntBinaryOperator callback);
void consumeArray(List<Object> array);
void consumeArray(Object[] array);
//JavaScript
var array = [3, 13, 3, 7];
var compare = (x, y) => (x < y) ? -1 : ((x == y) ? 0 : 1);

// throws TypeError: Multiple applicable overloads found
javaObject.sort(array, compare);
// explicitly select sort(List, Comparator)
javaObject['sort(java.util.List,java.util.Comparator)'](array, compare);

// will call consumeArray(List)
javaObject.consumeArray(array);
// explicitly select consumeArray(Object[])
javaObject['consumeArray(java.lang.Object[])'](array);

現在、コンストラクタのオーバーロードを明示的に選択する方法はありません。GraalVM JavaScriptの今後のバージョンでは、この制限が解消される可能性があります。

パッケージへのアクセス

GraalVM JavaScriptには、Packagesグローバル・プロパティが用意されています:

> Packages.java.io.File
JavaClass[java.io.File]

配列へのアクセス

GraalVM JavaScriptは、JavaScriptコードからのJava配列の作成をサポートしています。RhinoとNashornによって提案される両方のパターンがサポートされています:

//Rhino pattern
var JArray = Java.type('java.lang.reflect.Array');
var JString = Java.type('java.lang.String');
var sarr = JArray.newInstance(JString, 5);

//Nashorn pattern
var IntArray = Java.type("int[]");
var iarr = new IntArray(5);

作成される配列はJava型ですが、JavaScriptコードで使用できます:

iarr[0] = iarr[iarr.length] * 2;

マップへのアクセス

GraalVM JavaScriptでは、Javaマップ(java.util.HashMapなど)を作成してアクセスできます:

var HashMap = Java.type('java.util.HashMap');
var map = new HashMap();
map.put(1, "a");
map.get(1);

GraalVM JavaScriptは、Nashornと同様に、このようなマップの反復をサポートしています:

for (var key in map) {
    print(key);
    print(map.get(key));
}

リストへのアクセス

GraalVM JavaScriptでは、Javaリスト(java.util.ArrayListなど)を作成してアクセスできます:

var ArrayList = Java.type('java.util.ArrayList');
var list = new ArrayList();
list.add(42);
list.add("23");
list.add({});

for (var idx in list) {
    print(idx);
    print(list.get(idx));
}

文字列へのアクセス

GraalVM JavaScriptでは、Javaの相互運用性を備えたJava文字列を作成できます。文字列の長さは、lengthプロパティを使用して問い合せることができます(lengthは値プロパティであり、関数としてコールすることはできません):

var javaString = new (Java.type('java.lang.String'))("Java");
javaString.length === 4;

GraalVM JavaScriptでは、内部的にJava文字列を使用してJavaScript文字列が表されるため、前述のコードとJavaScript文字列リテラル"Java"は実際には区別できません。

プロパティの反復

JavaクラスおよびJavaオブジェクトのプロパティ(フィールドおよびメソッド)は、JavaScriptのfor..inループを使用して反復できます:

var m = Java.type('java.lang.Math')
for (var i in m) { print(i); }
> E
> PI
> abs
> sin
> ...

JavaからのJavaScriptオブジェクトへのアクセス

JavaScriptオブジェクトは、com.oracle.truffle.api.interop.java.TruffleMapのインスタンスとしてJavaコードに公開されます。このクラスにより、JavaのMapインタフェースが実装されます。

JavaImporter

JavaImporter機能は、Nashorn互換性モード(js.nashorn-compatオプション)でのみ使用できます。

JavaクラスおよびJavaオブジェクトのコンソール出力

GraalVM JavaScriptは、printconsole.logの両方に対応しています。

GraalVM JavaScriptには、Nashornと互換性のあるprint組込み関数が用意されています。

console.logは、Node.jsによって直接提供されます。相互運用性オブジェクトの特別な処理は行われません。GraalVM JavaScriptでのconsole.logのデフォルト実装は単にprintの別名であり、ノードの実装はNode.jsでの実行時にのみ使用できることに注意してください。

例外

Javaコードでスローされた例外は、JavaScriptコードで捕捉できます。これらはJavaオブジェクトとして表されます:

try {
    Java.type('java.lang.Class')
    .forName("nonexistent");
} catch (e) {
    print(e.getMessage());
}

Promise

GraalVM JavaScriptは、JavaScript PromiseオブジェクトとJavaの間の相互運用性をサポートしています。JavaオブジェクトをthenableなオブジェクトとしてJavaScriptコードに公開することによって、await Javaオブジェクトに対するJavaScriptコードを許可できます。さらに、JavaScript Promiseオブジェクトは通常のJavaScriptオブジェクトであり、このドキュメントで説明するメカニズムを使用してJavaからアクセスできます。これにより、JavaScriptのPromiseが解決または拒否されたときに、JavaScriptからJavaコードをコールバックできます。

Javaから解決可能なJavaScript Promiseオブジェクトの作成

JavaScriptアプリケーションでは、Promiseインスタンスの解決をJavaに委任するPromiseオブジェクトを作成できます。これは、JavaオブジェクトをJavaScript Promiseexecutor機能として使用することで、JavaScriptから実現できます。たとえば、次の関数型インタフェースを実装するJavaオブジェクトを使用して、新しいPromiseオブジェクトを作成できます:

@FunctionalInterface
public interface PromiseExecutor {
    void onPromiseCreation(Value onResolve, Value onReject);
}

PromiseExecutorを実装する任意のJavaオブジェクトを使用して、JavaScript Promiseを作成できます:

// `javaExecutable` is a Java object implementing the `PromiseExecutor` interface
var myPromise = new Promise(javaExecutable).then(...);

JavaScript Promiseオブジェクトは、関数型インタフェースだけでなく、GraalVM JavaScriptエンジンで実行できる他のJavaオブジェクト(たとえば、ポリグロットProxyExecutableインタフェースを実装する任意のJavaオブジェクト)を使用しても作成できます。使用例の詳細は、GraalVM JavaScriptユニット・テストを参照してください。

Javaオブジェクトでのawaitの使用

JavaScriptアプリケーションでは、Javaオブジェクトでawait式を使用できます。これは、JavaおよびJavaScriptが非同期イベントと対話する必要がある場合に便利です。JavaオブジェクトをthenableなオブジェクトとしてGraalVM JavaScriptに公開するには、Javaオブジェクトに次のシグネチャを持つthen()というメソッドを実装する必要があります:

void then(Value onResolve, Value onReject);

then()を実装するJavaオブジェクトでawaitを使用すると、そのオブジェクトはGraalVM JavaScriptランタイムでJavaScript Promiseとして扱われます。onResolveおよびonReject引数は、対応するJavaオブジェクトに関連付けられたJavaScript await式を再開または中断するためにJavaコードで使用される実行可能なValueオブジェクトです。使用例の詳細は、GraalVM JavaScriptユニット・テストを参照してください。

JavaからのJavaScript Promiseの使用

JavaScriptで作成されたPromiseオブジェクトは、他のJavaScriptオブジェクトと同様にJavaコードに公開できます。Javaコードでは、通常のValueオブジェクトと同様にこれらのオブジェクトにアクセスでき、Promiseのデフォルトのthen()およびcatch()関数を使用して新しいPromise解決関数を登録できます。たとえば、次のJavaコードでは、JavaScript Promiseの解決時に実行されるJavaコールバックが登録されます:

Value jsPromise = context.eval(ID, "Promise.resolve(42);");
Consumer<Object> javaThen = (value)
    -> System.out.println("Resolved from JavaScript: " + value);
jsPromise.invokeMember("then", javaThen);

使用例の詳細は、GraalVM JavaScriptユニット・テストを参照してください。

マルチスレッド

GraalVM JavaScriptは、Javaと組み合せて使用する場合にマルチスレッドをサポートしています。GraalVM JavaScriptマルチスレッド・モデルの詳細は、「マルチスレッド」ドキュメントを参照してください。

Javaクラスの拡張

JVMモード(--jvm)では、GraalVM JavaScriptによって、Java.extend関数を使用してJavaクラスおよびインタフェースを拡張するためのサポートが提供されます。この機能を使用可能にするには、ポリグロット・コンテキストでホスト・アクセスを有効にする必要があります。

Java.extend

Java.extend(types...)は、生成されたアダプタのJavaクラス・オブジェクトを返します。指定されたJavaクラスまたはインタフェース(あるいは両方)がこれによって拡張されます。たとえば:

var Ext = Java.extend(Java.type("some.AbstractClass"),
                      Java.type("some.Interface1"),
                      Java.type("some.Interface2"));
var impl = new Ext({
  superclassMethod: function() {/*...*/},
  interface1Method: function() {/*...*/},
  interface2Method: function() {/*...*/},
  toString() {return "MyClass";}
});
impl.superclassMethod();

スーパー・メソッドは、Java.super(adapterInstance)を介してコールできます。組合せの例を参照してください:

var sw = new (Java.type("java.io.StringWriter"));
var FilterWriterAdapter = Java.extend(Java.type("java.io.FilterWriter"));
var fw = new FilterWriterAdapter(sw, {
    write: function(s, off, len) {
        s = s.toUpperCase();
        if (off === undefined) {
            fw_super.write(s, 0, s.length)
        } else {
            fw_super.write(s, off, len)
        }
    }
});
var fw_super = Java.super(fw);
fw.write("abcdefg");
fw.write("h".charAt(0));
fw.write("**ijk**", 2, 3);
fw.write("***lmno**", 3, 4);
print(sw); // ABCDEFGHIJKLMNO

nashorn-compatモードでは、新しい演算子をインタフェースまたは抽象クラスの型オブジェクトに対して使用して、インタフェースと抽象クラスを拡張することもできます:

// --experimental-options --js.nashorn-compat
var JFunction = Java.type('java.util.function.Function');
 var sqFn = new JFunction({
   apply: function(x) { return x * x; }
});
sqFn.apply(6); // 36