はじめに †今回は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()の一部です。
tree->carはbody部分、tree->cdrはensure部分です。やっていることは以下の通りです。
なお、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はかなり複雑です。例によって部分に分けて解説します。
まず「例外が起きたらここへ」なコードを埋め込んでいます。ただし、実際のジャンプ先はまだ決まっていません。
body部分のコードを生成しています。
例外が起きなかった場合のジャンプ命令を埋め込んでいます。
以下、rescue部分のコードを生成します。というわけで「例外が起きたらここへ」がどこへ飛べばいいのかわかるので最初に埋めたON_ERRのジャンプ先を設定しています。
まず発生した例外を受け取る命令を生成しています。
rescue節の数だけ、ループを回します。n3には発生した例外を格納する変数情報、n4にはrescue対象とする例外のクラスが入るようです。
rescue対象となる例外クラスの数だけループを回します。具体的には以下のように書かれているとループが2度回ります。 rescue AAAError, BBBError 埋め込んでいる処理内容は以下のようになります。
あるrescue節で対象となる例外クラスでなかった場合に次のrescue節に飛ぶためのジャンプ命令です。
例外を受け取る変数に代入するコードを生成しています。
rescue節の本体に対応するコードを生成しています。
最後にrescueの末尾に飛ぶジャンプ命令を生成しています。
いずれのrescue節にも引っ掛からなかった場合に例外を再送する処理を行っています。
例外が起きなかった場合のジャンプ命令の飛び先を設定し、積んだrescue情報を下す処理を埋め込んでいます。
最後にelse節のコードを生成して終了です。 コードの実行 †コードの生成を見たので次はコードの実行を見てみましょう。なお、今後のコード断片はsrc/vm.cのmrb_run()の一部です。 例外に対する備え †OP_EPUSH †まず、ensureを積むOP_EPUSHです。callinfoのeidxで積まれているensureの数を管理しています。この情報は後で重要になるので覚えておいてください。
OP_ONERR †次にrescue情報を積むOP_ONERRです。基本的にensureと同じですがensureとrescueの実行方法が違うため処理が少し異なっています。
例外の発生 †mrb_f_raise(src/kernel.c) †さて、ここからが面白いところです。Rubyではraiseは予約語ではなくメソッドということは常識だと思いますがその実装はmrb_f_raise()としてsrc/kernel.cに書かれてます。
mrb_make_exception()は飛ばしてmrb_exc_raise()に進みます。ファイルはsrc/error.cに移動です。
例外処理ルーチン探索 †mrb_exc_raise()でlongjmpした飛び先はどこかというとmrb_run()の初めのほうに書かれているここです。 で、L_RAISEがどこにあるのかというと、OP_RETURNを処理しているところにあります。 では例によって分割して説明していきます。
whileの条件が成り立たない場合(つまり、ci[0].ridx > ci[-1].ridxの場合)、現在実行しているirep上にrescue節があることになります。rescue節がない場合は呼び出しを巻き戻すためにcipop()を呼んでいます。
ci->accが負の値とはどういう場合かというとCで実装されたメソッドを呼び出す場合や後述するecall()が実行された時です。実は最近までこのコードがなかったため、例外が発生すると巻き戻りすぎるという面白いバグがありました:-P
ensure節の実行です。ecall()の中身に踏み込みます。
というわけでensure節の実行はmrb_run()を再帰呼び出しすることで行われています。mrb_run()の直前直後のmrb_excクリア、条件付き戻しがわかりにくいと思いますがこれは、
という実装上の都合と仕様的な話からこのようになっています。 mrb_run()のOP_RETURN、例外処理部分に戻ります。 rescueを探したけどなかった場合です。この場合はmrb_run()を終了(VMを終了)します。
rescueがあった場合の処理です。rescueの先頭にpcが設定され、処理が継続されます。 rescueの実行 †rescueの先頭にはOP_RESCUE命令があります。
これでレジスタに例外が格納されました。 この後、例外が捕捉対象のクラスかの判定が行われます。以下のスクリプトを使ってその動きを説明します。 $ ./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
例外が起こらなかった場合の処理 †次に例外が起こらない場合どうなるかを見てみます。と思ったのですがあまり面白くないのでやめます。OP_POPERR, OP_EPOPが実行されて積まれていたrescue節とensure節が取り除かれ、ensure節はpop時に先ほどのecall()が実行されるという普通の処理が行われています。 おわりに †というわけでmrubyの例外処理周りを見てきました。このあたりはバグが多く、解説を書くためにバグを直すことになりました:-<*1。みなさんもがしがしバグを踏んで直してpull requestしmrubyの発展に貢献しましょう。制御構造関連についてはもうあまりないと思いますが。 なお、例外処理ルーチン実行の実装はYARVと結構違っています。YARVでの例外処理ルーチン実行の実装についてはRuby1.9/例外処理を読むをご参照ください。 |