at backyard

Color my life with the chaos of trouble.

WATでFizz Buzzにチャレンジ

あけましておめでとうございます。

以前WATを触ってみた際の続きとなる。

だいぶ間も空いてしまったが、書初めと称して今回はWATでFizz Buzzを書いてみようと思う。

shinshin86.hateblo.jp

WATのVScodeプラグイン

ちなみに前回はVimで試していたのだが、今回はVSCode環境で書くことにしてみた。

Vim環境では特にWasm関連のプラグインは入れずに試していたが、VSCode環境では下記の拡張機能をインストールしてみることにした。
(とくに前もって何かを調べたわけではない。単に好奇心というだけ)

marketplace.visualstudio.com

リポジトリは下記。

github.com

というわけで、今回はWATでfizz buzzを書いてみる。

目次

WATで足し算をするだけの関数を作ってみる。

まずはWATで文字列を返すだけの関数を作り、これをスタート地点にしようと思う。

基本的にMDNの下記のドキュメントを参考にしていく。

developer.mozilla.org

WATの関数を書く前に、wasmの実行はスタックマシンとして定義されていくということについて気をつけていきたい。

あまりここらへんのことに明るくはないので、ここから先もしかしたら誤った理解をしている箇所もあるかもしれないが、スタックマシンの基本的な考え方としては すべての命令がスタックから特定の数の値をプッシュし、ポップするようにする必要がある

なんのことやら、と自分でも書きながら思ったので、以下のようなTypeScriptで書いた関数を例にして説明する。
(ここでWATで書いたコードを例にすると、自分自身が混乱しそうに思えたのでTSにしといた)

const add = (n1: number, n2: number): number => {
    return n1 + n2;
}

この関数は数値型の引数を2つとり、2つの引数として渡した数値を合算して返す関数となる。

このようなコードをWasm上で実行する場合、挙動としては以下のようなものになる。

まずは引数の引数として n1 がスタックにつまれる。

次に第2の引数の n2 がスタックにつまれる。

スタックにつまれた2つの数値に対して add(合算処理) が実行される。

合算されて一つになった値が一つスタック上に存在している状態となる。
この値をreturnする。

これらの処理を実際にWATで書くと以下のようになる。

(module
  (func $add (param $n1 i32) (param $n2 i32) (result i32)
    local.get $n1
    local.get $n2
    i32.add
  )
  (export "add" (func $add))
)

$n1, $n2 というのが引数で、 result が返り値を表している。
ここではすべての引数と返り値は i32 という32ビット整数である。
上の図でも書いたように、$n1$n2 がスタック上につまれた後 i32.add が実行され、2つの数値が合算される。
そして一つになったスタック上の数値が返される。
なお、返り値の値と最後に残る(つまり返される予定の値)は必ず一致しなければならない。

例えば上のコードで、 i32.add の行を消した状態でwasmにコンパイルしようとすると、 wat2wasm 配下のようなエラーを吐き出す。

(module
  (func $add (param $n1 i32) (param $n2 i32) (result i32)
    local.get $n1
    local.get $n2
  )
  (export "add" (func $add))
)
$wat2wasm add.wat -o add.wasm
add.wat:4:5: error: type mismatch at end of function, expected [] but got [i32]
    local.get $n2
    ^^^^^^^^^

これは関数が終了した状態でのスタックマシンの状態と返り値が一致していないことによるエラーとなる。

例えば i32.addの行を消した状態で正常にコンパイルを完了させるには下記のようにする必要がある。

(module
  (func $add (param $n1 i32) (param $n2 i32) (result i32) (result i32)
    local.get $n1
    local.get $n2
  )
  (export "add" (func $add))
)

これは関数を実行した状態でスタックマシンに2つの i32 の値がつまれた状態となるが、返り値として i32 の返り値が2つであるため、正常にコンパイルされ、実行できる。

ちなみに (export "add" (func $add)) という行は add という関数名で export するということを意味している。

ではこのコードを add.wat という名称で保存して下記のようにコンパイルする。

wat2wasm add.wat -o add.wasm

そして以下のようなHTMLを書き、ローカルサーバを立ち上げて実行するとwasmの実行結果を確認できる。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>WAT sample</title>
  </head>
  <body>
    <script>
      WebAssembly.instantiateStreaming(fetch("add.wasm"))
      .then(obj => {
        const result = obj.instance.exports.add(10,2);
        console.log("result: ", result); // `result: 12` と表示される
      });
    </script>
  </body>
</html>

これでWAT(WASM)がスタックマシンであるという説明は以上となる。

WATで文字列を返す関数を作る

WASMは以下の4つの数値型が用意されている。

またこれとは別に参照型というものがあり、こちらは externref で定義できる。

externref という参照型について

これを使えば一応は文字列を利用できる。
(厳密には文字列型というわけではなく、ただ参照しているだけに過ぎない)

(module
  ;; 返り値を2つ返す場合、(result externref) (result externref)ではなく、下記のようにも書ける
  (func $hello (param $str1 externref) (param $str2 externref) (result externref externref)
    local.get $str1
    local.get $str2
  )
  (export "hello" (func $hello))
)

で、下記のようなコードをJS側から実行する。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>WAT string</title>
  </head>
  <body>
    <script>
      WebAssembly.instantiateStreaming(fetch("hello.wasm"))
      .then(obj => {
        const result = obj.instance.exports.hello("hello", "world");
        console.log(result);
      });
    </script>
  </body>
</html>

結果は ['hello', 'world'] という2つの文字列が格納された配列になる。

できればここで文字列を hello worldという形で連結して表示させたい。

JavaScriptの関数をインポートして使う

WasmではJSの関数をimportして利用することができる。

WATで用意されている機能では文字列連結は難しそうだったので(そもそも文字列型がない)、JavaScriptの関数側から文字列連結をして見るのはどうかと思い立った。

ここでは試しに配列内の文字列を連結して返すだけの関数をJS側で用意し、そちらをWATにimportして文字列連結を実現させるというアプローチを取ることにする。
※ちなみに今更ながらなぜ hello という関数名にしたのかという感じだが、あくまでサンプルということでここはスルーしてほしい...。

watのコードを以下に記載する。
externrefの変数を2つスタックさせた状態で、importした string.join関数を呼ぶというものになっている。
この後に記載するが join関数は2つの参照型の値を連結して返すだけの関数で、externref の値を一つ返す流れとなる。

(module
  (import "string" "join" (func $join (param externref externref) (result externref)))
  (func $hello (param $str1 externref) (param $str2 externref) (result externref)
    local.get $str1
    local.get $str2
    call $join
  )
  (export "hello" (func $hello))

ちなみにWebAssemblyでは2 階層の名前空間のインポート文を持っているようで、今回は string.join という形にしている。
これが1階層だとコンパイル時にエラーになる。

下記は仮に join という1階層のみで定義しようとした場合のエラー。

hello.wat:2:18: error: unexpected token "(", expected a quoted string (e.g. "foo").
  (import "join" (func $join (param externref externref) (result externref)))
                 ^

そしてJS側は以下のようになる。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>WAT string join sample</title>
  </head>
  <body>
    <script>
      const importObject = {
        string: {
          join: function (str1, str2) {
            return str1 + str2;
          }
        }
      };

      WebAssembly.instantiateStreaming(fetch("hello.wasm"), importObject)
      .then(obj => {
        const result = obj.instance.exports.hello("hello", "world");
        console.log(result);
      });
    </script>
  </body>
</html>

これで実行すると、helloworld がコンソール上に表示させる。

WATでif文を使う

MDNの下記のドキュメントが参考になりそうだ

developer.mozilla.org

以下のようにかんたんなサンプルを作ってみる。

;; if.wat
(module
  (import "console" "log" (func $log(param i32)))
  (func (export "ifsample") (param $n1 i32) (result i32)
    (local $result i32)
    (if (i32.eq (i32.const 1)(local.get $n1))
      (then
        (local.set $result (i32.const 100))
      )
      (else
        (local.set $result (i32.const 0))
      )
    )
    
    local.get $result
    return
  )
)

以下のコードが if のブロックとなる。

  (if (i32.eq (i32.const 1)(local.get $n1))
      (then
        (local.set $result (i32.const 100))
      )
      (else
        (local.set $result (i32.const 0))
      )
    )

私はまったくもって詳しくないので間違ったことを書いているかもしれないが、Lispなどで採用されているS式をwatでも採用されており、例えば2つの数字が同じかどうかを判定する箇所も (i32.eq (i32.const 1)(local.get $n1)) といった形で囲われている。

ja.wikipedia.org

今回のコードでは if の前に (local $result i32) でlocal変数を定義してこちらに一度if内での結果を格納している。
また if 内のブロックにおける処理もブロックとして定義する必要があり、

      (then
        (local.set $result (i32.const 100))
      )

という形で (local.set~ 内も囲う必要がある。

最初ここのカッコが抜けていて意図が不明なエラーに悩まされた。

そして、その結果の内容を return する前に local.get $result で一度スタックに積んで、そのスタックに積んだ値を return するという処理の流れになっている。

こちらを以下のようにコンパイルして、

wat2wasm if.wat -o if.wasm

以下のHTMLコードから呼び出せば意図したコードが実行されているのが確認できる。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>WAT if sample</title>
  </head>
  <body>
    <script>
      WebAssembly.instantiateStreaming(fetch("if.wasm"), {console})
      .then(obj => {
        const result = obj.instance.exports.ifsample(1);
        console.log(result);
      });
    </script>
  </body>
</html>

WATでループする

下記のドキュメントが参考になった。

developer.mozilla.org

loop 文でループは可能なようだが、どうやら loop それ自体を書いただけでは意図するループはかけないようだ。

以下は 1~100 までを出力するだけのサンプル。
(内容としては上のMDNないのサンプルとそこまで変わらないものとなった)

(module
  (import "console" "log" (func $log(param i32)))
  (func (export "loopsample")
    ;; 変数として$iを定義する。初期値は0
    (local $i i32)

    (loop $my_loop

      ;; $iに対して1を追加
      local.get $i
      i32.const 1
      i32.add
      local.set $i

      ;; 現在の値をconsole.logに出力する
      local.get $i
      call $log

      ;; もし$iの値が100以下であれば、再びループする
      local.get $i
      i32.const 100
      i32.lt_s
      br_if $my_loop
    )
  )

以下のHTMLをローカルサーバ上で読み出すことでログに1〜100までの値が出力されていることが確認できる。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>WAT loop</title>
  </head>
  <body>
    <script>
      WebAssembly.instantiateStreaming(fetch("loop.wasm"), {console})
      .then(obj => {
        obj.instance.exports.loopsample();
      });
    </script>
  </body>
</html>

WATでFizz Buzz

ここまでで、WATで

  • 関数の作り方
  • 文字列の扱い方
  • if文
  • loop文

の概要を少しだけ理解できた。

ここまでくれば FizzBuzz できるだろう、ということでやってみる。

WATで余りを割り出す

JavaScriptであれば % を用いて余りを計算していくと思うが、watでも同様のことはできる。

developer.mozilla.org

こちらを用いて、以下のような条件分岐を実装していく。

  ;; 15で割った数が0の場合、ifブロック内の処理が実行される
  (if (i32.eq (i32.rem_u (local.get $n)(i32.const 15))(i32.const 0))
      (then
        (local.get $fizzbuzz)
        (call $logStr)
        (local.set $fbFlag (i32.const 1))
      )
    )

WATで書いたFizz Buzzのコード

というわけで、以下がコード。
(もっと良い書き方がありそうだが...疲れたので一旦これで)

(module
  ;; それぞれの型に沿った出力を行いため
  (import "console" "log" (func $logStr(param externref)))
  (import "console" "log" (func $logNum(param i32)))
  (func $display (param $n i32) (param $fizz externref) (param $buzz externref) (param $fizzbuzz externref)
    ;; fizz, buzz, fizzbuzz、いずれにも該当しない場合はフラグを用いて数値を出力するように処理を分岐させる
    (local $fbFlag i32)

    (if (i32.eq (i32.rem_u (local.get $n)(i32.const 15))(i32.const 0))
      (then
        (local.get $fizzbuzz)
        (call $logStr)
        (local.set $fbFlag (i32.const 1))
      )
    )

    (if (i32.eq (i32.rem_u (local.get $n)(i32.const 5))(i32.const 0))
      (then
        (local.get $buzz)
        (call $logStr)
        (local.set $fbFlag (i32.const 1))
      )
    )

    (if (i32.eq (i32.rem_u (local.get $n)(i32.const 3))(i32.const 0))
      (then
        (local.get $fizz)
        (call $logStr)
        (local.set $fbFlag (i32.const 1))
      )
    )

    (local.get $fbFlag)
    (if
      (then)
      (else
        (local.get $n)
        (call $logNum)
      )
    )
  )

  (func $fizzbuzz (param $fizz externref) (param $buzz externref) (param $fizzbuzz externref)
    ;; 変数として$iを定義する。初期値は0
    (local $i i32)

    (loop $my_loop

      ;; $iに対して1を追加
      local.get $i
      i32.const 1
      i32.add
      local.set $i

      ;; display関数に渡すパラメータをスタックに載せてから関数を呼び出す
      local.get $i
      local.get $fizz
      local.get $buzz
      local.get $fizzbuzz
      call $display

      ;; もし$iの値が100以下であれば、再びループする
      local.get $i
      i32.const 100
      i32.lt_s
      br_if $my_loop
    )
  )
  (export "fizzbuzz" (func $fizzbuzz))
)

これを下記のようにコンパイルする。

wat2wasm fizzbuzz.wat -o fizzbuzz.wasm

そしてローカルサーバを起動して以下のHTMLにアクセスする。

<!doctype html>
<html>

<head>
  <meta charset="utf-8">
  <title>WAT fizzbuzz</title>
</head>

<body>
  <script>
    WebAssembly.instantiateStreaming(fetch("fizzbuzz.wasm"), { console })
      .then(obj => {
        obj.instance.exports.fizzbuzz("Fizz", "Buzz", "Fizz Buzz");
      });
  </script>
</body>

</html>

これでコンソールを開くと以下のようなコードが出力されている。

Chromeのコンソール上で確認している

というわけで無事にWATでFizz Buzzできた。

これを今年の書き初めとする。

実際に動くコードはGitHubにも載せました。

github.com

参考にしたドキュメント

developer.mozilla.org

あと最近、オライリーから出ているハンズオンWebAssemblyも読んでいるが、こちらも良さそう。

www.oreilly.co.jp

プログラミング言語は主にC++を取り扱います と書籍の説明欄には書かれているが、C++に特化した内容というよりかは WebAssembly を体系だって理解するような方向性なので、WebAssemblyには興味あるけど別にC++は使わないという人でも楽しく読める。