mrubyでruby/specを走らせることに成功した

長いと思うので結果だけ

リポジトリはこちら。

github.com

使い方はgit cloneしてmakeするだけと大変お手軽。

make TESTS="core/nil"のように、ディレクトリ指定もファイル名指定もできる。

全国のmrubyistの皆様に於かれましては、是非お試し願いたいところです。

以下つらつらと

モチベーション

数年前に始めてからというもの、mrubyという船に乗りかかったからには「mrubyには〜がない」とか「mrubyはバグが多い」とか言われたくない。と思うぐらいには愛着というか責任感を勝手に持っている。

「mrubyはCRubyと動作が違う」というのはよくある話なのだが、これを極力減らしたい。(完全には無理だけど)

仕様が同じならCRubyの知識がそのままmrubyに使えるし、ドキュメントもCRubyのものがそのまま使える。 「CRubyのライブラリをmrubyに移植」というのも行いやすくなってくる。 前回のブログで書いた「mrubyでもCRubyでも動くgem」なんていう離れ業も可能になる。

そんなわけで「mrubyの細かい挙動をCRubyに合わせる」という活動はメリットがあると信じて結構やってきたつもりなんだけど、細かい仕様は結構CRubyのテストやruby/specを読まないとわからなかったりすることが多かった。

もし誰でもmrubyでruby/specのチェックができたなら、どんどんmrubyがよくなっていくに違いないと思い、やってみたらできた。という感じ。

ruby/spec

ruby/spec とは様々に存在するRuby処理系において、その挙動を統一するために作られたもの。 「Rubyである」をチェックするテストケース集だ(要出典)。

「mrubyでruby/specを走らせる」

このパワーワードを実行に移すため、何年か前に試した時は超えなければいけない山が多すぎて挫折した。 しかし、ここ数年でmruby界隈も盛り上がり材料が揃ってきた気もするので再度挑戦してみたらできた。

途中でぶつかった様々な障害を紹介する。

mrubyでruby/specを走らせるために必要なものたち

Rubyである」という仕様チェックをするのがruby/specなわけだけど、実はそのruby/specを走らせるためにも、ある程度Ruby」でなくては動かない。

今回は以下のある程度を解消した。

構文

  • "a" "b" #=> "ab"のような文字列リテラルの連結はできない。(mrubyがなぜそうしているかはわからない)
  • defined?が使えない。https://github.com/mruby/mruby/issues/1696 によると、実装しちゃうとmrubyが大きく複雑になってしまうしObject.const_defined?でだいたいなんとかなるという理由のようだ。

ruby/specがテストケース集とすれば、そのランナーとなるのがruby/mspecruby/specを走らせるというのは、正確にはruby/mspecを走らせると同義だ。

この構文の問題がruby/mspecにあった。

構文の問題はとりあえずruby/mspecをforkして問題の部分を書き換えることで解消させている。 問題の部分がまとまったらmruby対応PRとしてpatchを投げようと思う。

ライブラリ

mrubyではOSの機能を使う部分はcoreに入っていない。 そのかわり、外部gemとして用意すれば、プラグイン的にmrubyを拡張できる。 ruby/mspecを走らせるためには以下の問題があり、それぞれライブラリによって解消できた。

  • ENVが使えない
  • Dir.[]が使えない
    • Dir.globを実装している gromnitsky/mruby-dir-glob を使用。
    • 後述する雑なpatchでalias [] globとすることで解消。
    • Dir.globは実装が複雑そうだったので非常に助かった。
  • FileIOが使えない
  • Signaltrapが使えない
    • ワー コンナ メンドクサソウナ ライブラリー スデニアッタワー ksss/mruby-signalを使用。
  • Threadが使えない
    • "Threadごとにmrb_stateを作って完全に分離する"という設計思想上CRubyのThreadと仕様は異なるが、こんなに知識が必要そうなものはなかなか作るのが難しそうなので mattn/mruby-thread をありがたく使用。
  • File.executable?が使えない。
    • 意外な伏兵だったが、便利そうなものがあったので ksss/mruby-file-stat を使用。
    • これを使ってメソッドを追加することでFile.executable?は実装できた。mruby-file-statでFile.executable?は実装するか悩み中。。。
  • Processが使えない。
  • at_exitが使えない。
    • 実はこの問題を解決するために ksss/mruby-at_exit は産まれたのだった。
  • Regexpというか正規表現が使えない。
    • ほとんどのmrubyアプリにはこれが入っているんじゃないだろうか。 mattn/mruby-onig-regexpを使用。
  • Object#pretty_printが使えない
    • CRubyではrequire 'pp'すれば使えるやつ。
    • mruby-ppの実装は2つあったので悩んだが、kou/mruby-ppを選択。 takahashim/mruby-ppでも動作は変わらないと思う。
  • requireが使えない
    • mattn/mruby-requireでほとんどの問題は解決するが、コーナーケースでCRubyと非互換性があったので、といあえずforkを使用中。これは問題点を整理してpatchを投げようと思う。
  • undefined method 'method'
    • mrubyでMethod, UnboundMethod classがつかえるという神がかり的に便利な ksss/mruby-method を使用。

パッチ

ここから割りと泥臭い。

まずruby/mspecが必要としている標準ライブラリはrbconfigppiconvのみで、それぞれrequire文がruby/mspecに書かれている。

mattn/mruby-requireでは、build_config.rbに書いた順番によって、mruby-require以降に書いたモジュールについては共有ライブラリを作成してrequireでモジュール名を指定すると読み込めるというユニークな機能がある。

これを利用してファイル名を変えるなどすれば、require 'rbconfig'も実行可能だし、rbconfigっぽいものも作ろうと思えば作れるだろう。

しかしながら、問題はrbconfigだけではなく、以下の追加パッチが必要となる。

  • File.executable?
  • Dir.[]
  • SystemExit
  • RUBY_PLATFORM
  • RUBY_PATCHLEVEL
  • $:の調整
  • Kernel#abort

実はrequire 'rbconfig'はかなりはじめの方に実行されるため、rbconfigモジュールを書くより、rbconfig.rbというファイルを作ってパッチ置き場にするほうが便利なのだ。

この理由により、rbconfig.rbは、じつはruby/mspecを走らせるための細かいパッチ置き場となっている。

他のppとiconvはそれぞれpp.rbiconv.rbという空のファイルを作っておいて、ロードされるパスに置くことで回避している。(iconvが必要な場面にまだ遭遇していないだけで、遭遇したらなにか考えないといけない)

それからmrubyのIntegerの範囲のデフォルトは32bit想定となっているため、様々なspecで想定以上の数字が使われると文字列生成などでArgumentError: string (10204000000) too big for integerと表示されたりするので、64bit想定となるようにmrbconf.hを調整したりしている。

メリット

mrubyのモジュールのうちCRubyと同じAPIを持つものについて(あるいは同じ動作を期待するものについて)、CRubyとの非互換性を簡単にチェックできる。

実際にmruby-signalでも、make TESTS="core/signal"とするだけで非互換性を発見できるので、 7 failuresを2 failuresに減らすことができた。

これからもバンバン修正していくつもり。 しかも、誰でも実行できるのでバンバンmruby界隈がよくなっていく。

デメリット

仕様を合わせることのデメリットも理解している。

ごくごく一部の機能のために全体のパフォーマンスやメンテナンス性を犠牲にしなければならなくなるというのは、よくある話。

その場合はメリット・デメリットのトレードオフをコツコツ各メンテナがやっていくしかないんじゃないだろうかなあ。

しかしながら議論をすること自体は有益なはずだし、気にせずやっていくといいんじゃないですかねえ。

そんなこんなで

さまざまなモジュールの助けを借りつつ、ついにruby/specをmrubyで走らせることに成功したというわけです。感謝。

実際に実行してみると、わっさわっさとspecが落ちまくる。 大抵が〜classがないとか〜methodがないとかだ。 Encodingなどのmrubyでは実装されていない機能はしょうがないとして、 $ make TESTS="language"ではランダムにSegmentation faultする。

がんばってmrubyをよくしていきたい。

というか一人では完全に無理。

一緒にがんばっていきませんか?