ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

RubyスクリプトからComponent Model対応WASMバイナリを作った(実験的バージョン)

まずは実験的ツールの紹介

componentize_any というコマンドラインツールを作りました。Rubyで書いたので以下の方法でインストールしてください。

$ gem install componentize_any
## もしくは
$ git clone https://github.com/udzura/componentize_any.git && cd componentize_any
$ bundle install

以下のようなスクリプトを用意します。(wittyファイルとでも名付けました)

witty do
  world do
    export "wasi:cli/[email protected]"
  end

  package "wasi:[email protected]" do
    interface "run" do
      define "run", :func, {[] => :result}, counterpart: "component_run"
    end
  end
end

以下のようなRubyとRBSのファイルを用意し、 mec コマンドをインストールしていわゆる普通の(WASI p1依存なしの)WASMバイナリを用意。

# run.rb
def component_run
  0
end
# run.export.rbs
def component_run: () -> Integer
$ cargo install mec --version=1.0.0-rc3
$ mec --no-wasi run.rb
$ file run.wasm
run.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)

mec とはRubyのスクリプトをCore WASMのバイナリにコンパイルするコマンドです。以下の記事などで解説しています。

udzura.hatenablog.jp

これらが揃ったら componentize_any でComponentを作ることができます。

$ bundle exec componentize_any \     
      -witty-file witty.rb \
      --input run.wasm \
      --output out.wasm

Writing WAT to /var/folders/sv/...
Compiling WAT to WASM0: /var/folders/sv/...
joining WASM0 files with run.wasm
created out.wasm
run to check: `wasm-tools dump out.wasm 2>&1 | less`

ちゃんとWIT表現を取り出せることを確認。

$ wasm-tools component wit out.wasm
package root:component;

world root {
  export wasi:cli/[email protected];
}
package wasi:[email protected] {
  interface run {
    run: func() -> result;
  }
}

このComponentは wasi:cli/[email protected] を実装しているので、現在の wasmtime ならそのままファイルを渡して実行可能です。実行が(何も警告を吐かずに)成功することを確認します。

$ wasmtime out.wasm
$ echo $?          
0

この時、Rubyスクリプトの Kernel#component_run で 1 を返すようにして再度この手順を踏めば、実行は正しく失敗します。

解説など

udzura.hatenablog.jp

この記事で書いた通りComponent型のWASMバイナリはWAT形式のコードとwasm-toolsで作ることができる。

以下のようなWATのコードをコンパイルする。

;; Run wasm-tools parse -o run0.wasm run0.wat
(component
    (core module (;0;)
        (func $_main (;1;) (export "main0") (result i32)
            i32.const 0
            (if (result i32)
                (then i32.const 0)
                (else i32.const 1)
            )
        )
    )
    (core instance $m (instantiate 0))
    (func $main (result (result)) (canon lift (core func $m "main0")))
    (component $C
        (import "main" (func $f (result (result))))
        (export "run" (func $f))
    )
    (instance $c (instantiate $C
        (with "main" (func $main))))
    (export "wasi:cli/[email protected]" (instance $c))
)

この成果物を dump すると、Core WASMのモジュールがそのまま埋め込まれていることがわかる。

$ wasm-tools dump run0.wasm          
   0x0 | 00 61 73 6d | version 13 (Component)
       | 0d 00 01 00
   0x8 | 01 3f       | [core module 0] inline size
     0xa | 00 61 73 6d | version 1 (Module)
         | 01 00 00 00
    0x12 | 01 05       | type section
    0x14 | 01          | 1 count
--- rec group 0 (implicit) ---
    0x15 | 60 00 01 7f | [type 0] SubType { is_final: true, supertype_idx: None, composite_type: CompositeType { inner: Func(FuncType { params: [], results: [I32] }), shared: false } }
    0x19 | 03 02       | func section
    0x1b | 01          | 1 count
    0x1c | 00          | [func 0] type 0
    0x1d | 07 09       | export section
    0x1f | 01          | 1 count
    0x20 | 05 6d 61 69 | export Export { name: "main0", kind: Func, index: 0 }
         | 6e 30 00 00
    0x28 | 0a 0e       | code section
    0x2a | 01          | 1 count
============== func 0 ====================
    0x2b | 0c          | size of function
    0x2c | 00          | 0 local blocks
    0x2d | 41 00       | i32_const value:0
    0x2f | 04 7f       | if blockty:Type(I32)
    0x31 | 41 01       | i32_const value:1
    0x33 | 05          | else
    0x34 | 41 02       | i32_const value:2
    0x36 | 0b          | end
    0x37 | 0b          | end
    0x38 | 00 0f       | custom section
    0x3a | 04 6e 61 6d | name: "name"
         | 65         
    0x3f | 01 08       | function name section
    0x41 | 01          | 1 count
    0x42 | 00 05 5f 6d | Naming { index: 0, name: "_main" }
         | 61 69 6e   
  0x49 | 02 04       | core instance section
  0x4b | 01          | 1 count
  0x4c | 00 00 00    | [core instance 0] Instantiate { module_index: 0, args: [] }
...

Component形式のWASMバイナリはセクションをネストすることができるが、ネストする場合でも、子要素全体の長さを含むヘッダのすぐ後に、セクションの塊がそのまま埋め込まれる。この辺のヘッダの仕様はCore WASMのバイナリのノリとあまり変わらない。

ここで、core module部分を空にしてみる。

(component
  (core module)
  (core instance $m (;0;) (instantiate 0))
  (type (;0;) (result))
  (type (;1;) (func (result 0)))
  (alias core export $m "main0" (core func (;0;)))
  (func $main (;0;) (type 1) (canon lift (core func 0)))
  (component $C (;0;)
    (type (;0;) (result))
    (type $main_t (func (result 0)))
    (import "main" (func $f (;0;) (type $main_t)))
    (export (;1;) "run" (func $f))
  )
  (instance $c (;0;) (instantiate $C
      (with "main" (func $main))
    )
  )
  (export (;1;) "wasi:cli/[email protected]" (instance $c))
)

Componentの他の箇所で色々Core moduleを参照しているが、そこは一旦無視してコンパイルできる。これをdumpしてみる。 [core module 0] としてはいわゆる空のモジュールが埋め込まれている。

  0x0 | 00 61 73 6d | version 13 (Component)
      | 0d 00 01 00
  0x8 | 01 08       | [core module 0] inline size
    0xa | 00 61 73 6d | version 1 (Module)
        | 01 00 00 00
 0x12 | 02 04       | core instance section
 0x14 | 01          | 1 count
 0x15 | 00 00 00    | [core instance 0] Instantiate { module_index: 0, args: [] }
...

この時、 01 08 00 61 73 6d 01 00 00 00 という バイナリ列を一種のマーカーとして、正しい長さ情報を持たせた上で正しい Core WASM モジュールのバイナリと置き換える ことができれば、結果的にvalidなComponentバイナリを作れるはず。

... というのを簡単に試したのが以下のRubyのコード。

def to_uleb128_bin(size)
  if size < 0x80
    [size].pack("C")
  else
    [(size & 0x7f) | 0x80].pack("C") + to_uleb128_bin(size >> 7)
  end  
end

b1 = IO.read ARGV[0], encoding: "BINARY"
idx = b1.index("\x01\x08\x00\x61\x73\x6d\x01\x00\x00\x00")
raise "not a component wasm file" if idx.nil?

b2 = IO.read ARGV[1], encoding: "BINARY"
size = b2.size
data = to_uleb128_bin(size)

buf = ""
buf << b1[0...idx]
buf << "\01" << data
buf << b2
buf << b1[idx...b1.size]

IO.write "combo.wasm", buf
puts "created combo.wasm"
puts "run: `wasm-tools dump combo.wasm 2>&1 | less`"

WATの定義からわかるように、埋め込まれた Core WASM モジュールはexportされた名前だけをを参照するので、埋め込むためのモジュールは通常のように関数をexportしておけば問題ない。

このバイナリ置き換えツールに加え、wit形式のRuby DSLにした witty DSLを実装し*1、それをベースにWAT形式を作成して、埋め込み元のバイナリも内部で生成するようにしたのが componentize_any という感じ。

これらの手順を実行して、たとえば mec や普通にRust経由など他のツールで作成したWASMバイナリで置き換えても、問題なく動くのは最初に示した通り。


新春隠し芸みたいなコードだが、とりあえず気になっていてやってみたかったことはできたのでブログに残した。

componentize_any をちゃんと作り込むかはわかんない...。 mruby/edge はFully Implemented by RustなのでそのままRustのプロジェクトに埋め込めるわけで、であれば wit-bindgen でグルーコードを書いた方が素直なので、そういう方向の対応を進めるつもりだったりするし...。その辺の成果もまたブログに書いて公開したいところ。

そもそもcomponentizeする方法は色々ある(wit-bindgenのREADMEを見るだけで色々想像が膨らむ)し、C資産でもなんかできそうな気がしてきたため、今年はMatz mruby、CRubyなどなどcomponentize-rbの夢を追っていき...ますか...ね?

*1:TODO: 普通のwit形式もパースできるようになった方がいいね