薊畑

シリーズWasmバイナリを読めるようになろうWasm処理系の概要

WebAssembly(Wasm)は高速さが売りの低レベルなバイトコート形式であり、今日ではゲームや音声解析など高速な処理を要求されるWebサイトや、ユーザコードを実行するため高度なサンドボックス性が要求されるプロキシのユーザプラグインなどで使用されている。

例えば、Unity roomのゲームはWasmで配信されており、DuckDB Web Shell もWasmで動いている。

滋賀県輪投げ DuckDB Web Shell
unity roomで配信されているゲームの例と、DuckDB Web Shell。
例としたゲームのwasmファイルは、配信用URLで配信されており、解凍すれば中身を見れる。

Wasmは2025年10月3日にバージョン3.0が公開された。本記事は、このバージョンのWasmについて解説するものである。

Wasmの歴史

2008年頃からJavaScriptは高速化の道を走り始めた。ブラウザ上でゲームや画像処理などを行うため、NaClやasm.jsなど様々な形式が提案されたが、移植性や高速化に限界があるなど、どの方法にも欠点が多かった。

このような流れの中で、WebAssemblyは2015年に開発が開始され、2017年に広く公開された。

WebAssemblyが目指す目標は、大まかには以下の4つである。

  • 高速である:ネイティブなパフォーマンス
  • 安全である:サンドボックス化された環境で実行される
  • 可搬である:プラットフォームやハードウェアに依存しない
  • 言語非依存:どんな言語からでもコンパイルできる

当初Web実行のために開発され、多数のブラウザに組み込まれるようになったWasmだが、最近はWasmtime, WAMR, WasmEdgeなどのネイティブ環境向けのWasmランタイムも開発されるようになり、活躍の場を広げている。

Wasmの処理系

テキストフォーマット

WebAssemblyの実行ファイルは、.wasm拡張子のバイナリファイルである。 .elfを.sに変換できるように、.wasmにも人間が読める形式が存在し、wat2wasmコマンドを使用して.watファイルを作成することができる。 これ以降、wat形式のwasm命令を確認していく。

Shellwat2wasm webgl.wasm -o webgl.wat

スタックマシン

Wasmの実行系は、ネイティブに迫るパフォーマンスを実現する最適化、JITでの高速なコンパイル中に可能にするために、構造の簡単なスタックマシンとして表現される。

全ての命令はスタックから0個以上の要素をpopし、0個以上の要素をpushする形式で表される。 (第3回で詳述)

Wasm(func $calc
  (param $a i32) (param $b i32)
  (result i32)
  local.get $a ;; $aをスタックに追加
  local.get $b ;; $bをスタックに追加
  i32.add      ;; $a,$bをスタックから除き、$a+$bをスタックに追加
  i32.const 2  ;; 2をスタックに追加
  i32.mul)     ;; 先頭2個の値をスタックから除き、($a+$b)*2をスタックに追加
命令実行後のスタック。local.get aでa、local.get bで上からbとa、i32.addでa+b、i32.const 2で上から2とa+b、i32.mulで(a+b)×2が残る。

モジュール

Wasmを実行するために必要なすべての要素は、モジュールとしてひとまとめに表現される。 モジュールは複数のコンポーネントからなっている。

型

Wasmの処理中で使われる型のうち、ユーザの定義したもののリスト(第2回で詳述)

Wasm(rec
  (type $node (struct
    (field i32)
    (field (ref null $children))
  ))
  (type $children (struct
    (field (ref null $node))
  ))
)
(type $unary (func (param i32) (result i32)))

import

外部からインポートする要素のモジュール名、名前、型のリスト。 インポート可能なものとしては、関数、メモリ、テーブル、タグ、グローバルの5種類がある。

Wasm(import "host" "increment"
  (func $increment (param i32) (result i32)))
(import "wasi_snapshot_preview1" "fd_write"
  (func $fd_write (param i32 i32 i32 i32) (result i32)))

タグ

例外の種類を表す型である。タグには名前と保有する情報が設定されており、タグの実体となる例外では、どのタグに表現されるかと、例外を説明する値が保持される。

Wasm(tag $problem (param i32))

グローバル変数

Cのグローバル変数と同様

Wasm(global $scale (mut i32) (i32.const 2))

線形メモリ

生のバイト列。数値データのみをしまうことができ(つまり参照は持てず)、インデックスでアクセスする。

Wasm(memory 1)

テーブル

参照を保存するためのデータ構造。関数への参照などを保持する。

Wasm(table 2 funcref)

関数

関数の名前と型のリストを持つ。

Wasm(type $unary (func (param i32) (result i32)))

データ

線形メモリを初期化するためのデータを保存する。

要素

テーブルを初期化するためのデータを保存する。

開始関数

モジュールを読み込んだ際、最初に実行する関数のインデックスを保持する。

export

外部にエクスポートする要素の名前と型のリスト

Wasm(export "ready" (global $ready))
(export "table" (table 0))

実行時の動作

ランタイム中では、概念上では主に2つのデータ構造が使用される。

スタック

1つ目は、スタックマシンとしてのWasmが動作するスタックである。 具体的な実装は処理系依存。

ストア

2つ目は、Wasmプログラムによって操作可能な全てのグローバル状態が入るストアである。 ストアは、具体的には、以下の要素を持つ。

  • タグ
  • グローバル変数
  • 線形メモリ
  • テーブル
  • 関数
  • データ
  • 要素
  • 構造体
  • 配列
  • 例外

高級言語からのコンパイル

Wasmは、C,Rustといった高級言語からコンパイルして使用することができる。実際にCのプログラムをコンパイルしたWasmにおいて、実行時にどのような挙動を示すか見てみよう。

Cint example(int n) {
  volatile int values[1] = {n};
  return values[0];
}
Wasm(module
(memory (;0;) 1)
(global $__stack_pointer (mut i32) (i32.const 65536))

(func $example (param i32) (result i32)
  (local i32)
  global.get $__stack_pointer
  i32.const 16
  i32.sub
  local.tee 1         ;; local 1にvaluesのアドレスをしまっている。
  local.get 0         ;; local 0は引数 n
  i32.store offset=12 ;; 線形メモリ 0 に値をしまう。ローカル変数ではない
  local.get 1
  i32.load offset=12)
(export "example" (func $example)))

ここで、Wasmではスタック領域が2つあることに注意が必要である。

1つ目は、スタックマシンの使用するスタックである。 このスタックは通常の演算の他に、ローカル変数の保存にも用いられる。 処理の抽象論では、スタック上にframeという構造体が保存され、ローカル変数はこのframe内に保存される(実際のランタイムで、frameのような構造体が作られているわけではない)

2つ目は、線形メモリ上に作成されるスタックである。 Cプログラムをコンパイルした際、線形メモリ中には、通常のx86バイナリと同様の構造を持ったスタックとヒープが作成される。 この内、スタックにはローカル変数として保存できない配列などを配置し、ヒープにはmallocで確保したメモリが保存されている。

使用例

ブラウザ上での高速な処理

自己完結した思い計算を行うための計算カーネルとしての利用

  • Figmaでは、描画・計算・パースと行った高負荷処理をC++からコンパイルしたWasmに置き換え、初期ロード時間が3倍超改善した
  • ONNX Runtime Web / TensorFlow.js ではWasmを使いブラウザで機械学習推論を行える

C/C++資産のWebへの移植

既存のソフトウェアをブラウザのセキュリティ環境の中に入れ、再実装も避けることができる

  • Photoshopをブラウザで動かせるようになった。実装時にSIMDを使うことができ、高速な処理が可能に
  • Unityで作成したゲームをブラウザで動作させることが可能に
  • FFmpegやOpenCVをWasmへ移植し、ブラウザ内で音声変換や画像処理を行える
  • その他、DuckDB, SQLite, CPython, R, .NET, Rustコンパイラなどが移植され、ブラウザ上での処理が可能になっている

オーケストレーション

Wasmを軽量な使い捨てプロセスとして使う方法。コンテナのような配布単位として利用できる。

  • サーバレス実行の基盤として、高速な軌道が可能なWasmが利用されている(Shopifyなど)
  • Cloudflare workersなどのCDNエッジコンピューティングでは、Wasmで処理を書くことができる
  • Dockerのコンテナの代替として、軌道が高速なWasmの利用が検討されている。

ユーザによるプラグイン

Wasmのサンドボックス性として利用する。

  • Shopify functionsでは、加盟店・アプリ独自ロジックをWasmとして登録し、Shopify側で実行することができる
  • EnvoyはWasm拡張をHTTPフィルタなどとして使うことができる

組み込み

  • WAMRは、ファームウェア全体を書き換えずアプリを書き換えられ、異なるアーキテクチャでも同じプログラムを動かせるIoT向けランタイムとして開発された
    • 同様に、組み込み機器への動的な機能追加も可能

ブロックチェーン

  • NEARでは、Rustなどで書いたスマートコントラクトをWasmにコンパイルし、チェーン上で実行する
  • StellarのSorobanでは、Rustで書いたスマートコントラクトをWasmにコンパイルし、stellar-core内のWasmインタープリターで実行する

コンポーネントモデルとしての利用

複数のプログラミング言語で書かれた資産を相互運用できる。実用化例は発見できず。