mruby
mrubyDay 5

自作mrbgemsのCIが数週間前に壊れていたので直した話

Travis CI から Broken のメールが飛んできているのに気づく

今年も、Advent Calendar 賑わってますね。僕も何か書きたいなぁ、golangかmrubyで小ネタないかなぁと思いながら、ふとメールを見ていると、Travis CI から見慣れぬメールが飛んできていました。

Subject: [CRON] Broken: bamchoh/mruby-expat#16 (master - a763a44)

中身を見て見てると、僕の作ったmrbgemsのmruby-expatが何やらビルドエラーになっているもよう。mrbgem側は何も変更を加えていないので、本体側の修正の影響でコケてるんだろうなぁと思ったところで、ピンときました。

「そうだ、これの修正をネタにしよう!あわよくばmruby本体にコントリビューションするチャンス!」

そう思って、調査開始。

不具合原因のコミットを特定

僕はmruby-expatのCIを一週間に一度、土曜日に仕掛けてるんですが、ビルドログを見てみると、直近の2個がBrokenの状態でした。(直近一個前の奴はなぜ気づかなかったのか。。。)

なんしか、直近の2週間(11/18以降)のmruby本体のコミットによってBrokenが引き起こされていることになります。

gitリポジトリを特定のコミットの状態に戻す

github でコミットログを見てみると、11/18近辺でコミットがあることが確認できました。一旦そのコミットまで戻してみます(*1)

$ git reset --hard 825e93eba41909f51d7c98a54f7c52fe1835d8ec

プラグインの設定をして、make testを実行。パスしました。

$ git reflog からHEADのコミットを取得して $git reset --hard でHEADに戻り、またコミット番号を入力し、テストを実行し...と繰り返すこと数回。ようやく原因のコミットを突き止めることに成功。

Add MRB_METHOD_TABLE_INLINE option.

これが原因だったようです。(中身を見てもなんのこっちゃでしたが。)

Travis CI のビルドエラー内容との照合

Travis CI のビルドエラーでは以下のようなエラーが表示されていました。

TypeError: XmlParser#parse(root) => Can't get cfunc env from non-cfunc proc. (mrbgems: mruby-expat)

このエラーメッセージが起こるのが、上記コミットで言うところの、src/proc.cの147行目になります。

140 MRB_API mrb_value
141 mrb_proc_cfunc_env_get(mrb_state *mrb, mrb_int idx)
142 {
143   struct RProc *p = mrb->c->ci->proc;
144   struct REnv *e;
145 
146   if (!p || !MRB_PROC_CFUNC_P(p)) {
147     mrb_raise(mrb, E_TYPE_ERROR, "Can't get cfunc env from non-cfunc proc.");
148   }
149   e = MRB_PROC_ENV(p);
150   if (!e) {
151     mrb_raise(mrb, E_TYPE_ERROR, "Can't get cfunc env from cfunc Proc without REnv.");
152   }
153   if (idx < 0 || MRB_ENV_STACK_LEN(e) <= idx) {
154     mrb_raisef(mrb, E_INDEX_ERROR, "Env index out of range: %S (expected: 0 <= index < %S)",
155                mrb_fixnum_value(idx), mrb_fixnum_value(MRB_ENV_STACK_LEN(e)));
156   }
157 
158   return e->stack[idx];
159 }

差分を見る限りでは特別何か特殊なことが行われているようには思えません。MRB_PROC_FUNC_P(p)の実装も変化があるようには思えませんでした。
if文の前にprintfを仕込んでpの中身を確認してみるとnullだったので、mrb->c->ci->procに何も入っていないことが原因のようです。

プラグイン側から追っかけてみる

mrb->c->ci->procがどこで代入されているのか特定するのが難しそうだったので、一旦、プラグイン側から追っかけて見ることにします。いろんなところにprintf文を仕掛けてどこでエラーが発生しているのか特定することにしました。問題の箇所はsrc/mrb_expat.cmrb_expat_parse内のmrb_funcallにあるようでした。

https://github.com/bamchoh/mruby-expat/blob/4facaee108d3b6d9103bb60aca6541b1890efaba/src/mrb_expat.c#L165

このmrb_funcallで何をしているかと言うと、mrblib/mrb_expat.rbattr_accessorで定義したrootメソッドを呼びたいんですね。それを呼ぶことでインスタンス変数の@rootの値を取得して戻り値として返したいという思いがありました。

https://github.com/bamchoh/mruby-expat/blob/4facaee108d3b6d9103bb60aca6541b1890efaba/mrblib/mrb_expat.rb#L71

mrb_funcallがおかしい?

いえ、本体で使われているmrb_fancallが動作していることからその可能性は低いと考えられます。呼び方が間違っていんだろうなぁという感覚はありました。

現に attr_accessorをやめて、rootメソッドを定義すると動作します

# attr_accessor :root

# こっちはイケる
def root
  @root
end

C側からattr_accessorのメソッドをmrb_funcallで呼び出すのが問題?

実験として、mrblibでrootメソッドを呼び出して見ました。XmlParser.parseメソッドを以下のように書き換えて、C側のmrb_funcallをコメントアウトして実行したところ、うまく動きます。

class XmlParser
  def self.parse(str)
    o = self.new
    o.__sys_parse__(str)
    o.root
  end

...

というところで時間切れ

attr_readerで定義しためそっどをmrb_funcallで呼び出すのがマズいというところまでは突き止めたのですが、なぜマズいのか。以前はなぜ動いていたのか。と言うところまでは突き止められませんでした。

修正方法

mrb_funcallを使わずに、インスタンス変数を直接読み出すことで解決させました。

https://github.com/bamchoh/mruby-expat/commit/ec08f0dbc95f242bd6c3dac5d13e9249078c7e49

まとめ

自作プラグインのエラーを解消するためにmrubyの動作を確認しつつ、どのように回収すればいいかを検討しました。今回はmruby本体の不具合ではなさそうですが、今後も自作プラグインを改修する中で色々な問題にぶち当たると思います。その都度、mrubyの理解を深め、最終的にはmruby本体にコントリビュートできるようになっていけたらいいなと思います。

追記(2017-12-05 02:15)

なんかモヤモヤするので、もうちょっと調べてみようと思い、件のコミットログの差分を見ていると、ci->procが削除されている箇所を発見しました。

https://github.com/mruby/mruby/commit/8f2c62407c0659d84126545e19505a851059e750#diff-86406329889f2c13524766839f0a96b3L435

この削除された部分は、少し下のProc/Funcのタイプ判定の部分のelse文で追加されています。

https://github.com/mruby/mruby/commit/8f2c62407c0659d84126545e19505a851059e750#diff-86406329889f2c13524766839f0a96b3R455

そして、今回の問題となっているattr_accessorはこのelse文には入りません。理由は、attr_accessorを定義するときに内部的に呼ばれるmrb_proc_new_cfuncの中でMRB_PROC_CFUNC_FLフラグをONにしており、このフラグがONになっていると、判定文の最初の分岐に入ってしまうからです。

試しに、最初の分岐の中でprocを代入し、make testを通してみると、エラーなく動作することが確認できます。

    if (MRB_METHOD_CFUNC_P(m)) {
      if (MRB_METHOD_PROC_P(m)) {         // +
        ci->proc = MRB_METHOD_PROC(m);    // + この部分を追加
      }                                   // +
      ci->nregs = (int)(argc + 2);
      stack_extend(mrb, ci->nregs);
    }

ただ、この修正をPRすることはないと思います。理由としては、このお試し修正が mruby 本体の設計思想にマッチするのかどうかわからないこと、mrb_funcallという副作用の強い関数を使っていたこと、rubyスクリプト側からの呼び出しでは正常に動作すること、エラーが出てるのが私だけである可能性が高いこと等色々な理由により、修正のわりにはメリットが少ないと感じたからです。

ともあれ、どの部分が修正されたことによって今回の問題が発生したのかの原因追求が自分の納得のいく形でできましたので、これで今回のお話は終わりにしようと思います。

参考文献

(*1) コミットを戻すのに参考にしたサイト:
https://qiita.com/yaotti/items/e37c707938847aee671b