令和最新版、と一度言ってみたかった。
先日、手作りでWASM Componentのバイナリを作ってみたんですが、mruby/edgeは全部Rustで書いているので、では最近のRustではどうするといいかをしゅっと残しときます。
結論ファースト
ここに書いてある通りです 〜完〜
が、世の中割とcargo-componentベースの手順が多かったりするので、少しでも新しい情報を多くしようかなと思って(あと自分の理解のため)ブログに残しておきます。
ざっくりした流れ
まず最新のRustで、 wasm32-wasip2
targetをインストールします(少し時間がかかるようです)。
$ rustup target add wasm32-wasip2
今のRustエコシステムでは、他に特別なコマンドラインツールは不要です。
さて、プロジェクトを作ります。
$ cargo new --lib componentize_mrbe
Cargo.toml も編集します。
まず wit-bindgen は必要なので、追加します。mrubyedge(RC版)も追加。 crate-type = ["cdylib"]
にする必要もあります。 [profile.release]
は直接動作に関係ないと思いますが適宜埋めておきます。
こういう感じ。
[package] name = "componentize_mrbe" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [profile.release] codegen-units = 1 opt-level = "s" debug = false strip = false lto = true [dependencies] mrubyedge = "1.0.0-rc2" wit-bindgen = "0.36.0"
また、現状 mruby/edge でwasmを作るには mrbc コマンドで作ったmrubyバイナリが別途必要なので、この辺を眺めながら3.3.0をインストールしてください*1。
mrbcを用意したら以下のようなRubyスクリプトを run.mrb
にします。putsもfibもある豪華版にしました。
def run puts "Hello, World from Really Ruby Script!" puts "fib(20) = #{fib(20)}" 0 end def fib(n) n < 2 ? n : fib(n - 1) + fib(n - 2) end
$ mrbc -o src/run.mrb src/mrblib/run.rb
mrbファイルが用意できたらwitファイルも用意しましょう。プロジェクトの wit/root.wit
というファイルを以下のようにします。
package root:component; world root { export wasi:cli/run@0.2.0; } package wasi:cli@0.2.0 { interface run { run: func() -> result; } }
よく、WASIの定義のwitファイルを落とさねば〜みたいな手順が載っていますが、今回は wasip2 を使うので直接witに書かなくて良いようで、cargoで上手いことやってくれるみたいです。 wasi:cli@0.2.0
は直接exportしているので直接記述します。
これでようやく、Rustを書けます。コード src/lib.rs
はこんな感じで書きます。
extern crate mrubyedge; wit_bindgen::generate!({ world: "root", generate_all }); use exports::wasi::cli::run::Guest; use mrubyedge::yamrb::helpers::mrb_funcall; struct TheRoot; const CODE: &[u8] = include_bytes!("mrblib/run.mrb"); impl Guest for TheRoot { fn run() -> Result<(),()> { let mut rite = mrubyedge::rite::load(CODE).unwrap(); let mut vm = mrubyedge::yamrb::vm::VM::open(&mut rite); let args = vec![]; vm.run().unwrap(); let result = mrb_funcall(&mut vm, None, "run", &args).unwrap(); match result.value { mrubyedge::yamrb::value::RValue::Integer(v) => { if v == 0 { Ok(()) } else { Err(()) } }, _ => { Err(()) } } } }
wit_bindgen::generate!
は wit
配下の定義を見てグルーコードを生成します。どういうコードを生成するか? は実はコマンドラインツールでも確認できます。
$ cargo install wit-bindgen-cli $ wit-bindgen rust --world root --generate-all wit/root.wit Generating "root.rs"
この root.rs
を include!()
しても多分同じ、です。やったことはないが。
上記のファイルを見れば分かる通り、 Guest
という run() -> Result<(),()>
を実装したtraitが生成されるので、その run() の実装を書いてあげれば、wit側の run() に対応した関数が書けます。run()
という関数名とシグネチャは当然witの定義によります。
その中身として、 mruby/edge のバイナリを読み込んで関数を実行するコードを書いたのが上記コードです。
で、ここまできたらあとは普通にコンパイルすればOK。
$ cargo build --target wasm32-wasip2 --release
本当に普通の、標準的な方法でのビルドです。
これで ./target/wasm32-wasip2/release/componentize_mrbe.wasm
ができている。 wit を見てみましょう。
$ wasm-tools component wit ./target/wasm32-wasip2/release/componentize_mrbe.wasm package root:component; world root { import wasi:cli/environment@0.2.0; import wasi:cli/exit@0.2.0; import wasi:io/error@0.2.0; import wasi:io/streams@0.2.0; import wasi:cli/stdin@0.2.0; import wasi:cli/stdout@0.2.0; import wasi:cli/stderr@0.2.0; import wasi:clocks/wall-clock@0.2.0; import wasi:filesystem/types@0.2.0; import wasi:filesystem/preopens@0.2.0; import wasi:random/random@0.2.0; export wasi:cli/run@0.2.0; } package wasi:io@0.2.0 { interface error { resource error; } interface streams { use error.{error}; resource output-stream { check-write: func() -> result<u64, stream-error>; write: func(contents: list<u8>) -> result<_, stream-error>; blocking-write-and-flush: func(contents: list<u8>) -> result<_, stream-error>; blocking-flush: func() -> result<_, stream-error>; } variant stream-error { last-operation-failed(error), closed, } resource input-stream; } } package wasi:cli@0.2.0 { interface environment { get-environment: func() -> list<tuple<string, string>>; } interface exit { exit: func(status: result); } interface stdin { use wasi:io/streams@0.2.0.{input-stream}; get-stdin: func() -> input-stream; } interface stdout { use wasi:io/streams@0.2.0.{output-stream}; get-stdout: func() -> output-stream; } interface stderr { use wasi:io/streams@0.2.0.{output-stream}; get-stderr: func() -> output-stream; } interface run { run: func() -> result; } } ...
wasi p2関係のインタフェースで使っているものはRustが全部用意してくれました。世の中は進歩した*2。
そしてこのバイナリはそのまま wasmtime で動かせます。手作りWASMと違って標準出力に書き出せる。
$ wasmtime ./target/wasm32-wasip2/release/componentize_mrbe.wasm Hello, World from Really Ruby Script! fib(20) = 6765
ということで、あとは薄めのコード生成(と言っても複雑ではあるだろう)とか仕様を決めれば mruby/edge でComponent対応WASMバイナリが作れるようになりそう、と見通せたところで今日はおしまい。
VMを書き直して以来、設計が見通せるようになった感があるので、とりあえずTCPクラスでも実装してウェブサーバでも立ち上げようかな...。あと、Rustを書きたいRubyistのコントリビュートは歓迎してますという感じ。