長いと思うので結果だけ
リポジトリはこちら。
使い方は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/mspec。 ruby/specを走らせるというのは、正確にはruby/mspecを走らせると同義だ。
この構文の問題がruby/mspecにあった。
構文の問題はとりあえずruby/mspecをforkして問題の部分を書き換えることで解消させている。 問題の部分がまとまったらmruby対応PRとしてpatchを投げようと思う。
ライブラリ
mrubyではOSの機能を使う部分はcoreに入っていない。 そのかわり、外部gemとして用意すれば、プラグイン的にmrubyを拡張できる。 ruby/mspecを走らせるためには以下の問題があり、それぞれライブラリによって解消できた。
ENV
が使えない- CRubyのように環境変数を扱える iij/mruby-env で解消。
Dir.[]
が使えないDir.glob
を実装している gromnitsky/mruby-dir-glob を使用。- 後述する雑なpatchで
alias [] glob
とすることで解消。 Dir.glob
は実装が複雑そうだったので非常に助かった。
File
やIO
が使えない- mruby界隈のデファクトスタンダード iij/mruby-ioを使用。
Signal
やtrap
が使えない- ワー コンナ メンドクサソウナ ライブラリー スデニアッタワー ksss/mruby-signalを使用。
Thread
が使えない- "Threadごとにmrb_stateを作って完全に分離する"という設計思想上CRubyの
Thread
と仕様は異なるが、こんなに知識が必要そうなものはなかなか作るのが難しそうなので mattn/mruby-thread をありがたく使用。
- "Threadごとにmrb_stateを作って完全に分離する"という設計思想上CRubyの
File.executable?
が使えない。- 意外な伏兵だったが、便利そうなものがあったので ksss/mruby-file-stat を使用。
- これを使ってメソッドを追加することで
File.executable?
は実装できた。mruby-file-statでFile.executable?
は実装するか悩み中。。。
Process
が使えない。- これまた(僕が勝手に)mruby界のデファクトスタンダード(と呼んでいる) iij/mruby-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でも動作は変わらないと思う。
- CRubyでは
require
が使えない- mattn/mruby-requireでほとんどの問題は解決するが、コーナーケースでCRubyと非互換性があったので、といあえずforkを使用中。これは問題点を整理してpatchを投げようと思う。
undefined method 'method'
- mrubyでMethod, UnboundMethod classがつかえるという神がかり的に便利な ksss/mruby-method を使用。
パッチ
ここから割りと泥臭い。
まずruby/mspecが必要としている標準ライブラリはrbconfig
とpp
とiconv
のみで、それぞれ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.rb
とiconv.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をよくしていきたい。
というか一人では完全に無理。
一緒にがんばっていきませんか?