LoginSignup
8

More than 3 years have passed since last update.

wasm S式 (WebAssemblyテキスト形式)入門

Last updated at Posted at 2019-08-09

通常は拡張子 .wat で表される、wasmのS式について
バイナリ形式をwasm,人間が読めるS式のものをwatと呼ぶ。

S式

watの例
(module (memory 1) (func))

watの基本的なコードの単位はモジュールで、S式はモジュールのツリー構造を表現する。
一つのモジュールは一つの(...)にあたる。括弧の中の1つ目のラベルmoduleはこのモジュールの種類を、それ以下のラベル(memory 1)(func)がモジュールの子ノードにあたる。
この例ではモジュールが入れ子構造になっているが、子ノードは存在しなくてもよい。そのため、最小構成は以下のようになる。

watモジュールの最小構成
(module)

ちなみに上の最小構成をバイナリに変換すると、(参考:WebAssembly テキストフォーマットから wasm に変換する)8バイトで表現されるモジュールヘッダのみになる。

0000000: 0061 736d      ; WASM_BINARY_MAGIC
0000004: 0100 0000      ; WASM_BINARY_VERSION

watモジュール

モジュールの括弧(...)の先頭は、モジュールの種類を表す。
以下の種類がある。

説明
type 関数の型を宣言する
import 外部から関数やデータを読み込み、wasm内で使えるように定義する
func 関数を定義する
global グローバル変数を宣言する
export 外部に関数やデータをエクスポート
elem テーブルの値を定義する
data メモリの初期値を定義する

funcモジュール

wasm内での関数定義は、以下のような形式になる。

( func <signature> <locals> <body> )

シグネチャ

signatureは、単体パラメータを(param i32)、戻り値を(return i32)のように書く。
したがって、2つの32ビット整数を引数にとり、64ビット浮動小数点数を返すバイナリ関数は次のように記述する:

(func (param i32) (param i32) (result f64) ... )
;;はコメントを表す。以下のS式でも有効
(func (param i32 i32) (result f64) ... )

また、現在のwasmでは、以下の4つの型が有効である。

  • i32 32bit整数
  • i64 64bit整数
  • f32 32bit浮動小数点数
  • f64 64bit浮動小数点数

ローカル変数

シグネチャの後に、(local i32)のように型付けされたローカル変数の並びが続く。

スタックマシン

関数が呼ばれると、空のスタックからスタートして、命令(local.getなど)にスタックにデータが積まれていく。

(func (param $p i32)
  local.get $p
  local.get $p
  i32.add)

local.get $p$pが指し示すものをスタックに詰め込む。それを2回行なった後にi32.addを行なっているので、スタックには$p + $pが詰め込まれている。

本体で有効なオペコードの例

  • local.get 0 ローカル変数(0番目)からスタックへ
  • local.set 0 スタックからローカル変数(0番目)へ
  • i32.const 19 19という定数(型はi32)を定義してスタックへ
  • i32.add スタックの最上の2つを足して消し、結果をスタックに書き込む

全て見るには、以下を参照。
https://webassembly.org/docs/semantics/

関数を呼ぶ

外部(Javascriptなど)から関数を呼び出すには、exportを用いる。

(module
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
    get_local $lhs
    get_local $rhs
    i32.add)
  (export "add" (func $add))
)

一方、定義した関数をwasmモジュール内で呼び出すには、callを用いる。

(module
  (func $getAnswer (result i32)
    i32.const 42)
  (func (export "getAnswerPlus1") (result i32)
    call $getAnswer
    i32.const 1
    i32.add))

memoryの使い方

文字列や複雑なデータ型を用いるのに、wasmではメモリを用いる。i32.loadやi32.storeを用いて、線形メモリの読み書きをする。javascriptと文字列などをやり取りするのは、メモリを通して行う。

//メモリを作る
var memory = new WebAssembly.Memory({initial:1});

//メモリのオフセットと長さから文字列を書き出す関数を作る
function consoleLogString(offset, length) {
  var bytes = new Uint8Array(memory.buffer, offset, length);
  var string = new TextDecoder('utf8').decode(bytes);
  console.log(string);
}

//wasmに渡す関数とメモリのデータを作る。
var importObject = { console: { log: consoleLogString }, js: { mem: memory } };

WebAssembly.instantiateStreaming(fetch('logger2.wasm'), importObject)
  .then(obj => {
    obj.instance.exports.writeHi();
  });

wasm内では、以下のようにしてメモリを利用する。

(module
  (import "console" "log" (func $log (param i32 i32)))
  (import "js" "mem" (memory 1))
  (data (i32.const 0) "Hi")
  (func (export "writeHi")
    i32.const 0  ;; pass offset 0 to log
    i32.const 2  ;; pass length 2 to log
    call $log))

(import "js" "mem" (memory 1))でメモリを読み込む。(memory 1)で、1ページ(64kB)分のメモリを読み込んでいる。
(data (i32.const 0) "Hi")では、i32.constで指定されたオフセットであるメモリの0番目以降に文字列"Hi"を書き込んでいる。オフセット0と長さ2から"Hi"を出力できる"writeHi"をexportする。

tableの使い方

テーブルは、以下のように定義する。

(module
  (table 2 anyfunc)
  (elem (i32.const 0) $f1 $f2)
  (func $f1 (result i32) ;; 42を返す関数
    i32.const 42)
  (func $f2 (result i32) ;; 13を返す関数
    i32.const 13)
  ...
)

i32.const 0はオフセットをあらわし、この場合はテーブルのindexが0から始める。0が関数$f1,1が関数$f2に対応する。

テーブルで定義した関数を使うには、以下のようにする。

(type $return_i32 (func (result i32))) ;; if this was f32, type checking would fail
(func (export "callByIndex") (param $i i32) (result i32)
  local.get $i
  call_indirect (type $return_i32))

これにより、定義したテーブルの$i番目の関数($iはスタックに置かれた値)をcall_indirectで呼び出すことができる。関数の型は(type $return_i32)に一致しているかどうか実行時にチェックされる。一致すると、WebAssembly.RuntimeError 例外がスローされる。
以上2つを合わせると、以下のようにしてテーブルで定義した関数を呼び出すことが出来る。

(module
  (table 2 anyfunc)
  (func $f1 (result i32)
    i32.const 42)
  (func $f2 (result i32)
    i32.const 13)
  (elem (i32.const 0) $f1 $f2)
  (type $return_i32 (func (result i32)))
  (func (export "callByIndex") (param $i i32) (result i32)
    get_local $i
    call_indirect (type $return_i32))
)

tableはミュータブルなので、Javascriptからは動的に操作できる(←やばい)

参考

WebAssembly テキストフォーマットを理解する / MDN
公式ドキュメント

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8