🎉

Wasmコンポーネントで始めるブラウザ実装

に公開1

閑話

技術書典14から継続してWasmネタを集めて同人誌を制作しており,4/11から始まる技術書典20でもWasm本の新刊を出す予定です.
Wasmは面白い技術なので多くの人に興味持ってもらえると良いなと思っています.

https://techbookfest.org/product/aA4Av5Bcu6ztjDfZiMgXWJ

さて,そんな新刊の第1章に「Wasmとウェブの今後について考える」という題で,Wasmがまたウェブで盛り上がるんじゃないかという個人的な期待を綴っているのですが(何をいっているんだ?と気になった人に是非読んでみてください),その中で触れているWasmコンポーネントをブラウザ上で(擬似的に)実行する方法を紹介したいと思います.

Why is WebAssembly a second-class language on the web?

https://hacks.mozilla.org/2026/02/making-webassembly-a-first-class-language-on-the-web/

Why is WebAssembly a second-class language on the web? というタイトルの記事が2月に投稿されています.
この記事をざっくり要約すると,次のような内容になります.

  1. ブラウザにおけるWasmは常にJSを必要としてしまっている(ウェブの最初の選択肢になり得ない).
  2. JSのグルーコードを介した分だけWasmのパフォーマンスに影響する(JSを介さない分だけパフォーマンスが良くなる).
  3. WasmコンポーネントのWeb APIを定義することで,JSを介さずにブラウザ上でWasmを実行できるようにしよう(そのための設計をワーキンググループで進めている).

Wasmコンポーネントが何かといった話は,これまた同人誌に何度か寄稿しているので,そちらを読んでほしいところではあるのですが,
WasmのCore SpecがCライクなインターフェースを提供していたのに対して,WasmコンポーネントではStringなどの高級インターフェースを定義できるのが大きな特徴です.

Web APIをWasmコンポーネントで定義する

WasmコンポーネントではWITと呼ばれるIDLが用意されており,console.logのAPIを次のように定義することができます.
https://github.com/a-skua/web-wasm/blob/1f29bf36b2d3c5b0d6afee5ee74fafddc89544d0/spec/std/wit/console.wit#L1-L10

そして,この定義をRustやGoなどの言語からは次のように実装することができます.

https://github.com/a-skua/web-wasm/blob/c2e57b4f9c233a525d9cb1355610e30ca5502fd9/examples/rust/src/main.rs#L1-L13

https://github.com/a-skua/web-wasm/blob/1f29bf36b2d3c5b0d6afee5ee74fafddc89544d0/examples/go/main.go#L1-L11

どちらも同じような実装になっているのを確認できます.
これらのコードをビルドするには,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モジュールにトランスパイルすることができます.

https://github.com/bytecodealliance/jco

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モジュールを動かすことができます.

https://github.com/a-skua/web-wasm/blob/c2e57b4f9c233a525d9cb1355610e30ca5502fd9/std/console.ts#L1-L11

このJSのグルーコードとなる実装例はJSRを通して公開しています.

https://jsr.io/@askua/web

そのため,次のようにesm.shを経由したインポートマップを定義することで,この実装がブラウザ上で動作するのを確認できます.

https://github.com/a-skua/web-wasm/blob/c2e57b4f9c233a525d9cb1355610e30ca5502fd9/examples/go/index.html#L1-L42

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

ktz_aliasktz_alias

bundleは必須になるかもだけど(rolldownでbundleした)、
jco transpile --no-wasi-shimの代わりに

pnpm exec jco transpile \
    --map "web:std/console=path/to/std/console" 
    --map "wasi:cli/environment=path/to/wasip2/cli/environment" \
    ...

として、明示的にマップしてあげると別途js shimの配布なくてもいけそう。
ローカルでは実行できました。