🪢

ECMAScript source phase imports がはやく動くようになってほしい

に公開

Node.js 24.0.0 でECMAScript source phase importsが動くようになりました。 (--experimental-wasm-modules が必要です)

Node.jsエコシステムは様々なサードパーティーツールに支えられているので、Node.js自身が新しい機能をサポートしたからといってすぐさまアプリケーションやライブラリで採用できるわけではないですが、この機会に何が嬉しいのか説明しようと思います。

サンプル

まず以下のサンプルコードを見てください。

https://github.com/qnighy/source-phase-import-example

本コードで重要なのは以下の3行だけです。

src/example.js
import source addModuleSource from "./add.wasm";

const addModule = await WebAssembly.instantiate(addModuleSource);

console.log("123 + 42 =", addModule.exports.add(123, 42));

ポイントは以下の2点です。

  • WebAssemblyのコードを importで読み込んでいる
  • その一方で、モジュールのインスタンス化を WebAssembly.instantiate明示的に行っている

何ができるのか

import は通常、リンク・初期化済みのモジュールを返します。しかし、インポート時に source キーワードを指定 すると、 リンク前のAST が入ったオブジェクトが手に入ります。

import source myModule from "./myModule.wasm";
//     ^^^^^^ sourceキーワードを指定

この機能自体はJavaScriptモジュールでもWebAssemblyモジュールでも利用できますが、WebAssemblyの場合は WebAssembly.Module オブジェクトが返ってきます。得られたモジュールソースを WebAssembly.instantiate に渡すことで、リンクすることができます。

何が嬉しいのか

既存の読み込み方式は移植性がない

WebAssemblyモジュールはJavaScriptモジュールと同じように直接インポートして使うこともできます。しかし実際には、WASIなどの外部環境を模倣したモジュールを別途作成し、それとリンクして使うといったより複雑な初期化ステップが必要なことも多いという実情があります。

こうした場合、何らかの方法でWebAssemblyのバイナリまたはモジュールソースオブジェクトを取得する必要があります。従来のJavaScriptのモジュールシステムに照らし合わせると、これには主に以下の3つの方法が考えられます。

  • 外部ファイルとして配置し、ランタイムに固有の方法でファイルを読み出す。
  • 外部ファイルとして配置し、バンドラーに固有の方法でファイルをリンクする。
  • JavaScriptソース中に、定数として埋め込む

しかし、ランタイムやバンドラーに固有の方法を使うと移植性がありません。たとえば@babel/plugin-proposal-import-wasm-sourceのトランスパイル例を見ると、

// ブラウザ/Webpack向け
const libMod = await WebAssembly.compileStreaming(fetch(import.meta.resolve("./lib.wasm")));

// Node.js向け
import { readFileSync as _readFileSync } from "fs";
const libMod = new WebAssembly.Module(_readFileSync(new URL(import.meta.resolve("./lib.wasm"))));

という形になっていて、環境ごとに出力コードが異なることがわかります。

また、ソースJavaScript中にwasmバイナリを埋め込む方法は確実ではあるものの、JavaScriptソースを生成する手順が必要になるなどの厄介な側面があり、実際にあまり使われていないようです。

import source を使うと、標準的な構文でソースコードを取得できるようになるため、こうした不都合が次第に解消されていくことが期待できます。

標準化されることで安全性を保証しやすい

既存の読み込み方式では、以下の操作をアプリケーション側の権限で実行することになります。

  • wasmモジュールファイルの読み取り
  • wasmモジュールのコンパイル

ランタイム環境によっては、こうした操作を安全性のために禁止していることがあります。たとえばDenoは前者のような読み取りを --allow-read で制限していますし、ブラウザ環境ではCSPによって後者のようなeval操作を制限しています。

source phase importsでは、これらの操作をまとめて「信頼できるソースコードの参照」とみなし、ランタイム環境が代理して実行できるようになります。そのため、read/evalのような不安全な操作の許可をせずにwasmモジュールを扱えるようになることが期待されます。

soure phase importsを使えるようになるには何が必要か

ようやくNode.jsがsource phase importsに対応しましたが、一般的なフロントエンドアプリケーションであれば以下のようなツールによる実装を待つ必要があるでしょう。

  • TypeScriptコンパイラによるサポート
  • トランスパイラによるサポート (SWC, Babel等)
  • バンドラーによるサポート (Webpack等)
  • Linter/Formatterによるサポート (ESLint, Prettier等)

また、WebAssemblyが必要なケースのうち、アプリケーションコードで独自にWebAssemblyモジュールを準備する場合よりも、ライブラリがWebAssemblyモジュールを用意している場合のほうが多いかと思います。このようなケースでは、ライブラリ側でsource phase importsを使ってWebAssemblyモジュールをバンドルするようになって初めてユーザー側で恩恵を受けることができるようになると考えられます。

また、こうした普及の過程で標準化が進むことも重要です。とはいってもすでに Stage 3 であり、こうして実装も進んでいるようなので、それほど心配はないのではないかと思います。

まとめ

  • JavaScriptからWebAssemblyを使うときは、直接インポートするのではなく、リンク前のモジュールを取得する必要がある場合が多い。
  • source phase imports はこのユースケースに適した標準的な構文を提供する。
  • まだ普及までは時間がかかりそうだが、普及すればWebAssemblyコードを含むJavaScriptライブラリを使いやすくなることが期待できる。

関連資料

https://zenn.dev/pixiv/articles/c7071eb29927fe

https://zenn.dev/sosukesuzuki/articles/af1f9afdc7275e

Discussion