Wasmコンポーネントで始めるブラウザ実装
閑話
技術書典14から継続してWasmネタを集めて同人誌を制作しており,4/11から始まる技術書典20でもWasm本の新刊を出す予定です.
Wasmは面白い技術なので多くの人に興味持ってもらえると良いなと思っています.
さて,そんな新刊の第1章に「Wasmとウェブの今後について考える」という題で,Wasmがまたウェブで盛り上がるんじゃないかという個人的な期待を綴っているのですが(何をいっているんだ?と気になった人に是非読んでみてください),その中で触れているWasmコンポーネントをブラウザ上で(擬似的に)実行する方法を紹介したいと思います.
Why is WebAssembly a second-class language on the web?
Why is WebAssembly a second-class language on the web? というタイトルの記事が2月に投稿されています.
この記事をざっくり要約すると,次のような内容になります.
- ブラウザにおけるWasmは常にJSを必要としてしまっている(ウェブの最初の選択肢になり得ない).
- JSのグルーコードを介した分だけWasmのパフォーマンスに影響する(JSを介さない分だけパフォーマンスが良くなる).
- WasmコンポーネントのWeb APIを定義することで,JSを介さずにブラウザ上でWasmを実行できるようにしよう(そのための設計をワーキンググループで進めている).
Wasmコンポーネントが何かといった話は,これまた同人誌に何度か寄稿しているので,そちらを読んでほしいところではあるのですが,
WasmのCore SpecがCライクなインターフェースを提供していたのに対して,WasmコンポーネントではStringなどの高級インターフェースを定義できるのが大きな特徴です.
Web APIをWasmコンポーネントで定義する
WasmコンポーネントではWITと呼ばれるIDLが用意されており,console.logのAPIを次のように定義することができます.
そして,この定義をRustやGoなどの言語からは次のように実装することができます.
どちらも同じような実装になっているのを確認できます.
これらのコードをビルドするには,Rustはwasm32-wasip2をビルドターゲットに,Goはtinygoを使ってwasip2をビルドターゲットにすることでビルドすることができます.
cargo build --target wasm32-wasip2 --release
tinygo build -target=wasip2 -wit-package wit -wit-world app -o example.wasm .
Wasmコンポーネントをブラウザで動かすには
残念ながら現時点でWasmコンポーネントを直接ブラウザで実行することはできないのですが,JCOを使ってWasmコンポーネントをブラウザ上で実行できるWasmモジュールにトランスパイルすることができます.
deno run -A npm:@bytecodealliance/jco transpile --no-nodejs-compat --no-wasi-shim -o dist/ example.wasm
Transpiled JS Component Files:
- dist/example.core.wasm 331 KiB
- dist/example.d.ts 1.46 KiB
- dist/example.js 275 KiB
- dist/interfaces/wasi-cli-environment.d.ts 0.2 KiB
- dist/interfaces/wasi-cli-run.d.ts 0.07 KiB
- dist/interfaces/wasi-cli-stderr.d.ts 0.16 KiB
- dist/interfaces/wasi-cli-stdin.d.ts 0.15 KiB
- dist/interfaces/wasi-cli-stdout.d.ts 0.16 KiB
- dist/interfaces/wasi-clocks-monotonic-clock.d.ts 0.12 KiB
- dist/interfaces/wasi-clocks-wall-clock.d.ts 0.16 KiB
- dist/interfaces/wasi-filesystem-preopens.d.ts 0.19 KiB
- dist/interfaces/wasi-filesystem-types.d.ts 3.74 KiB
- dist/interfaces/wasi-io-error.d.ts 0.15 KiB
- dist/interfaces/wasi-io-streams.d.ts 0.68 KiB
- dist/interfaces/wasi-random-random.d.ts 0.14 KiB
- dist/interfaces/web-std-console.d.ts 0.18 KiB
トランスパイルすると,このように指定したディレクトリに各種ファイルが作成されます.
生成されたdist/example.jsのコードを読んでみると,冒頭に次のようなモジュールインポートがあるのを確認できます.
// dist/example.js
import { log, time, timeEnd } from 'web:std/console';
このモジュールのインターフェース定義も一緒に生成されているので,生成されたdist/interfaces/web-std-console.d.tsを見てみると,次のようなコードが生成されているのを確認できます.
// dist/interfaces/web-std-console.d.ts
/** @module Interface web:std/console@0.1.0 **/
export function log(message: string): void;
export function time(label: string): void;
export function timeEnd(label: string): void;
あとはこのインターフェースを実際に実装してあげることで,ブラウザ上でトランスパイルしたWasmモジュールを動かすことができます.
このJSのグルーコードとなる実装例はJSRを通して公開しています.
そのため,次のようにesm.shを経由したインポートマップを定義することで,この実装がブラウザ上で動作するのを確認できます.
deno run -NR=. jsr:@std/http/file-server
Listening on:
- Local: http://0.0.0.0:8000

まとめ
このように,Wasmコンポーネントを活用することで,Web APIを簡単に定義して利用することができます.
現時点ではブラウザがWasmコンポーネントをサポートしていないため,JCOを使ってトランスパイルしている関係でJSのグルーコードを必要していますが,近い将来ブラウザ自体がWasmコンポーネントのWeb APIをサポートすることでJSを介さずにWasmを直接ブラウザから実行できるようになるかもしれません.
<script type="module" src="component.wasm"></script>
また,WasmコンポーネントはWasmモジュールのAPIを実装するよりも簡単にAPIの定義と実装ができるので,Wasmの入門にもちょうど良いのではないかと思います(技術書典20の新刊でWasmコンポーネントの活用方法を紹介しています).
Discussion
bundleは必須になるかもだけど(rolldownでbundleした)、
jco transpile --no-wasi-shimの代わりにとして、明示的にマップしてあげると別途
js shimの配布なくてもいけそう。ローカルでは実行できました。