🧩

WebAssemblyさくっと試すメモ

2022/08/08に公開

準備

brew install wabt
brew install wasmer
gem install wasmer
  • wabt は MS-DOS 時代を彷彿とさせる変換コマンド類の詰め合わせ
    • wat2wasm と wasm-objdump をよく使う
    • GitHub から clone してビルドする手順がよく紹介されているけど brew で入った
  • wasmer は WASM をコマンドラインで実行できるやつ
    • ブラウザで実行しようとしたら難しすぎたのでこれでいい
  • wasmer gem は wasmer の Ruby バインディング(?)
    • インストールに5分以上かかる
    • フリーズしたのかと思って何度か止めた
    • brew で入れてんのに wasmer 本体を独自にビルドしているっぽい

サンプルコード

a.wat
(module
  (func (export "foo") (result i32)
    (i32.const 1)))
  • foo を呼んだら 1 を返す
    • (i32.const 1) は 1 をスタックに push
    • (result i32) はスタックの直近の値を戻値にする
    • Emacs Lisp で (defun foo () 1) に相当する
  • 拡張子 wat は WebAssembly Text format のこと
  • どこから実行される?
    • 単に上から実行とはならないらしい
    • エントリーポイントも決まってないらしい
    • 名前はなんでもいいので外から呼ぶために export が必要らしい
  • module で囲まなかったら勝手に囲まれた

バイナリー変換

wat2wasm a.wat

a.wasm が出来上がる。

実行

$ wasmer a.wasm --invoke foo
# 1

export "foo" としてあったから foo を呼べる。

wasm に変換するのがめんどい

$ wasmer a.wat --invoke foo
# 1

別に wasm に変換する必要なかった。wasmer が wat2wasm 相当をやってくれていた。wabt いらんかった。

wasm バイナリーのコードの意味を知りたい

$ wat2wasm -v a.wat 2>&1
# 0000000: 0061 736d                                 ; WASM_BINARY_MAGIC
# 0000004: 0100 0000                                 ; WASM_BINARY_VERSION
# ; section "Type" (1)
# 0000008: 01                                        ; section code
# 0000009: 00                                        ; section size (guess)
# 000000a: 01                                        ; num types
# ; func type 0
# 000000b: 60                                        ; func
# 000000c: 00                                        ; num params
# 000000d: 01                                        ; num results
# 000000e: 7f                                        ; i32
# 0000009: 05                                        ; FIXUP section size
# ; section "Function" (3)
# 000000f: 03                                        ; section code
# 0000010: 00                                        ; section size (guess)
# 0000011: 01                                        ; num functions
# 0000012: 00                                        ; function 0 signature index
# 0000010: 02                                        ; FIXUP section size
# ; section "Export" (7)
# 0000013: 07                                        ; section code
# 0000014: 00                                        ; section size (guess)
# 0000015: 01                                        ; num exports
# 0000016: 03                                        ; string length
# 0000017: 666f 6f                                  foo  ; export name
# 000001a: 00                                        ; export kind
# 000001b: 00                                        ; export func index
# 0000014: 07                                        ; FIXUP section size
# ; section "Code" (10)
# 000001c: 0a                                        ; section code
# 000001d: 00                                        ; section size (guess)
# 000001e: 01                                        ; num functions
# ; function body 0
# 000001f: 00                                        ; func body size (guess)
# 0000020: 00                                        ; local decl count
# 0000021: 41                                        ; i32.const
# 0000022: 01                                        ; i32 literal
# 0000023: 0b                                        ; end
# 000001f: 04                                        ; FIXUP func body size
# 000001d: 06                                        ; FIXUP section size
  • 1 を返すだけでこんなに?
  • wasm からじゃなくて wat からわかる
  • やっぱり wabt いる
  • なぜかエラー出力するので標準出力に戻している

簡潔に表示する

$ wasm-objdump -d a.wasm
#
# a.wasm:   file format wasm 0x1
#
# Code Disassembly:
#
# 000020 func[0] <foo>:
#  000021: 41 01                      | i32.const 1
#  000023: 0b                         | end
  • 今度は wasm からじゃないとだめ
  • お約束のコードを除いて表示してくれる

単にダンプする

$ od -t x1 -c a.wasm
# 0000000  00  61  73  6d  01  00  00  00  01  05  01  60  00  01  7f  03
#          \0   a   s   m 001  \0  \0  \0 001 005 001   `  \0 001 177 003
# 0000020  02  01  00  07  07  01  03  66  6f  6f  00  00  0a  06  01  04
#         002 001  \0  \a  \a 001 003   f   o   o  \0  \0  \n 006 001 004
# 0000040  00  41  01  0b
#          \0   A 001  \v
# 0000044

0 'a' 's' 'm' から始まるルールらしいことがわかる。

wasm から wat を復元する

$ wasm2wat a.wasm
# (module
#   (type (;0;) (func (result i32)))
#   (func (;0;) (type 0) (result i32)
#     i32.const 1)
#   (export "foo" (func 0)))
  • (;0;) の顔文字はどゆこと?
  • 元のコードと違うし
  • 対応する wat は上書きされない

TODO: 一行ずつ実行するには?

  • デバッガーはない?
  • wasm-interp a.wasm としても何も起きない
  • 結構調べたけどわかんない
  • あとでわかったら書く

引数を取るコード

(module
  (func (export "foo") (param i32 i32) (result i32)
    (i32.add
      (local.get 0)
      (local.get 1))))
Code 意味
(param i32 i32) 2つの引数を受けとる
(local.get 0) 1つ目の引数を push する
(local.get 1) 2つ目の引数を push する
(i32.add a b) スタックの上2つを足して1つ残す。引数があるのはなんで?
(result i32) スタックの直近1つを戻値とする
  • local.getget_local と書いてあるサンプルが多くてはまった
    • 昔は命令の名前が違ってた?

Ruby からバイナリを実行する

require "wasmer"
bin = [0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 127, 3, 2, 1, 0, 7, 7, 1, 3, 102, 111, 111, 0, 0, 10, 6, 1, 4, 0, 65, 1, 11]
bin = bin.pack("C*")
store = Wasmer::Store.new
mod = Wasmer::Module.new(store, bin)
instance = Wasmer::Instance.new(mod, nil)
foo = instance.exports.foo
foo.call  # => 1

Ruby からさくっと確認するための雛形

require "wasmer"
require "pathname"

wat_src = <<~EOT
(module
  (func (export "foo") (param i32 i32) (result i32)
    (i32.add
      (local.get 0)
      (local.get 1))))
EOT

Pathname("a.wat").write(wat_src)
puts `wat2wasm a.wat`
puts `wat2wasm -v a.wat 2>&1`
puts `wasm2wat a.wasm`
puts `wasm-objdump -d a.wasm`
puts `od -t x1 -c a.wasm`
puts `wat2wasm --invoke foo 1 2`

bin = IO.binread("a.wasm")
bin.unpack("C*")  # => [0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 7, 7, 1, 3, 102, 111, 111, 0, 0, 10, 9, 1, 7, 0, 32, 0, 32, 1, 106, 11]
store = Wasmer::Store.new
mod = Wasmer::Module.new(store, bin)
instance = Wasmer::Instance.new(mod, nil)
foo = instance.exports.foo
foo.call(1, 2)    # => 3

Discussion