まずは実験的ツールの紹介
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/run@0.2.0" end package "wasi:cli@0.2.0" 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のバイナリにコンパイルするコマンドです。以下の記事などで解説しています。
これらが揃ったら 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/run@0.2.0; } package wasi:cli@0.2.0 { interface run { run: func() -> result; } }
このComponentは wasi:cli/run@0.2.0
を実装しているので、現在の wasmtime
ならそのままファイルを渡して実行可能です。実行が(何も警告を吐かずに)成功することを確認します。
$ wasmtime out.wasm $ echo $? 0
この時、Rubyスクリプトの Kernel#component_run
で 1
を返すようにして再度この手順を踏めば、実行は正しく失敗します。
解説など
この記事で書いた通り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/run@0.2.0" (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/run@0.2.0" (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形式もパースできるようになった方がいいね