ローファイ日記

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

Rust + WASM Component + mruby/edge (w/ WASI p2!) のバイナリを作る【令和最新版】

令和最新版、と一度言ってみたかった。

先日、手作りでWASM Componentのバイナリを作ってみたんですが、mruby/edgeは全部Rustで書いているので、では最近のRustではどうするといいかをしゅっと残しときます。

結論ファースト

zenn.dev

ここに書いてある通りです 〜完〜

が、世の中割と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:[email protected] は直接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/[email protected];
  import wasi:cli/[email protected];
  import wasi:io/[email protected];
  import wasi:io/[email protected];
  import wasi:cli/[email protected];
  import wasi:cli/[email protected];
  import wasi:cli/[email protected];
  import wasi:clocks/[email protected];
  import wasi:filesystem/[email protected];
  import wasi:filesystem/[email protected];
  import wasi:random/[email protected];

  export wasi:cli/[email protected];
}
package wasi:[email protected] {
  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:[email protected] {
  interface environment {
    get-environment: func() -> list<tuple<string, string>>;
  }
  interface exit {
    exit: func(status: result);
  }
  interface stdin {
    use wasi:io/[email protected].{input-stream};

    get-stdin: func() -> input-stream;
  }
  interface stdout {
    use wasi:io/[email protected].{output-stream};

    get-stdout: func() -> output-stream;
  }
  interface stderr {
    use wasi:io/[email protected].{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のコントリビュートは歓迎してますという感じ。

*1:近いうちにmrbcコマンドのCコードをRustで薄くラップしたようなcrateでも配ろうかな...という気持ち

*2:なおrandomとかclockとかを無駄にimportしていますが、これは以前実装していたrandとかの依存をそのままにしているだけという横着ですね...。そのうちRandomやTimeクラスを再実装します。