mrubyのなおしかた


この記事はmruby Advent Calendar 2016 18日目の記事です。
Advent Calenderを作ったのははじめてですが、皆様のお陰で25日分全て埋まっており、mrubyの注目度の高さが窺えますね。

mruby-spec

mruby-specとは「mrubyでruby/specを走らせるぞ」というモチベーションとともに開発した、テストランナーです。

mruby-specを使うことで、簡単にmrubyでruby/specを走らせる環境が作れるので、「mrubyの挙動が怪しいな?」と思った時に動作を確かめてみることができて便利です。

今回はmruby-specの使い方と使用例を紹介します。

使い方

$ git clone https://github.com/ksss/mruby-spec.git
$ cd mruby-spec
$ make TESTS="core/nil"

たったこれだけで使い始めることができます。この例では"core/nil"のspecを走らせていますが、ruby/specが用意している全てのspecを走らせることができます。

$ make TESTS="language/if"

わー簡単。始めない手はない。

specは落ちまくるよ

大前提として、大抵のspecは落ちまくります。
最悪segmentation faultするでしょう。

でも大丈夫。なおせばいいのです。

しかしながら、あまりに落ちるspecの数が多いのでなおしきれていないのが現状です。

また、ただなおせばいいというわけではない場合もあります。
設計思想的に、CRubyでの負債をmrubyに引き継ぎたくなかったり("a" "b" => "ab"とか)、
一部の機能のためにパフォーマンスや使用メモリを犠牲にしてしまったり、
そもそも同じ動作をするように設計されていなかったり(mruby-threadとか)、
CRubyとmrubyは違う思想と実装で産まれたプロダクトなので、差異があって当たり前です。

それでも「まあこれは大抵の人が期待するものと違うよね」というやつからなおしていきましょう。

mrubyのなおしかた

落ちたspecはbacktraceが表示されているので、どういう流れで落ちたのかだいたい見当がつきます。
specはたいてい短いコードで書かれているので、これで小さな再現コードが手に入ります。

Segmentation fault

Segmentation faultと表示されたときはどこで落ちたか表示されないのでわかりません。
こんなときは、gdbやlldbを使うと便利です。

$ lldb mruby/bin/mruby
(lldb) target create "mruby/bin/mruby"
Current executable set to 'mruby/bin/mruby' (x86_64).
(lldb) run mspec/bin/mspec-run spec/language
...snip...
[/ | ==================62%===                 | 00:00:00]     46F     69EProcess 16027 stopped
* thread #1: tid = 0xce5da, 0x0000000100037cca mruby`kh_get_n2s(mrb=0x0000000100202ac0, h=0x0000000100203930, key=0) + 26 at symbol.c:37, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=2, address=0x7fff5f3ffff8)
    frame #0: 0x0000000100037cca mruby`kh_get_n2s(mrb=0x0000000100202ac0, h=0x0000000100203930, key=0) + 26 at symbol.c:37
   34   #define sym_hash_equal(mrb,a, b) (mrb->symtbl[a].len == mrb->symtbl[b].len && memcmp(mrb->symtbl[a].name, mrb->symtbl[b].name, mrb->symtbl[a].len) == 0)
   35
   36   KHASH_DECLARE(n2s, mrb_sym, mrb_sym, FALSE)
-> 37   KHASH_DEFINE (n2s, mrb_sym, mrb_sym, FALSE, sym_hash_func, sym_hash_equal)
   38   /* ------------------------------------------------------ */
   39
   40   static void

こんな感じで落ちた部分のCコードを教えてくれます。

gdbやlldbの使い方は自分も探り探りなので割愛。

で、Rubyのどのコードで落ちたの

vmはmrb_vm_execメソッド内で各命令をループで処理しているので、この段階のデバッグができるならなんとなく、どこで落ちたか類推するヒントになるかもしれません。

p mrb_codedump_all(mrb, proc)

または、地道に実行するspecをしぼっていくのもよくやります。

まずは簡単なやつから

さて、やり方はわかりましたが、広大なspecのどこから手を付ければいいのでしょう。

おすすめはmrbgems以下の拡張メソッドたちです。(例: mruby-array-ext)

ここなら影響範囲は限られますし、実装は案外雑だったりするので、なおしやすいです。
Rubyのコードで書かれた拡張も多いのがポイントです。

自分がやった、実際の例としてはこんな感じ。

mruby-string-ext: https://github.com/mruby/mruby/pull/3176
mruby-range-ext: https://github.com/mruby/mruby/pull/3261
mruby-enum-ext: https://github.com/mruby/mruby/pull/3271

最新への追従

mruby-specを使い続けてくると、mrubyやspecが古くなってきます。
そんなときは以下のコマンドで、mruby,spec,mspecのリポジトリをgit pullでまとめてupdateできます。

$ make pull

つぎはきみのばんだ

昨今、mrubyを使った魅力的なプロダクトが増えてきました。
h2oでは、mrubyでwebサーバーの細かな制御ロジックを書くことができますし、mitamaeのような面白いプロダクトもでてきています。
mruby-specを使って、mrubyをより良いものにしていきましょう。