🙆

WasmGCについて予習する

2023/12/02に公開

この記事はWebAssembly Advent Calendar 2023 2日目の記事です.


ガベージコレクタ(WasmGC)[1]が10月頃にChromeとFirefoxでデフォルトで有効になり,標準化目前ということで大いに盛り上がったのは記憶に新しいかと思います.
これはChromeが対応したというよりはChromeが採用しているJavaScriptのランタイムであるV8がデフォルトでWasmGCが有効になったというのが正確な表現で,DenoでもDartからビルドしたWasmを実行する際にGCを有効にするフラグを立てる必要がなくなり,実行しやすくなったなぁと皆さんも感じているかと思います.

-$ deno run --allow-read --v8-flags=--experimental-wasm-gc run.js
+$ deno run --allow-read run.js

さて,ガベージコレクタがWasmのランタイムでも使えるようになったと聞くと「JavaもWasmをサポートできるようになるのか(小並感)」と思う人が大多数だと思いますが,実際にはC/C++やRustといったガベージコレクタを用いない言語にもメリットがある話だったりします.
そもそもの話として,GC言語であるGoも1.11からWasmをサポートしていますので,WasmGCがなくともGC言語をWasmに移植することは可能でした.

では,WasmGCが使えるようになる前と後で何が変わるのでしょうか?
これに関してV8プロジェクトのブログで紹介されています.

Wasmのメモリ管理

WasmのVMにはWasmのプログラムが自由に扱えるメモリが存在します.
ただし,Wasm自体はN番地に値を書き込む,M番地の値を読み込む,という原始的な操作しか用意しておらず, malloc/freeといった操作は用意されていません.

Wasm上のメモリ操作

ではC/C++やRustがメモリ操作をどのように行なっているかというと,malloc/freeもWasmのバイナリに同梱しています.

C/C++やRustのメモリ操作のイメージ

ではGo言語などのGC言語がどのようにWasmをサポートしているかというと,GCの機能も込みでWasmにビルドされていました.
そのため,WasmGCがランタイムに実装されるとGC言語はGCの実装をWasmに持ち込む必要がなくなるため,Wasmのバイナリサイズの削減につながります.

GC言語のメモリ操作

既存の課題

GoからWasmをビルドしたことがあるでしょうか?
Goは全て同梱のシングルバイナリを生成します.
これは実行プログラムのサイズが大きくなりやすいという欠点はありますが,1つの実行プログラムを配置するだけで動くというメリットもあり,コンテナイメージの作成では強力な強みとなっています.
これはWasmでも同様で,Hello, World を出力するだけのプログラムであっても約2-3MBくらいのバイナリサイズになってしまいます.

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, World")
}
$ GOOS=js GOARCH=wasm go build -o main.wasm main.go

サーバー上だとそれほど問題にはなりませんが,ウェブ上だと数MBのデータは悩みの種となります.
そのため,WasmGCがランタイムに実装されるとGC実装をWasmに組み込む必要がなくなるため,軽量化が期待されているのはいうまでもありません.

メモリの共有

問題は他にもあります.
例えば,Wasmのメモリ上で扱っている次のようなオブジェクトをJSから参照したいとき,どのようにしたら良いでしょうか?

type P struct{
    X int32
    Y int32
}

このオブジェクトをJSに渡す場合,あらかじめ渡すオブジェクトのデータ構造を決めておき,Wasmのメモリのアドレス情報をJSに伝える必要があります.
このようにすることで,WasmとJSは初めてデータを共有できるようになります.

WasmとJS間のデータ共有

type P = {
  x: number,
  y: number,
};

が,これには多くの問題があります.

  1. オブジェクトは常にコピーされてしまうこと.
  2. JSにJSらしからぬバイナリ操作が発生してしまうこと.
  3. JSからはWasm上のメモリすべてにアクセス可能になってしまっていること.
  4. Wasmのプログラムから,メモリの解放タイミングがわからなくなってしまうこと.

1,2はある程度無視しても良いですし,3もセキュリティ的に問題はありますが,現状はこれが問題になる程のWasmの実装はそれほどないでしょう[2]

C/C++やRustにとって,4が一番の悩みでした.
Rustは malloc/freeのタイミングを言語の仕組みで管理することでメモリ安全を謳っていますが,Wasmの外からメモリ上のオブジェクトを参照できるようにする時点でいつメモリを解放されるのか,するべきなのかが分からなくなるため,メモリ安全でなくなってしまいます.

extern "C" {
    fn write(pointer: i32, length: i32);
}

pub fn exec() {
    let msg = "Hello, World".as_bytes();
    unsafe {
        let p = msg.as_ptr() as i32;
        let l = msg.len() as i32;
        write(p, l);
    }
}

wasm-packなどを利用しない場合,このようなコードを書くことになります.
この例だと writeを呼び出してプログラムが終了してしまうので多くを気にすることはありませんが,こういったコードを書いてしまうと, msgがいつまで参照されているのか,Rustから分からなくなってしまいます.

WasmGCと関係ないのでは?と思うかもしれませんが,実はWasmGCによってこの課題も解決されようとしているのです.

WasmGC以降のメモリ管理

WasmGCによって管理されるメモリは既存のWasmのメモリではなく,ヒープメモリ上のデータがWasmGCの管理対象となります.

WasmGCが管理するメモリ

WasmGCが管理するメモリでは,既存のメモリのようにメモリ番地を指定してNバイト読み書きするといったことはできません.
代わりに, structarrayといったオブジェクトが用意されています[3]

structというオブジェクトは,Wasmでは次のように定義することができます.

(type $point (struct
  (field $x f64)
  (field $y f64)
  (field $z f64)
))

このオブジェクトのインスタンスを生成するにはオペコード struct.new $point を用います.
各フィールドへの値の読み書きは struct.get $point $xstruct.set $point $y というふうに行われます.
DartからビルドしたWasmのバイトコードを見てみると,実際に struct.newstrcut.setなどが使われていることがわかります.

ChromeでWasmのバイトコードを確認

このように,WasmGCが導入されるとstructarrayといったオブジェクトと,そのオペコードを用いてオブジェクト操作を行うことができるようになり,JSでもメモリ番地を直接やりとりせずにオブジェクトの参照をやりとりできるようになるため,Wasmの外にメモリを公開する必要がなくなります.
もちろんGCを用いるため,WasmからJSに渡したオブジェクトの参照がいつ解放されるのかといったことを気にする必要もありません.

WasmGCを用いたWasmとJSのオブジェクト共有

そして, structarrayといった決まった形式のオブジェクトを定義できるのであれば,Goなどの言語の構造体とJSのオブジェクトのマッピングが比較的簡単に行えるようになることが期待できます.

type Point struct{  // (type $point (struct
    X float64       //   (field $x f64)
    Y float64       //   (field $y f64)
    Z float64       //   (field $z f64)
}                   // ))
type Point = {      // (type $point (struct
  x: number,        //   (field $x f64)
  y: number,        //   (field $y f64)
  z: number,        //   (field $z f64)
};                  // ))

残念ながら記事執筆時点でGCオブジェクトのJS APIの仕様を見つけきれませんでしたが,Wasmオブジェクトの参照ラッパーを用意すると書かれているため,今までのようなグルーコードを用意しなくてもよくなることが将来的には期待できるのではないでしょうか?[4]

まとめ

WasmGCは単にGC言語を移植するだけでなく,既存のC/C++やRustからビルドされたWasmが抱えていた課題も解決する仕様です.
WasmGCが完全に標準化されると,JSのグルーコードの量も減りますし,単なるWasmのバイトコードの軽量化以上のメリットをもたらします.

今後のWasmGCの動向から目が離せませんね🌲

脚注
  1. https://github.com/WebAssembly/gc ↩︎

  2. これがセキュリティ的に問題になるのは,不特定多数のモジュールを用いてシステムを組み出したときです. ↩︎

  3. https://github.com/WebAssembly/gc/blob/main/proposals/gc/MVP.md#structures ↩︎

  4. https://github.com/WebAssembly/gc/blob/main/proposals/gc/MVP-JS.md ↩︎

Discussion