WAT (WebAssembly Text Format) と Common Lisp で遊ぶ ~小ネタ: 文字列出力マクロ~
相変わらずCommon Lisp + WAT (WebAssembly Text Format)で遊んでいますが、文字列の出力もできないと遊ぶには不便な感じがしたので、公式で紹介されている方法を参照しつつ、前々回の記事で作ったマクロつきパーサも活用しつつ、前回の記事で作ったメモリアロケータも利用しつつ文字列出力を簡易に行っていく記事です。
前々回記事: eshamster.hatenablog.com
前回記事: eshamster.hatenablog.com
目次
公式で紹介されている方法
まず公式のドキュメントである Understanding WebAssembly text format -WebAssembly Memory-で紹介されている方法に触れます(この項のコードは同ページから引用しています)。
まず、WASM側にimportさせる consoleLogString
をJavaScript側で定義します。これは、WASMとの共有メモリ(memory
)上の offset
~offset+length-1
間をUTF8エンコードされたバイト列と解釈してデコードし、結果を console.log
で出力するという代物です。
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); }
この memory
, consoleLogString
は次のようにしてWASM側に渡します。
var importObject = { console: { log: consoleLogString }, js: { mem: memory } }; WebAssembly.instantiateStreaming(fetch('logger2.wasm'), importObject) .then(obj => { obj.instance.exports.writeHi(); });
WASM側は次のようになります。data セグメントを利用して、文字列 "Hi" をUTF8エンコーディングしたバイト列を memory
上に配置します。
(module ;; consoleLogString を $log として improt (import "console" "log" (func $log (param i32 i32))) (import "js" "mem" (memory 1)) ;; "Hi" をUTF8エンコーディングして memory 上に配置する (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))
ということで、consoleLoeString
が memory
上のバイト列を0~1(バイト単位)まで読み取ってUTF8デコードし、"Hi" を出力することになります。
func 中で同じことをしたい
C言語などでは固定文字列には固定のアドレスが割り付けられるようなので、ちゃんとしたコンパイラをつくる場合は前記の data
セグメントをうまく使ってそうしたやり方を模倣するのが良いでしょう。が、もう少しカジュアルに遊びたいので、関数定義(func
)中で固定文字列を出力する方法について(無駄に)考えてみます。
JavaScript側は前項から特に変更はありませんが、WAT側は data
セグメントが func
中では利用できないので、別の方法を考える必要があります。
別の方法といっても、地道に自分でUTF8エンコードした文字列を memory
中に埋めていくしか(たぶん)ありません。そのため、"Hi"を出力するには次のようになります(関数呼び出し等はS式形式で書いています)。
(func test-log-string (i32.store8 (i32.const 0) (i32.const 72)) ; 72 = "H" (i32.store8 (i32.const 0) (i32.const 105)) ; 105 = "i" (call $log (i32.const 0) (i32.const 2)))
Asciiコードの範囲内ならまだマシで、「斑鳩」などと出力しようとすると次のようになります。
(func test-log-string ;; "斑" = (230 150 145) (i32.store8 (i32.const 0) (i32.const 230)) (i32.store8 (i32.const 1) (i32.const 150)) (i32.store8 (i32.const 2) (i32.const 145)) ;; "鳩" = (233 179 169) (i32.store8 (i32.const 3) (i32.const 233)) (i32.store8 (i32.const 4) (i32.const 179)) (i32.store8 (i32.const 5) (i32.const 169)) (call $log (i32.const 0) (i32.const 6)))
全然カジュアルじゃないですね。
マクロで簡単に書けるようにする
前々回の記事ではCommon LispにWATを書かせることができるようになりました。ここで一番やりたかったのがマクロの導入ですが、それを利用してもっと簡単に固定文字列の出力を書けるようにしてみます。なお、このWAT生成部分は watson というライブラリに切り出してみました。
先に、前項の test-log-string
(と合わせてimport, export部分も)をこのwatsonを使って書く場合は次のようになります。
(defimport.wat log console.log (func ((i32) (i32)))) (defimport.wat mem js.mem (memory 1)) (defun.wat test-log-string () () ;; "斑" = (230 150 145) (i32.store8 (i32.const 0) (i32.const 230)) (i32.store8 (i32.const 1) (i32.const 150)) (i32.store8 (i32.const 2) (i32.const 145)) ;; "鳩" = (233 179 169) (i32.store8 (i32.const 3) (i32.const 233)) (i32.store8 (i32.const 4) (i32.const 179)) (i32.store8 (i32.const 5) (i32.const 169)) (log (i32.const 0) (i32.const 6))) (defexport.wat test-log-string (func test-log-string))
さて、これを次のように書けるように log-string
マクロを作成します。なお、前回の記事でメモリアロケータ(malloc
, free
)を作ったので、それを使ってきちんとメモリを確保・解放する仕込みとして変数 ptr
を用意しています*1。
(defun.wat test-log-string () () (let ((ptr i32)) (log-string "斑鳩" ptr)))
この log-string
マクロの定義は次のようになります。
(defmacro.wat log-string (text var-ptr) (unless (stringp text) (error "input should be string. got: ~A" text)) (let* ((octets (flexi-streams:string-to-octets text :external-format :utf-8)) ;; octets は8バイト単位、malloc は32バイト単位であることに注意 (alloc-size (ceiling (/ (length octets) 4)))) `(progn (set-local ,var-ptr (malloc (i32.const ,alloc-size))) ,@(loop :for i :from 0 :below (length octets) :collect `(i32.store8 (i32.add (i32.mul ,var-ptr (i32.const 4)) (i32.const ,i)) (i32.const ,(aref octets i)))) (log (i32.mul ,var-ptr (i32.const 4)) (i32.const ,(length octets))) (free ,var-ptr))))
リストを出力するまではCommon Lispの領域なので、当然Common Lispライブラリも利用できます。ここでは、flexi-streamsを利用してUTF8エンコードしたバイト列 octets
を生成しています。これを元に malloc
で確保するサイズ alloc-size
を計算しておきます。これらの情報を利用して、(progn
以下で下記を展開します。
alloc-size
分のメモリをmalloc
する(32bit単位)- 確保したアドレスの先頭からバイト列の値を詰めていく(8bit単位)
log
に格納したバイト列を指すようにoffset, lengthを渡す(8bit単位)free
で確保したメモリを解放する
ということで、次のように書けるようになります。先程は省略しましたが、自作 malloc
, free
を使う準備として init-memory
も呼んでおきます。
(defun.wat test-log-string () () (let ((ptr i32)) (init-memory) (log-string "斑鳩" ptr)))
log-string
マクロ部分を展開すると次のようになります。
(defun.wat test-log-string () () (let ((ptr i32)) (init-memory) (progn (set-local ptr (malloc (i32.const 2))) ;; "斑" = (230 150 145) (i32.store8 (i32.add (i32.mul ptr (i32.const 4)) (i32.const 0)) (i32.const 230)) (i32.store8 (i32.add (i32.mul ptr (i32.const 4)) (i32.const 1)) (i32.const 150)) (i32.store8 (i32.add (i32.mul ptr (i32.const 4)) (i32.const 2)) (i32.const 145)) ;; "鳩" = (233 179 169) (i32.store8 (i32.add (i32.mul ptr (i32.const 4)) (i32.const 3)) (i32.const 233)) (i32.store8 (i32.add (i32.mul ptr (i32.const 4)) (i32.const 4)) (i32.const 179)) (i32.store8 (i32.add (i32.mul ptr (i32.const 4)) (i32.const 5)) (i32.const 169)) (log (i32.mul ptr (i32.const 4)) (i32.const 6)) (free ptr))))
こんな風にマクロでわっと展開されると気分が良いですね。
*1:これが純粋にCommon Lispであればマクロ内で自分で変数を用意すれば良いのですが、WATでは関数冒頭でしか変数を用意できないのでそうもいかず...。ちょっとかっこ悪い。watsonパーサ側で中途に表れる変数宣言を関数冒頭に持ってくるなどすれば対処できそうですが、そこまではできておらず