原文はこちら。
The article was written by Daniele Bonetta (Oracle Labs).
https://medium.com/graalvm/asynchronous-polyglot-programming-in-graalvm-javascript-and-java-2c62eb02acf0
GraalVMの多くの素敵な機能の一つに、polyglotアプリケーションのサポートがあります。プログラミングスタイルやパラダイムを組み合わせることができるため、異なるプログラミングモデルを簡単に組み合わせることができ、開発者はひとつのアプリケーションで異なる言語を活用できます。GraalVMがオープンソース化されて以来、JavaScriptやPythonのような動的言語を使って、スクリプティング機能を提供するために多くのプラットフォームやフレームワークがGraalVM言語ランタイムの組み込みを始めています(例:Vert.x Es4x、Oracle Databaseの実験的なビルドなど)。
ES for Eclipse Vert.x
https://reactiverse.io/es4x/
Oracle Database Multilingual Engine (MLE)
https://oracle.github.io/oracle-db-mle/releases/0.3.0/
異なるプログラミング言語を混合して組み合わせるとは、多くの場合、異なるプログラミングスタイルやパラダイムを混合して組み合わせることを意味します。これは、同時実行や並列実行のモデルが異なるプログラミング言語を混在させたアプリケーションで明らかです。このブログエントリでは、非同期アプリケーションのコンテキストにおけるJavaとJavaScriptの組み合わせに焦点を当てています。どちらの言語も非同期実行をビルトインでサポートしていますが、ノンブロッキング実行の方法に微妙な違いがあるため、この2つの言語を組み合わせたアプリケーションの作成は困難なものです。
Hello JavaScript, this is Java
非同期コードの実行はJavaScriptの得意とするところです。組み込み言語はasync/awaitやPromiseオブジェクトのような組み込み言語コンストラクトにより、JavaScript開発者は非同期制御フローを自然に取り扱えるようにしています。Polyglotプログラミングに関して言えば、GraalVMを使えばJavaScript開発者が同じ使い慣れた組み込み言語のサポートを使ってJavaオブジェクトと対話できます。
以下のようなシグネチャを持つthenと呼ばれるメソッドを実装することで、任意のJavaオブジェクトをGraalVM JavaScriptアプリケーションに公開できます。
void then(Value resolve, Value reject); |
JavaScriptでは、JavaScriptのPromiseオブジェクトのためのexecutor関数としてthenメソッドを実装するJavaオブジェクトを利用できます。このように、JavaScriptアプリケーションはJaavを使って関連するJavaScriptのpromiseを解決したり拒否したりできます。
Promise() constructor
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise
例として、次のJavaScriptコードを考えてみましょう。
// a Java object implementing the `then` method (created from Java) | |
let javaExecutor = Java.type('some.awesome.Klass').createNew(); | |
// create a JavaScript promise | |
let interopPromise = new Promise(javaExecutor); | |
// register some promise reactions | |
interopPromise.then(result => { console.log('Java resolves: ' + result); }, | |
error => { console.log('Java rejects: ' + error); }); |
ここで、interopPromise
は次のクラスのJavaオブジェクトインスタンスです。
public class ThenableObject { | |
void then(Value resolve, Value reject) { | |
try { | |
Object someResult = computeSomething(); | |
resolve.executeVoid(someResult); | |
} catch (Throwable t) { | |
reject.executeVoid(t); | |
} | |
} | |
} |
実行時には、GraalVMはJavaオブジェクトにJavaScriptのpromiseの解決を委譲します。この例では、JavaのcomputeSomething()というメソッド呼び出しの結果次第で、JavaScriptのpromiseがinteropPromiseオブジェクトを解決するか拒否する、という意味です。
Javaのexecutor関数を使って作成されたPromise
オブジェクトは、事実上JavaScriptオブジェクトのインスタンスです。それゆえ、他のPromiseオブジェクトと同様に使用できます。このプロパティの良い結果は、async functionから使用できることです。
async function() { | |
// a Java object implementing the `then` method | |
let javaTrampoline = ... // created from Java | |
// create a JavaScript promise | |
let interopPromise = new Promise(javaTrampoline); | |
// register some promise reactions | |
try { | |
// await for the “interop” promise to resolve | |
let result = await interopPromise; | |
console.log('Java resolves: ' + result); | |
} catch (error) { | |
// handle project rejection | |
console.log('Java rejects: ' + error); | |
} | |
} |
Hello Java, this is JavaScript!
すべてのJavaScriptオブジェクトは、Promiseオブジェクトインスタンスを含め、GraalVM polyglot APIを使用してJavaアプリケーションから使用できます。したがって、JavaアプリケーションはPromise APIを使用してそのようなオブジェクトと対話できます。以下のコードは、Javaのラムダ関数がJavaScriptのpromiseのリアクションとしてどのように使用できるかを示しています。
Package org.graalvm.polyglot
https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/package-summary.html
// Expose a JS asynchronous function to Java | |
Value asyncFunction = context.eval("js", "(async function(x) { return x; })"); | |
// Invoke the function. The result is a JavaScript Promise object | |
Value jsPromise = asyncFunction.execute(42); | |
// Register promise reactions | |
jsPromise.invokeMember("then", (Consumer<Object>) (result) -> { | |
System.out.println("JavaScript resolves: " + result); | |
}) | |
.invokeMember("catch", (Consumer<Throwable>) (error) -> { | |
System.out.println("JavaScript rejects: " + error); | |
}); |
実行すると、このコードは asyncFunctionというJavaScriptの関数を呼び出し、解決値42を持つPromiseオブジェクトを解決します。Promise解決メカニズムの結果として、JavaのConsumerラムダ関数が呼び出され、標準出力に「JavaScript resolves: …」と表示されます。
Polyglot HTTP Web サービスでの非同期実行と並行実行の組み合わせ
以前のブログエントリで説明したように、GraalVMのJavaScriptは、Shared nothing並行プログラミングモデルに依存しています。
Multi-threaded Java ←→JavaScript language interoperability in GraalVM
https://medium.com/graalvm/multi-threaded-java-javascript-language-interoperability-in-graalvm-2f19c1f9c37b
このモデルによれば、JavaScriptオブジェクトを2個のスレッドが同時に利用できませんが、専用のpolyglotのContextを使う並行スレッドは許可されます。さらに、Javaオブジェクトをコンテキスト間で共有でき、適切な同期化が行われていれば、1つのコンテキストを異なるスレッドから使用できます。非同期プログラミングに関しても同じルールが適用されるので、マルチスレッド実行の恩恵を受ける非同期のJavaとJavaScript間の呼び出しを行うアプリケーションを実装できます。
では、polyglotなHelidonのWebアプリを考えてみましょう。下図はこのデモで利用するサンプルアプリ内での、主要な言語間の対話に関するハイレベルな概要です。
Helidon
https://helidon.io/

アプリケーションはJavaとJavaScriptの実行を組み合わせて、以下のような操作を行います。
- リクエスト処理 (Java)
HelidonのWebサーバーは、受信リクエストを処理します。リクエストURLを解析し、リクエストIDのような有用な情報を抽出します。この時点で、Helidonはすでに複数の並列スレッドを使用して同時リクエストを処理している可能性があります。 - リクエストの検証 (JS)
入力リクエストIDを検証するためにJavaScriptのasync関数を使用します。値が受け入れられた場合、JavaScriptの関数は(awaitを使用して)実行を一時停止し、別のJavaスレッドに重い計算処理を委譲します。 - リクエスト処理(Java)
別のスレッドでは、JavaのメソッドがCPU集約的な計算を行います。実行されると、JavaScriptのPromiseを解決することで,#2のJavaScriptのasync関数を再開します。 - レスポンス作成(JS)
再開されると、JavaScriptの非同期関数はJSON.stringify()を使用して、Javaで計算されたデータを含むレスポンスオブジェクトを作成できます。 - クライアント・レスポンス(Java)
最後に、JavaScriptのasync関数が戻ります。実行フローはJavaに戻り、CompletableFutureがJavaScriptで作成したJSONデータを使ってクライアント・レスポンスを生成します。
各ステップの実装は、GraalVM examplesというGitHubリポジトリにあるサンプルの完全版ご覧ください。以下は、JavaScriptのPromiseがJavaのCompletableFutureにどのようにマッピングされるかを示す、2つの(簡略化された)コード例です。
GraalVM Demos: Asynchronous Polyglot Programming in GraalVM Using Helidon and JavaScript
https://github.com/graalvm/graalvm-demos/tree/master/js-java-async-helidon
// HTTP request handler | |
WebServer.create(configuration, route.get("/request", (req, res) -> { | |
// ( #1 ) An HTTP GET request is received. Get some request data | |
int requestId = getRequestId(req); | |
// Send the data to JavaScript. Perform computation in JS and | |
// send back to the HTTP client a response message when done. | |
executeJs(requestId).whenComplete((r, ex) -> { | |
if (ex != null) { | |
res.status(404) | |
.send("404 error"); | |
} else { | |
// ( #5 ) Client response. Result 'r' is a JSON string created in JavaScript | |
res.send(r); | |
} | |
}); | |
}); | |
// JavaScript code that will be executed (for each request) | |
private static final String jsSource = "(async function(requestId) {" + | |
" // ( #2 ) The request id is validated in JS" + | |
" if (!validate(requestId)) return 'Bad request!';" + | |
" // ( #3 ) Process the request in Java" + | |
" let data = await computeFromJava(requestId);" + | |
" // ( #4 ) Create a JSON object for the response " + | |
" return JSON.stringify({requestId:requestId,result:data});" + | |
"})"; | |
// Offload request handling to JavaScript | |
private CompletionStage<Object> executeJs(int requestId) { | |
CompletableFuture<Object> jsExecution = new CompletableFuture<>(); | |
Context cx = getCurrentPolyglotContext(); | |
// Access to a polyglot context should be synchronized. We use a lock here. | |
contextAccessLock.lock(); | |
try { | |
// Enter the context | |
cx.enter(); | |
Value jsAsyncFunction = cx.eval(JS, jsSource); | |
// Execute the JavaScript code. The `async` function returns a JS | |
// Promise object. The Java `then` reaction will be executed by | |
// the JavaScript engine when `await` completes. | |
jsAsyncFunction.execute(requestId) | |
.invokeMember(THEN, (Consumer<?>) jsExecution::complete) | |
.invokeMember(CATCH, (Consumer<Throwable>) jsExecution::completeExceptionally); | |
} finally { | |
cx.leave(); | |
contextAccessLock.unlock(); | |
} | |
return jsExecution; | |
} |
このシンプルなデモアプリケーションは、JavaとJavaScriptを組み合わせたマルチスレッドと非同期プログラミングを使用するシンプルな(しかし効果的な)方法を示しており、Polyglotプログラミングがいかに強力なものであるかを示しています。JavaからのリクエストをJavaScriptにシームレスに委譲できるため、async/awaitのようなお馴染みの概念を使用して、並列実行される非同期計算を待ちながら、別のJavaスレッドに計算をオフロードすることができます。
このブログエントリで説明したように、GraalVMのPolyglot機能により、複数の言語やパラダイムにまたがる非同期プログラミングが可能になります。この機能が有用であると考えられるドメインの1つがWebアプリケーションで、それは異なる言語で書かれた多くの優れたフレームワークが存在するためです。もちろん、他の多くのドメインでもPolyglotプログラミングの恩恵を受けることができるかもしれません。将来、GraalVM JavaScriptを統合したすばらしいPolyglotアプリケーションがどれだけ登場するのか、今から楽しみでなりません。
サンプルアプリケーションはご自身でお試しいただけます。GraalVMをダウンロードし、GitHubからデモアプリケーションを入手して是非お試しください。
GraalVM Downloads
https://www.graalvm.org/downloads
GraalVM Demos: Asynchronous Polyglot Programming in GraalVM Using Helidon and JavaScript
https://github.com/graalvm/graalvm-demos/tree/master/js-java-async-helidon