mrubyを読む

はじめに

今回はmrubyの例外処理を読解します。なお、実行手順読解で使ったバージョンは例外処理にバグがあるので今回は2012/6/20に取得したcommit 7744315d88を使用します。

サンプルスクリプト

基本的に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ツリーへの変換

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対象となるのがどの部分かということがわかります。

実行コードへの変換

実行コード変換結果

次に実行コード生成です。ソースを見る前に生成される実行コードを見ます。

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

それではソースを見てみましょう。なお、以下のコード断片はsrc/codegen.c中のcodegen()の一部です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 
-
|
|
|
|
|
|
|
|
|
|
!
 
  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部分です。やっていることは以下の通りです。

  1. まず、OP_EPUSHを埋め込んで
  2. body部分のコードを生成し
  3. 次にensure部分のコードを生成してirep番号を取得
  4. 先ほど埋め込んだOP_EPUSHに取得したirep番号を設定
  5. 最後に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

NODE_ENSUREに比べNODE_RESCUEはかなり複雑です。例によって部分に分けて解説します。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
 
-
|
|
|
|
|
  case NODE_RESCUE:
    {
      int onerr, noexc, exend, pos1, pos2, tmp;
      struct loopinfo *lp;
 
      onerr = new_label(s);
      genop(s, MKOP_Bx(OP_ONERR, 0));

まず「例外が起きたらここへ」なコードを埋め込んでいます。ただし、実際のジャンプ先はまだ決まっていません。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
 
 
-
|
!
      lp = loop_push(s, LOOP_BEGIN);
      lp->pc1 = onerr;
      if (tree->car) {
        codegen(s, tree->car, val);
      }

body部分のコードを生成しています。

Everything is expanded.Everything is shortened.
  1
  2
  3
 
 
 
      lp->type = LOOP_RESCUE;
      noexc = new_label(s);
      genop(s, MKOP_Bx(OP_JMP, 0));

例外が起きなかった場合のジャンプ命令を埋め込んでいます。

Everything is expanded.Everything is shortened.
  1
 
      dispatch(s, onerr);

以下、rescue部分のコードを生成します。というわけで「例外が起きたらここへ」がどこへ飛べばいいのかわかるので最初に埋めたON_ERRのジャンプ先を設定しています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 
 
 
-
|
|
|
|
|
      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();

まず発生した例外を受け取る命令を生成しています。

Everything is expanded.Everything is shortened.
  1
  2
  3
-
|
|
        while (n2) {
          node *n3 = n2->car;
          node *n4 = n3->car;

rescue節の数だけ、ループを回します。n3には発生した例外を格納する変数情報、n4にはrescue対象とする例外のクラスが入るようです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 
-
-
|
!
-
|
|
!
|
|
|
|
|
|
|
|
-
|
!
!
          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

埋め込んでいる処理内容は以下のようになります。

  1. 例外クラスを取得して
  2. 発生した例外を引数に例外クラスに対して===を実行
  3. trueが返ってきたらrescue節の中身が書かれている部分にジャンプ
Everything is expanded.Everything is shortened.
  1
  2
  3
 
 
 
          pos1 = new_label(s);
          genop(s, MKOP_sBx(OP_JMP, 0));
          dispatch_linked(s, pos2);

あるrescue節で対象となる例外クラスでなかった場合に次のrescue節に飛ぶためのジャンプ命令です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
-
|
!
          pop();
          if (n3->cdr->car) {
            gen_assignment(s, n3->cdr->car, exc, NOVAL);
          }

例外を受け取る変数に代入するコードを生成しています。

Everything is expanded.Everything is shortened.
  1
  2
  3
-
|
!
          if (n3->cdr->cdr->car) {
            codegen(s, n3->cdr->cdr->car, val);
          }

rescue節の本体に対応するコードを生成しています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
 
 
 
 
!
          tmp = new_label(s);
          genop(s, MKOP_sBx(OP_JMP, exend));
          exend = tmp;
          n2 = n2->cdr;
          push();
        }

最後にrescueの末尾に飛ぶジャンプ命令を生成しています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
-
|
|
!
!
        if (pos1) {
          dispatch(s, pos1);
          genop(s, MKOP_A(OP_RAISE, exc));
        }
      }

いずれのrescue節にも引っ掛からなかった場合に例外を再送する処理を行っています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
      pop();
      tree = tree->cdr;
      dispatch(s, noexc);
      genop(s, MKOP_A(OP_POPERR, 1));

例外が起きなかった場合のジャンプ命令の飛び先を設定し、積んだrescue情報を下す処理を埋め込んでいます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
-
|
!
 
 
!
|
      if (tree->car) {
        codegen(s, tree->car, val);
      }
      dispatch_linked(s, exend);
      loop_pop(s, NOVAL);
    }
    break;

最後にelse節のコードを生成して終了です。

コードの実行

コードの生成を見たので次はコードの実行を見てみましょう。なお、今後のコード断片はsrc/vm.cのmrb_run()の一部です。

例外に対する備え

OP_EPUSH

まず、ensureを積むOP_EPUSHです。callinfoのeidxで積まれているensureの数を管理しています。この情報は後で重要になるので覚えておいてください。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
-
|
|
|
|
|
-
|
|
|
!
|
|
!
    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

次にrescue情報を積むOP_ONERRです。基本的にensureと同じですがensureとrescueの実行方法が違うため処理が少し異なっています。

ensureの場合
積まれるのは別のirep
rescueの場合
積まれるのは同じirep内のアドレス
Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
-
|
-
|
|
|
!
|
|
!
    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;
    }

例外の発生

mrb_f_raise(src/kernel.c)

さて、ここからが面白いところです。Rubyではraiseは予約語ではなくメソッドということは常識だと思いますがその実装はmrb_f_raise()としてsrc/kernel.cに書かれてます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 
-
|
|
|
|
-
|
|
|
|
|
-
|
|
!
|
|
|
!
|
!
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に移動です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
 
-
|
|
!
void
mrb_exc_raise(mrb_state *mrb, mrb_value exc)
{
    mrb->exc = (struct RObject*)mrb_object(exc);
    longjmp(*(jmp_buf*)mrb->jmp, 1);
}

例外処理ルーチン探索

mrb_exc_raise()でlongjmpした飛び先はどこかというとmrb_run()の初めのほうに書かれているここです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
-
|
|
!
-
|
!
  if (setjmp(c_jmp) == 0) {
    prev_jmp = mrb->jmp;
    mrb->jmp = &c_jmp;
  }
  else {
    goto L_RAISE;
  }

で、L_RAISEがどこにあるのかというと、OP_RETURNを処理しているところにあります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
-
|
|
-
|
|
|
|
    CASE(OP_RETURN) {
      /* A      return R(A) */
    L_RETURN:
      if (mrb->exc) {
        mrb_callinfo *ci;
        int eidx;
 
      L_RAISE:

では例によって分割して説明していきます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
 
 
-
|
|
        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()を呼んでいます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
-
|
|
!
          if (ci->acc < 0) {
            mrb->jmp = prev_jmp;
            longjmp(*(jmp_buf*)mrb->jmp, 1);
          }

ci->accが負の値とはどういう場合かというとCで実装されたメソッドを呼び出す場合や後述するecall()が実行された時です。実は最近までこのコードがなかったため、例外が発生すると巻き戻りすぎるという面白いバグがありました:-P

Everything is expanded.Everything is shortened.
  1
  2
  3
-
|
!
          while (eidx > mrb->ci->eidx) {
            ecall(mrb, --eidx);
          }

ensure節の実行です。ecall()の中身に踏み込みます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
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、例外処理部分に戻ります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
-
-
|
|
!
|
!
!
          if (ci == mrb->cibase) {
            if (ci->ridx == 0) {
              mrb->stack = mrb->stbase;
              goto L_STOP;
            }
            break;
          }
        }

rescueを探したけどなかった場合です。この場合はmrb_run()を終了(VMを終了)します。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 
 
 
 
 
!
-
 
!
|
!
        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の実行

rescueの先頭にはOP_RESCUE命令があります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
-
|
|
|
|
!
    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
  1. レジスタR2に例外を格納
  2. AAAErrorに対してレジスタR2(発生した例外)を引数に===を実行
  3. trueが返ってきた(AAAErrorのサブクラスだった)ら009のrescue節本体へ
    1. rescue節を実行したらその後のコードに飛ぶ(013のOP_JMP部分)
  4. falseの場合は008に進み、014(次のrescue条件判定)にジャンプする
  5. いずれのrescueも発生した例外を捕捉しない場合はOP_RAISE(例外再送)を実行

例外が起こらなかった場合の処理

次に例外が起こらない場合どうなるかを見てみます。と思ったのですがあまり面白くないのでやめます。OP_POPERR, OP_EPOPが実行されて積まれていたrescue節とensure節が取り除かれ、ensure節はpop時に先ほどのecall()が実行されるという普通の処理が行われています。

おわりに

というわけでmrubyの例外処理周りを見てきました。このあたりはバグが多く、解説を書くためにバグを直すことになりました:-<*1。みなさんもがしがしバグを踏んで直してpull requestしmrubyの発展に貢献しましょう。制御構造関連についてはもうあまりないと思いますが。

なお、例外処理ルーチン実行の実装はYARVと結構違っています。YARVでの例外処理ルーチン実行の実装についてはRuby1.9/例外処理を読むをご参照ください。


*1 大体、先行してまつもとさんに直されてしまいましたが

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2012-06-20 (水) 21:30:58 (4320d)