Minutus という mruby の Rust バインディングを作った

このところ、夏休みの自由研究として「mruby と Rust をいい感じにつなぎこむ」というのをやっていました。

github.com

お盆休みのすべてを費やし、なんとか「実用可能」といえそうなレベル*1まで来たので、この記事で簡単に説明したいと思います。

(↓は Matz にリツイートされてとても嬉しかったツイート)

Minutus とは

Minutus は、Rust と mruby をいい感じに連携するためのライブラリです *2

Minutus を使うと、Rust の中で mruby のスクリプトを eval できます。以下のようなイメージです。

gist.github.com

minutus/minutus-test/src を覗くと、他にも多くの例を見ることができます。
例えば、mruby のメソッドを Rust から呼べたりします。

また、それとは別に、 mrbgem (mruby 用の ライブラリ) を Rust で作ることもできます。
minutus/examples/mruby-polars はその例で、Rust のデータフレームである polars を mruby に(全く不完全な形で)ポートした mrbgem です。

Minutus で達成したいこと

Minutus で達成したいことは2つあります。

  1. 最新の mruby (3系) で動く
  2. 利用者が難しいことを考えずに、とにかく mruby と Rust を接続できる

これらを目指す理由を次で述べます。

mruby と Rust を接続する際の問題点

人類には「Rust で作ったツールに mruby の DSL を入れたい」という普遍的な欲求がある、ということは広く知られています。また、mruby のネイティブ拡張を Rust で書きたいという人類も多く存在していると思います。

しかし、その実現には高いハードルがあります。

まず、新しい mruby (3系)で動く Rust / mruby のバインディングは存在しません。例えば、mrusty がサポートする mruby のバージョンは 1.2.0 です。現在の mruby の最新のバージョンは 3.1.0 なので、これは相当古いと言えるでしょう*3*4

かといって、mruby の C API を、 Rust から直接触るのは茨の道です。

  • 避けられない unsafe 祭り
  • 自前で mruby をビルドし、リンクする必要がある
  • mruby の C API に習熟する必要がある
    • つまり、Rust も mruby も書ける、というだけではダメ
  • mruby の C API はマクロの形態で提供されるモノが多く、bindgen が認識してくれない*5
  • mruby から取り出した値を Rust の型に変換するのは面倒
  • Rust で作ったバイナリを mrbgem にするのは地味にハマりどころが多い
    • src/ 以下に C のソースコードがないと(おそらく) _init 関数が呼ばれない
    • mrbgem のインストール時に C言語以外のバイナリをビルドさせる正式な方法が(おそらく)ない*6

むかし私もこの問題にぶち当たり、Rust に mruby を埋め込むのを諦めたことがあります。
Rubyで設定を書けるLinux用キーマッパー 「rumap」をRustで作った - さんちゃのblog

Minutus の工夫

これらの問題を受け、最新の mruby で動き、かつ mruby の C API に詳しくなくても使える mruby / Rust のバインディングが必要だと感じました。

特に後者に関しては、以下のような工夫をしています。

  • mruby と接続するためのコードを生成するマクロを提供する
  • mrbgem のテンプレートを生成する CLI ツールを提供する
  • 実装例を提供する 
    • 前述の CLI ツール自体も Minutus の利用例となっています

これらの工夫により、mruby の C API に詳しくない人でも、気軽に mruby と Rust を接続できます。

Minutus の課題

「実用可能」といえるレベルにはなったと思うものの、Minutus には様々な問題が残っています。

  • GC と向き合えていない
    • 基本的には正しく実装できているはずで、利用中の値がうっかりGCされることはおそらく無いが、メモリリークしていない自信がない
    • evaluate 関数の実装に使っている mruby_load_string の返り値に対する GC の挙動がよくわからない
  • mruby のメソッドを Rust から安全に呼ぶ方法がない
    • 現状では、`define_funcall!` で定義した関数で問題が発生するとパニックするようになっている
    • これはマクロを改良したらいい感じにできるはずなので、暇を見つけて直したい
  • キーワード引数のある mruby のメソッドを呼ぶ方法がない
    • Rust の関数にはキーワード引数がなく、`define_funcall!` に渡すシグネチャが Rust の文法からどうしても乖離してしまうため、マクロの文法をどうするか悩んでいる
    • mruby 側に適当なメソッドを定義すれば、そのメソッド経由で呼び出すことはいちおう可能
  • マクロのエラーメッセージが雑すぎて文法ミスをデバッグできない
    • 例えば `define_funcall! { fn hoge(i64) -> i64 }` はコンパイルが失敗するが、`unexpected token` みたいなエラーメッセージしか出ないため、何が悪いのか全然わからない*7
  • Rust のモジュールシステムと組み合わせて動かせるのか未検証
    • `define_funcall!` や `wrap` と、それらを利用するコードが同じファイルにまとまってるパターンしか検証しておらず、それらが別モジュールに分離したときにうまく動くか怪しい
  • パフォーマンス
    • mruby と Rust の世界で値をやり取りするときの関数呼び出しの回数がエグいし、inline 展開とかもしてない
  • `wrap` で生成したクラスに initialize_copy を実装してないので、たぶん dup すると死ぬ
    • やるだけなので暇なときにやる
  • 実用的な例が存在しない
    • Rust の資産を mruby にポートするというようなことをやって、minutus 自体の欠陥を洗い出して洗練させつつ、minutus 利用者が参考にできるようにしたい
    • 例えば actix-web の mruby ラッパーを作りたいなと思っている

書き出してみると色々ありますが、暇を見つけて一つづつ潰していこうと思っています。

おわりに

この記事では、mruby と Rust をいい感じに接続するためのライブラリである Minutus について説明しました。

個人的には割と良いコンセプトのものができたと思っているので、興味のある方は使っていただけるととても嬉しいです。
こんなことはできないの?とか、ここがイケてないとか、ドキュメントが分かりづらいとか、なんでも良いのでフィードバックをいただけると泣いて喜びます。

以下の記事で言及されている通り、mruby 界隈は、特に周辺のエコシステムが古くなりつつあり*8、厳しい気持ちになることもあるのですが、mruby 自体は触っていてとても面白いので、盛り上げていけたらいいなと思っています。

udzura.hatenablog.jp

以上

*1:割と限定された用途ではあるが...

*2: Ruby の Rust バインディングである Magnus をめっちゃ参考にしました

*3:2系に追従しようとしている人もいるが、PR がスルーされている Support mruby 2.1.0 by wordijp · Pull Request #100 · anima-engine/mrusty · GitHub

*4:頑張って探したら他にもあるかもしれないが、サッと見た感じでは無さそう

*5: see: Improve macro defined constants to recognize #define CONSTANT ((int) 1) · Issue #316 · rust-lang/rust-bindgen · GitHub

*6: つまり、mrbgem をインストールするときに `cargo build --release` を実行させる方法がない

*7:このケースでは、引数名を書き忘れていることが原因。そもそも引数を省略しても動くようにしろという話もある。

*8: mrusty が 1.2.0 しかサポートしていないのはその一例