[[mrubyを読む]]
 
 #contents
 
 *はじめに [#l5d6a909]
 
 今回はmrubyの例外処理を読解します。なお、実行手順読解で使ったバージョンは例外処理にバグがあるので今回は2012/6/20に取得したcommit 7744315d88を使用します。
 
 *サンプルスクリプト [#ya182d05]
 
 基本的にYARV読解の時に使ったものと同じですがmrubyは$!やbacktraceがないので少し違います。
 
  def exc_func
    begin
      raise "Exception"
    ensure
      puts "ensure in exc_func"
    end
  end
  
  begin
    exc_func
  rescue => e
    puts "rescue in top"
    puts e.message
  ensure
    puts "ensure in top"
  end
 
 *NODEツリーへの変換 [#p114693a]
 
 NODEツリーは--verboseオプション付きでmrubyを実行すれば出力されます。
 
  NODE_DEF:
    exc_func
    NODE_BEGIN:
      NODE_ENSURE:
        body:
          NODE_BEGIN:
            NODE_CALL:
              NODE_SELF
              method='raise' (34)
              args:
                NODE_STR "Exception" len 9
        ensure:
          NODE_BEGIN:
            NODE_CALL:
              NODE_SELF
              method='puts' (286)
              args:
                NODE_STR "ensure in exc_func" len 18
 
 exc_funcの定義です。どの部分の実行に対してensureが適用されるかがわかります。
 
  NODE_ENSURE:
    body:
      NODE_RESCUE:
        body:
          NODE_BEGIN:
            NODE_CALL:
              NODE_SELF
              method='exc_func' (298)
        rescue:
          exc_var:
            NODE_LVAR e
          rescue body:
            NODE_BEGIN:
              NODE_CALL:
                NODE_SELF
                method='puts' (286)
                args:
                  NODE_DSTR
                    NODE_STR "rescue in top: Exception#message: " len 34
                    NODE_BEGIN:
                      NODE_CALL:
                        NODE_LVAR e
                        method='message' (175)
                    NODE_STR "" len 0
    ensure:
      NODE_BEGIN:
        NODE_CALL:
          NODE_SELF
          method='puts' (286)
          args:
            NODE_STR "ensure in top" len 13
 
 トップレベルのbegin〜end部分です。こちらはrescue対象となるのがどの部分かということがわかります。
 
 *実行コードへの変換 [#jd9f7105]
 
 **実行コード変換結果 [#dbba5bd4]
 
 次に実行コード生成です。ソースを見る前に生成される実行コードを見ます。
 
  irep 134 nregs=7 nlocals=3 pools=2 syms=5
  000 OP_TCLASS   R3
  001 OP_LAMBDA   R4      I(135)  1
  002 OP_METHOD   R3      :exc_func
  003 OP_EPUSH    :I(137)
  004 OP_ONERR    009
  005 OP_LOADSELF R3
  006 OP_LOADNIL  R4
  007 OP_SEND     R3      :exc_func       0
  008 OP_JMP              029
  009 OP_RESCUE   R4
  010 OP_GETCONST R5      :StandardError
  011 OP_MOVE     R6      R4
  012 OP_LOADNIL  R7
  013 OP_SEND     R5      :===    1
  014 OP_JMPIF    R5      016
  015 OP_JMP              028
  016 OP_MOVE     R1      R4
  017 OP_LOADSELF R4
  018 OP_STRING   R5      "rescue in top: Exception#message: "
  019 OP_MOVE     R6      R1
  020 OP_LOADNIL  R7
  021 OP_SEND     R6      :message        0
  022 OP_STRCAT   R5      R6
  023 OP_STRING   R6      ""
  024 OP_STRCAT   R5      R6
  025 OP_LOADNIL  R6
  026 OP_SEND     R4      :puts   1
  027 OP_JMP              030
  028 OP_RAISE    R4
  029 OP_POPERR   1
  030 OP_EPOP     1
  031 OP_STOP
 
 トップレベルの実行コードです。OP_EPUSHとOP_EPOP、OP_ONERRとON_POPERRが対になっていそうな感じです。詳しくはまた後で説明します。
 
  irep 135 nregs=5 nlocals=3 pools=1 syms=1
  000 OP_ENTER    0:0:0:0:0:0:0
  001 OP_EPUSH    :I(136)
  002 OP_LOADSELF R3
  003 OP_STRING   R4      "Exception"
  004 OP_LOADNIL  R5
  005 OP_SEND     R3      :raise  1
  006 OP_EPOP     1
  007 OP_RETURN   R3
 
 exc_funcの実行コードです。
 
  irep 136 nregs=4 nlocals=2 pools=1 syms=1
  000 OP_LOADSELF R2
  001 OP_STRING   R3      "ensure in exc_func"
  002 OP_LOADNIL  R4
  003 OP_SEND     R2      :puts   1
  004 OP_RETURN   R3
 
 exc_funcのensure節部分です。
 
  irep 137 nregs=4 nlocals=2 pools=1 syms=1
  000 OP_LOADSELF R2
  001 OP_STRING   R3      "ensure in top"
  002 OP_LOADNIL  R4
  003 OP_SEND     R2      :puts   1
  004 OP_RETURN   R3
 
 トップレベルのensure節部分です。
 
 **NODE_ENSURE [#f7a60df8]
 
 それではソースを見てみましょう。なお、以下のコード断片はsrc/codegen.c中のcodegen()の一部です。
 
 #code(C){{
   case NODE_ENSURE:
     {
       int idx;
       int epush = s->pc;
 
       genop(s, MKOP_Bx(OP_EPUSH, 0));
       s->ensure_level++;
       codegen(s, tree->car, val);
       idx = scope_body(s, tree->cdr);
       s->iseq[epush] = MKOP_Bx(OP_EPUSH, idx);
       s->ensure_level--;
       genop_peep(s, MKOP_A(OP_EPOP, 1), NOVAL);
     }
     break;
 }}
 
 tree->carはbody部分、tree->cdrはensure部分です。やっていることは以下の通りです。
 
 +まず、OP_EPUSHを埋め込んで
 +body部分のコードを生成し
 +次にensure部分のコードを生成してirep番号を取得
 +先ほど埋め込んだOP_EPUSHに取得したirep番号を設定
 +最後にOP_EPOPを埋め込む
 
 なお、OP_EPOPの埋め込み出genop_peep()が呼ばれていますがこれは連続するOP_EPOPを統合するためです。例えば、以下のような場合です。
 
 
  begin
    begin
    ensure
    end
  ensure; end
 
  NODE_SCOPE:
    NODE_BEGIN:
      NODE_ENSURE:
        body:
          NODE_BEGIN:
            NODE_ENSURE:
              body:
                NODE_BEGIN:
              ensure:
                NODE_BEGIN:
        ensure:
          NODE_BEGIN:
 
  irep 134 nregs=0 nlocals=2 pools=0 syms=0
  000 OP_EPUSH    :I(136)
  001 OP_EPUSH    :I(135)
  002 OP_EPOP     2
  003 OP_STOP
 
 **NODE_RESCUE [#c7e6ebb2]
 
 NODE_ENSUREに比べNODE_RESCUEはかなり複雑です。例によって部分に分けて解説します。
 
 #code(C){{
   case NODE_RESCUE:
     {
       int onerr, noexc, exend, pos1, pos2, tmp;
       struct loopinfo *lp;
 
       onerr = new_label(s);
       genop(s, MKOP_Bx(OP_ONERR, 0));
 }}
 
 まず「例外が起きたらここへ」なコードを埋め込んでいます。ただし、実際のジャンプ先はまだ決まっていません。
 
 #code(C){{
       lp = loop_push(s, LOOP_BEGIN);
       lp->pc1 = onerr;
       if (tree->car) {
         codegen(s, tree->car, val);
       }
 }}
 
 body部分のコードを生成しています。
 
 #code(C){{
       lp->type = LOOP_RESCUE;
       noexc = new_label(s);
       genop(s, MKOP_Bx(OP_JMP, 0));
 }}
 
 例外が起きなかった場合のジャンプ命令を埋め込んでいます。
 
 #code(C){{
       dispatch(s, onerr);
 }}
 
 以下、rescue部分のコードを生成します。というわけで「例外が起きたらここへ」がどこへ飛べばいいのかわかるので最初に埋めたON_ERRのジャンプ先を設定しています。
 
 #code(C){{
       tree = tree->cdr;
       exend = 0;
       pos1 = 0;
       if (tree->car) {
         node *n2 = tree->car;
         int exc = cursp();
 
         genop(s, MKOP_A(OP_RESCUE, exc));
         push();
 }}
 
 まず発生した例外を受け取る命令を生成しています。
 
 #code(C){{
         while (n2) {
           node *n3 = n2->car;
           node *n4 = n3->car;
 }}
 
 rescue節の数だけ、ループを回します。n3には発生した例外を格納する変数情報、n4にはrescue対象とする例外のクラスが入るようです。
 
 #code(C){{
           if (pos1) dispatch(s, pos1);
           pos2 = 0;
           do {
             if (n4) {
               codegen(s, n4->car, VAL);
             }
             else {
               genop(s, MKOP_ABx(OP_GETCONST, cursp(), new_msym(s, mrb_intern(s->mrb, "StandardError"))));
               push();
             }
             genop(s, MKOP_AB(OP_MOVE, cursp(), exc));
             push();
             genop(s, MKOP_A(OP_LOADNIL, cursp()));
             pop(); pop();
             genop(s, MKOP_ABC(OP_SEND, cursp(), new_msym(s, mrb_intern(s->mrb, "===")), 1));
             tmp = new_label(s);
             genop(s, MKOP_AsBx(OP_JMPIF, cursp(), pos2));
             pos2 = tmp;
             if (n4) {
               n4 = n4->cdr;
             }
           } while (n4);
 }}
 
 rescue対象となる例外クラスの数だけループを回します。具体的には以下のように書かれているとループが2度回ります。
 
  rescue AAAError, BBBError
 
 埋め込んでいる処理内容は以下のようになります。
 
 +例外クラスを取得して
 +発生した例外を引数に例外クラスに対して===を実行
 +trueが返ってきたらrescue節の中身が書かれている部分にジャンプ
 
 #code(C){{
           pos1 = new_label(s);
           genop(s, MKOP_sBx(OP_JMP, 0));
           dispatch_linked(s, pos2);
 }}
 
 あるrescue節で対象となる例外クラスでなかった場合に次のrescue節に飛ぶためのジャンプ命令です。
 
 #code(C){{
           pop();
           if (n3->cdr->car) {
             gen_assignment(s, n3->cdr->car, exc, NOVAL);
           }
 }}
 
 例外を受け取る変数に代入するコードを生成しています。
 
 #code(C){{
           if (n3->cdr->cdr->car) {
             codegen(s, n3->cdr->cdr->car, val);
           }
 }}
 
 rescue節の本体に対応するコードを生成しています。
 
 #code(C){{
           tmp = new_label(s);
           genop(s, MKOP_sBx(OP_JMP, exend));
           exend = tmp;
           n2 = n2->cdr;
           push();
         }
 }}
 
 最後にrescueの末尾に飛ぶジャンプ命令を生成しています。
 
 #code(C){{
         if (pos1) {
           dispatch(s, pos1);
           genop(s, MKOP_A(OP_RAISE, exc));
         }
       }
 }}
 
 いずれのrescue節にも引っ掛からなかった場合に例外を再送する処理を行っています。
 
 #code(C){{
       pop();
       tree = tree->cdr;
       dispatch(s, noexc);
       genop(s, MKOP_A(OP_POPERR, 1));
 }}
 
 例外が起きなかった場合のジャンプ命令の飛び先を設定し、積んだrescue情報を下す処理を埋め込んでいます。
 
 #code(C){{
       if (tree->car) {
         codegen(s, tree->car, val);
       }
       dispatch_linked(s, exend);
       loop_pop(s, NOVAL);
     }
     break;
 }}
 
 最後にelse節のコードを生成して終了です。
 
 *コードの実行 [#hd55905a]
 
 コードの生成を見たので次はコードの実行を見てみましょう。なお、今後のコード断片はsrc/vm.cのmrb_run()の一部です。
 
 **例外に対する備え [#oa0661dc]
 
 ***OP_EPUSH [#j97e0ee0]
 
 まず、ensureを積むOP_EPUSHです。callinfoのeidxで積まれているensureの数を管理しています。この情報は後で重要になるので覚えておいてください。
 
 #code(C){{
     CASE(OP_EPUSH) {
       /* Bx     ensure_push(SEQ[Bx]) */
       struct RProc *p;
 
       p = mrb_closure_new(mrb, mrb->irep[irep->idx+GETARG_Bx(i)]);
       /* push ensure_stack */
       if (mrb->esize <= mrb->ci->eidx) {
         if (mrb->esize == 0) mrb->esize = 16;
         else mrb->esize *= 2;
         mrb->ensure = mrb_realloc(mrb, mrb->ensure, sizeof(struct RProc*) * mrb->esize);
       }
       mrb->ensure[mrb->ci->eidx++] = p;
       NEXT;
     }
 }}
 
 ***OP_ONERR [#n154a1a6]
 
 次にrescue情報を積むOP_ONERRです。基本的にensureと同じですがensureとrescueの実行方法が違うため処理が少し異なっています。
 :ensureの場合|積まれるのは別のirep
 :rescueの場合|積まれるのは同じirep内のアドレス
 
 #code(C){{
     CASE(OP_ONERR) {
       /* sBx    pc+=sBx on exception */
       if (mrb->rsize <= mrb->ci->ridx) {
         if (mrb->rsize == 0) mrb->rsize = 16;
         else mrb->rsize *= 2;
         mrb->rescue = mrb_realloc(mrb, mrb->rescue, sizeof(mrb_code*) * mrb->rsize);
       }
       mrb->rescue[mrb->ci->ridx++] = pc + GETARG_sBx(i);
       NEXT;
     }
 }}
 
 **例外の発生 [#je8109e7]
 
 ***mrb_f_raise(src/kernel.c) [#s4c8b279]
 
 さて、ここからが面白いところです。Rubyではraiseは予約語ではなくメソッドということは常識だと思いますがその実装はmrb_f_raise()としてsrc/kernel.cに書かれてます。
 
 #code(C){{
 mrb_value
 mrb_f_raise(mrb_state *mrb, mrb_value self)
 {
   mrb_value a[2];
   int argc;
 
   argc = mrb_get_args(mrb, "|oo", &a[0], &a[1]);
   switch (argc) {
   case 0:
     mrb_raise(mrb, mrb->eRuntimeError_class, "");
     break;
   case 1:
     a[1] = mrb_check_string_type(mrb, a[0]);
     if (!mrb_nil_p(a[1])) {
       argc = 2;
       a[0] = mrb_obj_value(mrb->eRuntimeError_class);
     }
     /* fall through */
   default:
     mrb_exc_raise(mrb, mrb_make_exception(mrb, argc, a));
   }
   return mrb_nil_value();            /* not reached */
 }
 }}
 
 mrb_make_exception()は飛ばしてmrb_exc_raise()に進みます。ファイルはsrc/error.cに移動です。
 
 #code(C){{
 void
 mrb_exc_raise(mrb_state *mrb, mrb_value exc)
 {
     mrb->exc = (struct RObject*)mrb_object(exc);
     longjmp(*(jmp_buf*)mrb->jmp, 1);
 }
 }}
 
 **例外処理ルーチン探索 [#nea8649d]
 
 mrb_exc_raise()でlongjmpした飛び先はどこかというとmrb_run()の初めのほうに書かれているここです。
 
 #code(C){{
   if (setjmp(c_jmp) == 0) {
     prev_jmp = mrb->jmp;
     mrb->jmp = &c_jmp;
   }
   else {
     goto L_RAISE;
   }
 }}
 
 で、L_RAISEがどこにあるのかというと、OP_RETURNを処理しているところにあります。
 
 #code(C){{
     CASE(OP_RETURN) {
       /* A      return R(A) */
     L_RETURN:
       if (mrb->exc) {
         mrb_callinfo *ci;
         int eidx;
 
       L_RAISE:
 }}
 
 では例によって分割して説明していきます。
 
 #code(C){{
         ci = mrb->ci;
         eidx = mrb->ci->eidx;
         if (ci == mrb->cibase) goto L_STOP;
         while (ci[0].ridx == ci[-1].ridx) {
           cipop(mrb);
           ci = mrb->ci;
 }}
 
 whileの条件が成り立たない場合(つまり、ci[0].ridx > ci[-1].ridxの場合)、現在実行しているirep上にrescue節があることになります。rescue節がない場合は呼び出しを巻き戻すためにcipop()を呼んでいます。
 
 #code(C){{
           if (ci->acc < 0) {
             mrb->jmp = prev_jmp;
             longjmp(*(jmp_buf*)mrb->jmp, 1);
           }
 }}
 
 ci->accが負の値とはどういう場合かというとCで実装されたメソッドを呼び出す場合や後述するecall()が実行された時です。実は最近までこのコードがなかったため、例外が発生すると巻き戻りすぎるという面白いバグがありました:-P
 
 #code(C){{
           while (eidx > mrb->ci->eidx) {
             ecall(mrb, --eidx);
           }
 }}
 
 ensure節の実行です。ecall()の中身に踏み込みます。
 
 #code(C){{
 static void
 ecall(mrb_state *mrb, int i)
 {
   struct RProc *p;
   mrb_callinfo *ci;
   mrb_value *self = mrb->stack;
   struct RObject *exc;
 
   p = mrb->ensure[i];
   ci = cipush(mrb);
   ci->stackidx = mrb->stack - mrb->stbase;
   ci->mid = ci[-1].mid;
   ci->acc = -1;
   ci->argc = 0;
   ci->proc = p;
   ci->nregs = p->body.irep->nregs;
   ci->target_class = p->target_class;
   mrb->stack = mrb->stack + ci[-1].nregs;
   exc = mrb->exc; mrb->exc = 0;
   mrb_run(mrb, p, *self);
   if (!mrb->exc) mrb->exc = exc;
 }
 }}
 
 というわけでensure節の実行はmrb_run()を再帰呼び出しすることで行われています。mrb_run()の直前直後のmrb_excクリア、条件付き戻しがわかりにくいと思いますがこれは、
 
 -クリアしておかないとensure終了時にまた例外処理に入ってしまう
 -ensure中で例外が発生したら例外を差し替える
 
 という実装上の都合と仕様的な話からこのようになっています。
 
 mrb_run()のOP_RETURN、例外処理部分に戻ります。
 
 #code(C){{
           if (ci == mrb->cibase) {
             if (ci->ridx == 0) {
               mrb->stack = mrb->stbase;
               goto L_STOP;
             }
             break;
           }
         }
 }}
 
 rescueを探したけどなかった場合です。この場合はmrb_run()を終了(VMを終了)します。
 
 #code(C){{
         irep = ci->proc->body.irep;
         pool = irep->pool;
         syms = irep->syms;
         regs = mrb->stack = mrb->stbase + ci[1].stackidx;
         pc = mrb->rescue[--ci->ridx];
       }
       else {
         (省略)
       }
       JUMP;
     }
 }}
 
 rescueがあった場合の処理です。rescueの先頭にpcが設定され、処理が継続されます。
 
 **rescueの実行 [#wb50515a]
 
 rescueの先頭にはOP_RESCUE命令があります。
 
 #code(C){{
     CASE(OP_RESCUE) {
       /* A      R(A) := exc; clear(exc) */
       SET_OBJ_VALUE(regs[GETARG_A(i)],mrb->exc);
       mrb->exc = 0;
       NEXT;
     }
 }}
 
 これでレジスタに例外が格納されました。
 
 この後、例外が捕捉対象のクラスかの判定が行われます。以下のスクリプトを使ってその動きを説明します。
 
  $ ./bin/mruby.exe --verbose -e 'begin; rescue AAAError; p "AAA"; rescue BBBError; p "BBB"; end; p "XXX"'
  irep 134 nregs=5 nlocals=2 pools=3 syms=4
  000 OP_ONERR    002
  001 OP_JMP              026
  002 OP_RESCUE   R2
  003 OP_GETCONST R3      :AAAError
  004 OP_MOVE     R4      R2
  005 OP_LOADNIL  R5
  006 OP_SEND     R3      :===    1
  007 OP_JMPIF    R3      009
  008 OP_JMP              014
  009 OP_LOADSELF R2
  010 OP_STRING   R3      "AAA"
  011 OP_LOADNIL  R4
  012 OP_SEND     R2      :p      1
  013 OP_JMP              027
  014 OP_GETCONST R3      :BBBError
  015 OP_MOVE     R4      R2
  016 OP_LOADNIL  R5
  017 OP_SEND     R3      :===    1
  018 OP_JMPIF    R3      020
  019 OP_JMP              025
  020 OP_LOADSELF R2
  021 OP_STRING   R3      "BBB"
  022 OP_LOADNIL  R4
  023 OP_SEND     R2      :p      1
  024 OP_JMP              027
  025 OP_RAISE    R2
  026 OP_POPERR   1
  027 OP_LOADSELF R2
  028 OP_STRING   R3      "XXX"
  029 OP_LOADNIL  R4
  030 OP_SEND     R2      :p      1
  031 OP_STOP
 
 +レジスタR2に例外を格納
 +AAAErrorに対してレジスタR2(発生した例外)を引数に===を実行
 +trueが返ってきた(AAAErrorのサブクラスだった)ら009のrescue節本体へ
 ++rescue節を実行したらその後のコードに飛ぶ(013のOP_JMP部分)
 +falseの場合は008に進み、014(次のrescue条件判定)にジャンプする
 +いずれのrescueも発生した例外を捕捉しない場合はOP_RAISE(例外再送)を実行
 
 **例外が起こらなかった場合の処理 [#h2b2dd9e]
 
 次に例外が起こらない場合どうなるかを見てみます。と思ったのですがあまり面白くないのでやめます。OP_POPERR, OP_EPOPが実行されて積まれていたrescue節とensure節が取り除かれ、ensure節はpop時に先ほどのecall()が実行されるという普通の処理が行われています。
 
 *おわりに [#m877ff55]
 
 というわけでmrubyの例外処理周りを見てきました。このあたりはバグが多く、解説を書くためにバグを直すことになりました:-<((大体、先行してまつもとさんに直されてしまいましたが))。みなさんもがしがしバグを踏んで直してpull requestしmrubyの発展に貢献しましょう。制御構造関連についてはもうあまりないと思いますが。
 
 なお、例外処理ルーチン実行の実装はYARVと結構違っています。YARVでの例外処理ルーチン実行の実装については[[Ruby1.9/例外処理を読む]]をご参照ください。
 

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS