💿

Fastly Compute SDK の機能と役割 (4) 各 SDK の特徴 <後編: JavaScript>

2023/12/12に公開

この記事は 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 buildgo 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 です。以下のような記事で知っている方も多いかもしれません;

https://www.publickey1.jp/blog/21/webassemblyjitjavascriptbytecode_alliancejavascript.html

アドベントカレンダー 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