あけましておめでとうございます。
以前WATを触ってみた際の続きとなる。
だいぶ間も空いてしまったが、書初めと称して今回はWATでFizz Buzzを書いてみようと思う。
WATのVScodeプラグイン
ちなみに前回はVimで試していたのだが、今回はVSCode環境で書くことにしてみた。
Vim環境では特にWasm関連のプラグインは入れずに試していたが、VSCode環境では下記の拡張機能をインストールしてみることにした。
(とくに前もって何かを調べたわけではない。単に好奇心というだけ)
リポジトリは下記。
というわけで、今回はWATでfizz buzzを書いてみる。
目次
- WATのVScodeプラグイン
- 目次
- WATで足し算をするだけの関数を作ってみる。
- WATで文字列を返す関数を作る
- WATでif文を使う
- WATでループする
- WATでFizz Buzz
- 参考にしたドキュメント
WATで足し算をするだけの関数を作ってみる。
まずはWATで文字列を返すだけの関数を作り、これをスタート地点にしようと思う。
基本的にMDNの下記のドキュメントを参考にしていく。
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の下記のドキュメントが参考になりそうだ
以下のようにかんたんなサンプルを作ってみる。
;; 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))
といった形で囲われている。
今回のコードでは 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でループする
下記のドキュメントが参考になった。
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でも同様のことはできる。
こちらを用いて、以下のような条件分岐を実装していく。
;; 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>
これでコンソールを開くと以下のようなコードが出力されている。
というわけで無事にWATでFizz Buzzできた。
これを今年の書き初めとする。
実際に動くコードはGitHubにも載せました。
参考にしたドキュメント
あと最近、オライリーから出ているハンズオンWebAssemblyも読んでいるが、こちらも良さそう。
プログラミング言語は主にC++を取り扱います
と書籍の説明欄には書かれているが、C++に特化した内容というよりかは WebAssembly を体系だって理解するような方向性なので、WebAssemblyには興味あるけど別にC++は使わないという人でも楽しく読める。