薊畑

シリーズWasmバイナリを読めるようになろうWasmのデータ型

WebAssemblyのコードでは、全てのデータを単なるバイト列として扱っていた元祖のアセンブリとは異なり型システムが導入されている。実行前の静的解析で、これらの型を用いてコードが安全かどうかを検証している。

以下で、Wasmに存在する具体的な型と、それがどんな状況で登場するのかを観察する。

Wasmの抽象意味論では、ここで紹介する以外に様々な型が登場するが、Wasmコードの動作を理解するために必要な型のみを紹介するに留める。

非参照型

数値型

数値型としては、以下の4種類がある。符号の有無は区別しない。

i32: 32ビット整数
i64: 64ビット整数
f32: 32ビット浮動小数点数
f64: 64ビット浮動小数点数
Cfloat numeric(int a, long b, float c) {
    return a + b + c;
}
Wasm(func $numeric (param i32 i64 f32) (result f32)
  local.get 0
  i64.extend_i32_s   ;; スタックトップの値を64bitに拡張する
  local.get 1
  i64.add
  f32.convert_i64_s  ;; スタックトップの値をf32に変換する
  local.get 2
  f32.add)

ベクトル型

整数または浮動小数点数

v128: 128ビットベクトル型
Ctypedef int Vector __attribute__((vector_size(16)));

Vector add_vectors(Vector a, Vector b) {
    return a + b;
}
Wasm(func $add_vectors (param v128 v128) (result v128)
  local.get 1
  local.get 0
  i32x4.add)

参照型

以下で説明するデータ型は、これまでの型とは異なり直接的に扱うことができない。そのため、ストアに値の実体を配置しスタック等にはその参照を持つ。 実体としてヒープ型、参照としては参照型を用いる。

ヒープ型

値型

i31:      整数型(実際にはストア内に保存されない)

複合型

1. 構造体

複数の型のリストを持つ。

Wasm(type $Point
  (struct
    (field (mut i32))
    (field (mut i32))
  )
)
2. リスト

この例では、後述する参照型を用いて、座標の配列を作っている。

Wasm(type $Array
  (array (ref null $Point))
)
3. 関数
Wasm(type $F
  (func (param i32 i32) (result i64))
)

これらの複合型を使う際には、型インデックス(type use)を用いる。 ここで定義された型($Pointや$Array)はモジュール中の型コンポーネントに保管されており、コード中などでアクセスする際には、そのインデックスを用いる。例えば、コード中で新しい$Point型の値を作る際には、

Wasmi32.const
i32.const 20
struct.new $Point

とする。ここで、$Pointには型コンポーネント内での番号である値、例えば0が入っている。

上位型・下位型

ヒープ型には、具体的な型のスーパーセットやサブセットが存在している

any:      すべての型のスーパーセット
eq:       比較可能な型のスーパーセット
none:     すべての方のサブセット

struct:   全ての構造体のスーパーセット
array:    全ての配列のスーパーセット

func:     すべての関数型のスーパーセット
nofunc:   すべての関数型のサブセット

exn:       例外参照のスーパーセット
noexn:     例外参照のサブセット
extern:    外部参照のスーパーセット
noextern:  外部参照のサブセット

参照型

ランタイムのストアへの参照を表す。

i31ref     i31への参照(実態は参照ではない。後述)
structref  構造体への参照
arrayref   配列への参照(以下同じ)
anyref
eqref
nullref
funcref
nullfuncref
exnref
nullexnref
externref
nullexternref

参照型は32bitの値として表現されるが、先頭1ビットは参照か、i31の即値かの区別のために用いられる。例えば最初のビットが0なら即値、1なら参照として、参照テーブル内のインデックスとして扱われる。こうすることで、他の参照と同じ枠組みで値型を扱い、余計な記憶領域も使わないでいることができる。

再帰型

再帰型は、複数の相互参照をする型をひとまとめにするための構造であり、具体的にコード中で使える型ではない。

例えば、お互いに参照を持つ型Aと型Bは、相互参照をするということをシステムに通知するため、一箇所にまとめて定義する必要があり、その単位が再帰型である。

Wasm(rec
  (type $A
    (struct
      (field (mut (ref null $B)))
    )
  )
  (type $B
    (struct
      (field (mut (ref null $A)))
    )
  )
)

全ての複合型は、再帰型を用いて定義されている。ヒープ型の項で例示した$Point型も、バイナリでは内部にstructを一つだけ持つ再帰型として表現される。

例

以下は、struct、array、externrefを使うWasmコードの例である。

Cstruct Entry { int value; void *object; };

int example(void *object) {
    struct Entry entries[1] = {{42, object}};
    return entries[0].value;
}
Wasm(module
  (type $Entry (struct (field i32) (field externref)))
  (type $Entries (array (ref null $Entry)))
  (func (export "example") (param $object externref) (result i32)
    (struct.get $Entry 0
      (ref.as_non_null
        (array.get $Entries
          (array.new $Entries
            (struct.new $Entry (i32.const 42) (local.get $object))
            (i32.const 1))
          (i32.const 0)))))
)