🍡
WASM入門

myuon

myuon

2022年4月11日
wasm

WASMについて調べたことをまとめておきます。

# はじめに

WebAssembly(以下 WASM) に入門するかと思い立って色々調べたことをまとめます。

この記事では、あんまり WASM のことを知らない人が大体の概要を掴み、wat をなんとなく読めるようになることを目的にしています。

# WebAssembly と wat

WASM には binary format と text format の 2 種類があります。前者は.wasm、後者は.wat の拡張子を持ちます。基本的には機械語とアセンブリ言語のような対応になっており、後者は WASM を手書きする時に使うためのやつです。それぞれ互いに wat2wasm, wasm2wat のツールにより相互変換ができます。

以下、基本的には wat 形式でプログラムを記述します。

# Hello, World 的な

以下に WASM(テキスト形式) のプログラムを示します。

(module
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
    local.get $lhs
    local.get $rhs
    i32.add))

S 式で書かれており、スタックマシンへの命令列としてプログラムを記述することが特徴的です。上記のプログラムは関数を1つ定義していますが、この関数を呼び出すと次のように実行されます。

;; 上記の関数の呼び出しプログラム断片
(
  i32.const 1
  i32.const 2
  call $add
)

;; 呼び出した時のおおよその動作
;; <スタック, 実行する行>の形式で記述します
;;
;; <[], i32.const 1> 1をpush
;; <[1], i32.const 2> 2をpush
;; <[1,2], call $add> $addを呼び出し(引数はスタックに乗っている1と2)
;; <[1,2], local.get $lhs> $lhsを取得してpush
;; <[1,2,1], local.get $rhs> $rhsを取得してpush
;; <[1,2,1,2], i32.add> add
;; <[1,2,3], _> returnの処理を行なって呼び出し側に戻る。この時に戻り値だけを保存して引数などは不要なのでpopする
;; <[3], _> プログラムの終了時、stackには3が積まれた状態になる

# Try Online

上記のプログラムについて、例えば以下の wat2wasm demo で試すことができます。

wat2wasm demo

WAT コードとして例えば以下を、

(module
  (func $add (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add)
  (func (export "main") (result i32)
    i32.const 1
    i32.const 2
    call $add)
)

そして JS コードとして例えば以下を

const wasmInstance = new WebAssembly.Instance(wasmModule, {});
const { main } = wasmInstance.exports;
console.log(main());

入力して実行すると、 1+2 を計算させることができます。

# JavaScript FFI

WASM と JavaScript は相互に呼び出しが可能です。JavaScript から呼び出す場合、WASM の関数に名前をつけて export したものを JS 側から呼ぶことになります。逆に WASM から呼び出す場合、 global を使って関数をインポートして使うことになります。

詳細は MDN などを参照してください。

# 基本的なデータ構造

WASM にはオペランドに指定できる型として i32, i64, f32, f64 の 4 種類があります。(意味は見たままです)

そのほかに基本的なデータ構造(?)としてテーブルが用意されています。これはリサイズ可能な配列で、関数などの参照を入れて dynamic dispatch をするために使われます。 テーブルの宣言と、テーブルに入っている関数参照の呼び出しは例えば以下のようにできます:

(module
  ;; テーブル宣言部(長さ2の関数参照を入れるテーブルを宣言)
  (table 2 funcref)
  (func $f1 (result i32)
    i32.const 42)
  (func $f2 (result i32)
    i32.const 13)
  ;; elemでテーブルに参照をセット
  (elem (i32.const 0) $f1 $f2)


  (type $return_i32 (func (result i32)))
  (func (export "callByIndex") (param $i i32) (result i32)
    local.get $i
    ;; ここでdispatch
    call_indirect (type $return_i32))
)

call_indirect で呼び出します。呼び出す時に引数として、関数の型を指定します(型は type 命令により事前に引数と戻り値を定義しておく必要があります)。この型は型チェックによりチェックが行われます。 また、呼び出す関数はそのモジュール内のテーブルの、スタックの先頭にある数値を index とするとものです。この場合、 loca.get $i$i に入った値がスタックの top に積まれるので、それが 0 の場合はテーブルの 0 番目の関数を、1 の場合はテーブルの 1 番目の関数を呼び出すことになります。

# Memory

さて、WASM では i32,i64,f32,f64,funcref とテーブルがあることを見ました。一方でプログラムを書くためには文字列やもっと複雑なデータ構造を扱える必要があります。このようなケースのために WASM ではメモリーが用意されています。

メモリーは基本的にはただのバイト列で(JS 側からは単なる ArrayBuffer としてみえます)、WASM からはアドレスを経由して操作します。WASM の命令である i32.loadi32.store を使ってメモリ上の値を読み書きできます。

例として、メモリ上に "Hello,World" の文字列をセットし、WASM からそれを "Hello,WorldHello,World" のように複製して JS でそれを出力するだけのプログラムをあげます

(module
  (import "js" "mem" (memory 1))
  (data (i32.const 0) "Hello,World")
  (func $repeat (param $i i32)
    ;; if i == 22 { return }
    local.get $i
    i32.const 22
    i32.eq
    if
    else
      ;; mem[i+11] = mem[i]
      local.get $i
      i32.const 11
      i32.add

      local.get $i
      i32.load

      i32.store
      ;; repeat(i+1)
      local.get $i
      i32.const 1
      i32.add
      call $repeat
    end
  )
  (func (export "main")
    i32.const 0
    call $repeat
  )
)
const memory = new WebAssembly.Memory({ initial: 1 });

const wasmInstance = new WebAssembly.Instance(wasmModule, {
  js: { mem: memory },
});
const { main } = wasmInstance.exports;
console.log(main());

const bytes = new Uint8Array(memory.buffer, 0, 22);
const string = new TextDecoder("utf8").decode(bytes);
console.log(string);

# おわりに

WASM が何となく読み書きできるようになってきたので、C 言語から WASM へのコンパイラを書いてみたくなりました。

# 参考文献