[[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/例外処理を読む]]をご参照ください。