Fastly Compute SDK の機能と役割 (4) 各 SDK の特徴 <後編: JavaScript>
この記事は Fastly Compute (旧 Compute@Edge) 一人アドベントカレンダー 11 日目の記事です。
Fastly Compute での開発に欠かせない SDK について少し深掘りして見ていくシリーズ(Fastly Compute SDKの機能と役割)の第四回目です。前回は前編として Rust SDK と Go SDK について紹介しました。本稿では残る JavaScript SDK の特徴について紹介します。
JavaScript SDK
JavaScript SDK の一番の特徴は、JS のエンジンとして SpiderMonkey を採用している点かと思います。前編で見てきた Rust や Go がコンパイル型言語で cargo build
や go build
といったコンパイル用のコマンドが直接 .wasm ファイルを生成したのに対して、JavaScript SDK はそれとは異なる方式で .wasm ファイルを生成します。
JavaScript の場合に $fastly compute build
コマンドが内部的に実行するコマンドを見てみましょう(source)。
npm exec js-compute-runtime ./src/index.js ./bin/main.wasm
JS SDK の package.json の bin フィールドには
"bin": {
"js-compute-runtime": "js-compute-runtime-cli.js"
},
とあり、SDK に含まれている js-compute-runtime-cli.js
にビルドの実装があるようです。このスクリプトは 40 行程度の小さなファイルになっていて、読み進めると後半に compileApplicationToWasm()
なるメソッド呼び出しが出てきます。
await compileApplicationToWasm(input, output, wasmEngine, enableExperimentalHighResolutionTimeMethods, enablePBL);
このメソッドの前後を読むとビルドの流れが追えるので詳しく見ていきます。(source)
処理の大きな流れは以下のように整理できます;
- (A) で入力された JavaScript のソースコードを Syntax エラーのチェックや precompile などの前処理を行った上で、
$wizer
コマンドに標準入力として入れる - (C) で入力された
js-compute-runtime.wasm
というバイナリをベースに$wizer
コマンドが./bin/main.wasm
という新しいバイナリを生成する
以上のように、Go や Rust SDK の時とは明らかに異なるビルドプロセスを踏んでいることが分かるかと思います。これらのうち明らかになっていないのは js-compute-runtime.wasm
というバイナリが何かと、 $wizer
コマンドが何をしているかです。これらについて順番に説明します。
js-compute-runtime.wasm
ソースはここで見れます。C++ で書かれた JavaScript SDK の実態で、SpiderMonkey をリンクして JS のエンジンとして利用しながら各種 API を提供しています。この .wasm ビルドは @fastly/js-compute パッケージの一部としてプリビルドされた状態で SDK として配布されるため、 LLVM/Clang といった C++ のビルド環境は必要とせずに SDK を使うことができます。
$wizer
コマンド
wizer は実行時速度の最適化のために $ wizer input.wasm -o initialized.wasm
のようなコマンドで使うことができる WebAssembly の Pre-initializer です。以下のような記事で知っている方も多いかもしれません;
アドベントカレンダー 3 日目の記事で紹介した Jake の podcast 出演の時の 43:06~ あたりの wizer の紹介が要領を得た説明になっていて分かりやすいです。
What this means for JavaScript is it ends up running your top level scope of your JavaScript application.
And when we're compiling your program to WebAssembly, So the the top level of your application has already executed, and the runtime has already, like, initialized.
JavaScript SDK を利用してアプリケーションをビルド(コンパイル)すると、JavaScript の top レベルのスコープまでビルドステップの一部として実行されて、初期化された状態でバイトコードに変換されると説明されています。
実際に動かして wizer の動作を試す
この wizer による "JavaScript の top レベルのスコープまでビルドステップの一部として実行" する動きは以下のようなコード例で簡単に試すことができます。
まず以下のようなソースコードを用意して、 $fastly compute build
コマンドが成功することを確認してください。この時、"cnsole" という存在しないオブジェクトが参照されていることに注意してください。
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
async function handleRequest(event) {
cnsole.log("hello world"); // ビルドに成功する(実行時エラー)
return new Response("OK", { status: 200 });
}
次に、同じオブジェクト呼び出しを以下のように top レベルに移動して同じようにビルドすると、 $fastly compute build
コマンドが Exception while evaluating JS: (new ReferenceError("cnsole is not defined", "<stdin>", 4))
というエラーとともに失敗します。
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
cnsole.log("hello world"); // ビルドに失敗する
async function handleRequest(event) {
return new Response("OK", { status: 200 });
}
これが以前の記事 で (JS SDKの場合のみ) wizer の初期化で失敗してビルドに失敗する
として紹介したエラーが起こる仕組みです。
まとめ
JavaScript SDK がどのようにして SpiderMonkey 入りの .wasm ビルドを生成しているかについて見てきました。昨日の前編とは少し毛色が異なる紹介となりましたが、この幅の広さが Fastly Compute の特色の一つです。興味があれば、さらに詳しく公式/非公式 SDK の各種ソースコードを見てみてください。
明日は本シリーズの最終回として、Wasm ABI で定義される Hostcall について機能の概要や SDK の API 実装例について見ていきます。
Discussion