ローファイ日記

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

wasm環境で動くmrubyを作りたかった... 話

mrubyファミリー Advent Calendar 2023

24日目の記事です..............................

qiita.com

wasm環境で動くmrubyを作っているのでその途中経過をダラダラ書きます。

本当は..............

今の時点である程度まともに動かしたり、情報も整理したかったんですね。で、一昨日〜昨日と夜に時間があったはずなのですが、その日程で存在した会社の全社集会後に無限に飲み明かしてしまい、その時間と体力が.......。

なので生活の状況をシェアします。

mruby/edge (仮) を作りたかった話

すごく昔、WASM CRubyの発表があった時に、「必要な機能だけを取捨選択できるmrubyの方が向いてたりしませんか?」という質問をした覚えがあります(全てがうろ覚え)。その時は、それでも利用者が多い方のランタイムでやりたいという回答だった気がしますが、それはそれとしてmrubyでのやり方がありそうで興味をずっと持っていました。

ところで、WASM CRubyが生成するwasmを本日の段階の(リリース前日?)Ruby 3.2.0-dev commitish 82015496b924744ae3999987ba679978beb153c7 で確認すると、18メガバイトでした。環境は binaryen 116、wasm-sdk 20.0です。

$ ls -lh ruby-wasm32-wasi/usr/local/bin/ruby
-rwxr-xr-x  1 udzura  staff    18M 12 24 17:23 ruby-wasm32-wasi/usr/local/bin/ruby*

さて、mrubyを軸にするなら、以下のような戦略をとれば、「Rubyスクリプトを書けば比較的小さいwasm経由でコンテナ上で関数が動くんじゃね?」と思ってます。

  • Rubyスクリプトはmrbcでmrubyバイナリにする
  • mrubyバイナリをプログラムに埋め込む ...(1)
  • そのプログラムに、mrubyの実行するVMも一緒に埋め込む ...(2)
  • (1)、(2)を固めたwasm形式のファイルを作る ...(3)
  • (3) をwasmedge + crunなどで動かす

まあ(3)まで進めばなんでもいいのですが、wasmedgeは面白いので一旦主要なターゲットに据えています。

そして設計段階で、そこまで実装に進んでないのに晒しちゃうのはなんか怖い 頼みますよ(何を)。


で、まずは個別のpassを地道に検証したり実装しているところ。最終的にコマンド一つで隠蔽できれば使えるものになるでしょう*1

まず、(1) のようなバイナリデータをプログラム内部に埋め込むような仕様・実装はいろんな言語にあります。Goにもありますし、Rustならマクロでやれたりしますね。

fn main() {
    let bin = include_bytes!("./merry-christmas.mrb");
    println!("{}", bin.len());
    // => 282
}

大事なのは (2) ですね。普通に考えれば mruby でもややデカく、 mruby/c が極めて向いていると思うのですが、上述するようにRustのバイナリ組み込みマクロと、後そもそもコンパイルターゲットに複数のwasm環境を持っている便利さを鑑みてmruby/cやmrubyの実装を参照して Rustでmrubyバイトコードを評価するVM作ればいんじゃね? と思って作ろうかなと思ってます。これが mruby/edge です(仮)。上の設計の通り、第一に wasmedge をランタイムとしてサポートしたいのでこういう名前ですが、当然ブラウザでも動くでしょう。

個人的に mruby + C + emscripten ベースの組み合わせでもいいのですが、CベースではCRuby WASMとかぶっている*2ため、ちょっと違う環境で作りたいのもありました。

できている範囲としては、mrubyバイナリを一通りRustだけでパースするところはやれてます。今のところ mruby/edge のriteというモジュールにまとめています。クレートは独立させるかも:

extern crate mrubyedge;

fn main() {
    let bin = include_bytes!("./merry-christmas.mrb");
    let rite = mrubyedge::rite::load(bin).unwrap();
    dbg!(&rite);
    for (i, irep) in rite.irep.iter().enumerate() {
        println!("irep #{}", i);
        mrubyedge::eval::eval_insn(irep.insn).unwrap();
    }
    ()
}
cargo run --package mrubyedge --example dump --
   Compiling mrubyedge v0.1.0 (/opt/ghq/github.com/udzura/mooby/mrubyedge)
    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
     Running `target/debug/examples/dump`
skipped section Some(['D', 'B', 'G', '\0'])
end section detected
[mrubyedge/examples/dump.rs:6] &rite = Rite {
    binary_header: RiteBinaryHeader {
        ident: [
            82,
            73,
            84,
            69,
        ],
        major_version: [
            48,
            51,
        ],
        minor_version: [
            48,
            48,
        ],
        size: [
            0,
            0,
            1,
            26,
        ],
        compiler_name: [
            77,
            65,
            84,
            90,
        ],
        compiler_version: [
            48,
            48,
            48,
            48,
        ],
    },
//...
irep #0
insn: TCLASS B(1)
insn: METHOD BB(2, 0)
insn: DEF BB(1, 0)
insn: TCLASS B(1)
insn: METHOD BB(2, 1)
insn: DEF BB(1, 1)
insn: SSEND BBB(1, 0, 0)
insn: RETURN B(1)
insn: STOP Z
irep #1
insn: ENTER W(0)
insn: SSEND BBB(2, 0, 0)
insn: RETURN B(2)
irep #2
insn: ENTER W(0)
insn: STRING BB(3, 0)
insn: SSEND BBB(2, 0, 1)
insn: RETURN B(2)

Rustでいうwasm32-wasiターゲットでの動作

じゃあこれはwasmで動くのか、というのはあるんですが、はっきり言ってあっさり動きます。以下はRustのwasm32-wasiターゲットでの動作の抜粋です。

$ cargo build --example dump --target wasm32-wasi 

$ wasmedge ../target/wasm32-wasi/debug/examples/dump.wasm
skipped section Some(['D', 'B', 'G', '\0'])
end section detected   
[mrubyedge/examples/dump.rs:6] &rite = Rite {
    binary_header: RiteBinaryHeader {
        ident: [
            82,
            73,
            84,
            69,
        ],
...

できてるバイナリサイズはこういう感じでした。う〜ん

$ ls -lh ../target/wasm32-wasi/debug/examples/dump.wasm    
-rwxr-xr-x  1 udzura  staff   2.7M 12 24 11:14 ../target/wasm32-wasi/debug/examples/dump.wasm

$ wasm-objdump -h ../target/wasm32-wasi/debug/examples/dump.wasm

dump.wasm:      file format wasm 0x1

Sections:

     Type start=0x0000000b end=0x000000d4 (size=0x000000c9) count: 22
   Import start=0x000000d7 end=0x0000016d (size=0x00000096) count: 4
 Function start=0x00000170 end=0x0000033f (size=0x000001cf) count: 461
    Table start=0x00000341 end=0x00000348 (size=0x00000007) count: 1
   Memory start=0x0000034a end=0x0000034d (size=0x00000003) count: 1
   Global start=0x0000034f end=0x00000358 (size=0x00000009) count: 1
   Export start=0x0000035a end=0x0000037b (size=0x00000021) count: 3
     Elem start=0x0000037e end=0x0000046a (size=0x000000ec) count: 1
     Code start=0x0000046e end=0x00022bff (size=0x00022791) count: 461
     Data start=0x00022c02 end=0x000262ec (size=0x000036ea) count: 2
   Custom start=0x000262f0 end=0x0002b45c (size=0x0000516c) ".debug_abbrev"
   Custom start=0x0002b460 end=0x000c434e (size=0x00098eee) ".debug_info"
   Custom start=0x000c4352 end=0x000f6358 (size=0x00032006) ".debug_ranges"
   Custom start=0x000f635c end=0x001ee7e6 (size=0x000f848a) ".debug_str"
   Custom start=0x001ee7ea end=0x00248696 (size=0x00059eac) ".debug_pubnames"
   Custom start=0x0024869a end=0x00254139 (size=0x0000ba9f) ".debug_pubtypes"
   Custom start=0x0025413d end=0x002a7913 (size=0x000537d6) ".debug_line"
   Custom start=0x002a7915 end=0x002a793d (size=0x00000028) ".debug_loc"
   Custom start=0x002a7941 end=0x002b0444 (size=0x00008b03) "name"
   Custom start=0x002b0447 end=0x002b04f9 (size=0x000000b2) "producers"
   Custom start=0x002b04fb end=0x002b0534 (size=0x00000039) "target_features"

そもそもこれは実行する対象のプログラムも含めたサイズなので、他に必要なものがないという意味では少し妥協できます。でもなんかデバッグ情報も入ってたりするのが気になりますね、色々深追いできそう。

そもそもそもそも、ちゃんとmrubyのプログラム部分を同梱してるかというのについては、一応stringsとかで確認できるかなと思います。

$ strings ../target/wasm32-wasi/debug/examples/dump.wasm | grep MATZ
MATZ0000IREP

ちなみに、本当ならOSは関係なく wasiなしのwasmで動かせる、と思われるので*3 stdライブラリのダイエットを将来やりたい。VecとかBoxが使えないんでしたっけ。しんど...。

あと、ひとまずRiteパーサーがあればmruby-objdumpみたいなのは作れそうな感じがするので最初は試しにそれを完成させる。色々です。

本当は mruby-asm も欲しい

デバッグ用に、普通にRubyコンパイルしてできるやつじゃなく、もっと単純な命令だけのmrubyバイナリが欲しくなるはずなので作ろうとしています。asmの文法とパーサはざっくり書いたのですが、mrubyのバイナリの仕様に沿って命令を詰め詰めするのがかなり難儀だと分かり止まっています。mrubyとwasmにより詳しくなれば書けると思うのでそのうちやります。

github.com

結局何が嬉しいのか

何が嬉しいんだろう... 確かにバイナリサイズはかなり小さくなります。全然実行環境を含んでないため(今そもそもまだ含んでないが、今後何を含めるかもコントロールできる)、それはそうなのですが...。

wasmってブラウザで動かすだけじゃなく、それこそwasmedgeのような環境だったり、あとproxy-wasmのような用途もあるので、そういう場合はイメージをpullしたりとかって話が出るのでバイナリサイズが小さい方が喜ばれる、というのはあると思います。

あとは、Rubyで「関数」を書いて、mruby化した関数本体とmruby/edgeを埋め込んでエントリポイントを叩けば、それでいきなり実行できる状態にできるというのが設計思想です。そういうwasmバイナリはそれはそれで欲しい場面がありそうです。

基本的にwasmでの動作以外を考えないつもりで開発する予定です。なのでもっと踏み込んだ使いやすさの工夫も将来できるのかも。


とりあえずWASMもmrubyも何もわからんまま書いてるのでめちゃくちゃ突っ込まれそうですが、しょうがない...。皆様のツッコミがサンタさんからの贈り物です。

明日もmrubyファミリーアドカレの記事はあるようです。お楽しみに。

*1:そのエントリポイントたるコマンドを仮に mooby と名づける予定

*2:今はemscriptenは直接使っていないが、clang ベースで、 wasm-sdk + binaryenという認識 https://github.com/ruby/ruby/blob/daefbf8fbfa1bd2109315bab7658f52460f7ed59/wasm/README.md

*3:それこそがmrubyっぽさ