qiitaの下書きを整理していたら何か書きかけの奴が見つかったが、消すのももったいないというか自分もそのうち見返したいのでそのまま投下しておく。ので、変なところがあるかもしれない
背景
前回の記事による理解で、以下のコードのデバッグが多少進んだ。
def timeout(sec)
bl = proc do
y = Thread.start {
sleep sec
}
result = yield(sec)
y.kill
y.join
return result
end
bl.call
caller(0)
end
timeout(100) do
foo.bar
end
ところが、実際にはbl.callでVMを起動する時のISeqがfoo.barのあるブロックになってしまい、そこでfoo.barを呼び出した後普通に次の行に抜け、本来大域脱出により到達しないはずのcaller(0)の呼び出しに到達してしまいました。
gdbで見てみるとbl.call
からvm_call_opt_call
経由でinvoke_block
により積まれているcfpのiseqには正しいiseqがセットされ、実行され始めていた。なので上記の引用の内容は間違っていた。
一方、そこで起動されたVM内でfoo.bar
のブロックのiseqが実行されるのもまた事実のようである。
これは、例外処理のルーチンからvm_exec_core
の手前にgotoされることで、一度のvm_exec
呼び出しの中でvm_exec_core
が二度実行されていることがprintデバッグによりわかった。
なので、このメソッドが本来どういう順番でVMに処理されるはずなのかを追いかけることにする。
returnでの大域脱出の仕組み
どのような時にexception_handlerに到達するか
長すぎるため全体の引用はしないのでこちらを参照: https://github.com/ruby/ruby/blob/9f68d0f59196f356b9c20f23a5299c3d32559eae/vm.c#L1765-L2005
まず、goto vm_loop_start
があるのはexception_handler:
ラベル以降なので、一度のvm_exec
の中で複数回vm_exec_core
が実行されるためには、vm_exec
内のexception_handler:
というラベルに到達しないといけない。
exception_handler:
に来るパスには、最初の分岐のEC_EXEC_TAG()
がTAG_NONE
以外を返すケースと、vm_exec_core
を抜けた後_tag.state
がTAG_NONE
以外を返すケースの2通りある。
全部の条件を見ていくのはダルめなので、今回の例のコードの大域脱出だとどちらに行きそうかだけ考える
ブロック内returnでメソッドから抜ける時の処理
最初に出したコードのISeqはこれ
$ RBENV_VERSION=trunk-svn ruby --dump=insns b.rb
== disasm: #<ISeq:<main>@b.rb:0>========================================
== catch table
| catch type: break st: 0010 ed: 0017 sp: 0000 cont: 0017
== disasm: #<ISeq:block in <main>@b.rb:15>==============================
== catch table
| catch type: redo st: 0001 ed: 0008 sp: 0000 cont: 0001
| catch type: next st: 0001 ed: 0008 sp: 0000 cont: 0008
|------------------------------------------------------------------------
0000 nop ( 15)[Bc]
0001 putself ( 16)[Li]
0002 opt_send_without_block <callinfo!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0005 opt_send_without_block <callinfo!mid:bar, argc:0, ARGS_SIMPLE>, <callcache>
0008 leave [Br]
|------------------------------------------------------------------------
0000 putspecialobject 1 ( 1)[Li]
0002 putobject :timeout
0004 putiseq timeout
0006 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>
0009 pop
0010 putself ( 15)[Li]
0011 putobject 100
0013 send <callinfo!mid:timeout, argc:1, FCALL>, <callcache>, block in <main>
0017 leave
== disasm: #<ISeq:timeout@b.rb:1>=======================================
== catch table
| catch type: break st: 0000 ed: 0005 sp: 0000 cont: 0005
== disasm: #<ISeq:block in timeout@b.rb:2>==============================
== catch table
| catch type: break st: 0001 ed: 0012 sp: 0000 cont: 0012
== disasm: #<ISeq:block (2 levels) in timeout@b.rb:3>===================
== catch table
| catch type: redo st: 0001 ed: 0008 sp: 0000 cont: 0001
| catch type: next st: 0001 ed: 0008 sp: 0000 cont: 0008
|------------------------------------------------------------------------
0000 nop ( 3)[Bc]
0001 putself ( 4)[Li]
0002 getlocal sec, 2
0005 opt_send_without_block <callinfo!mid:sleep, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0008 leave [Br]
| catch type: redo st: 0001 ed: 0036 sp: 0000 cont: 0001
| catch type: next st: 0001 ed: 0036 sp: 0000 cont: 0036
|------------------------------------------------------------------------
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] y [ 1] result
0000 nop ( 2)[Bc]
0001 getinlinecache 8, <is:0> ( 3)[Li]
0004 getconstant :Thread
0006 setinlinecache <is:0>
0008 send <callinfo!mid:start, argc:0>, <callcache>, block (2 levels) in timeout
0012 setlocal_OP__WC__0 y
0014 getlocal_OP__WC__1 sec ( 6)[Li]
0016 invokeblock <callinfo!argc:1, ARGS_SIMPLE>
0018 setlocal_OP__WC__0 result
0020 getlocal_OP__WC__0 y ( 7)[Li]
0022 opt_send_without_block <callinfo!mid:kill, argc:0, ARGS_SIMPLE>, <callcache>
0025 pop
0026 getlocal_OP__WC__0 y ( 8)[Li]
0028 opt_send_without_block <callinfo!mid:join, argc:0, ARGS_SIMPLE>, <callcache>
0031 pop
0032 getlocal_OP__WC__0 result ( 9)[Li]
0034 throw 1
0036 leave [Br]
|------------------------------------------------------------------------
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] sec<Arg> [ 1] bl
0000 putself ( 2)[LiCa]
0001 send <callinfo!mid:proc, argc:0, FCALL>, <callcache>, block in timeout
0005 setlocal_OP__WC__0 bl
0007 getlocal_OP__WC__0 bl ( 11)[Li]
0009 opt_send_without_block <callinfo!mid:call, argc:0, ARGS_SIMPLE>, <callcache>
0012 pop
0013 putself ( 12)[Li]
0014 putobject_OP_INT2FIX_O_0_C_
0015 opt_send_without_block <callinfo!mid:caller, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0018 leave [Re]
return
に該当しそうなところを探すと、
0034 throw 1
なんかこの後に無駄っぽいleaveあるけどまあ置いておく。
throwの定義を見る。
/**
@c exception
@e longjump
@j 大域ジャンプを行う。
*/
DEFINE_INSN
throw
(rb_num_t throw_state)
(VALUE throwobj)
(VALUE val)
{
RUBY_VM_CHECK_INTS(ec);
val = vm_throw(ec, GET_CFP(), throw_state, throwobj);
THROW_EXCEPTION(val);
/* unreachable */
}
vm_throw はどのようにvm_throw_data
を作るか
static VALUE
vm_throw(const rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
rb_num_t throw_state, VALUE throwobj)
{
const int state = (int)(throw_state & VM_THROW_STATE_MASK);
const int flag = (int)(throw_state & VM_THROW_NO_ESCAPE_FLAG);
const rb_num_t level = throw_state >> VM_THROW_LEVEL_SHIFT;
if (state != 0) {
return vm_throw_start(ec, reg_cfp, state, flag, level, throwobj);
}
else {
return vm_throw_continue(ec, throwobj);
}
}
enum ruby_vm_throw_flags {
VM_THROW_NO_ESCAPE_FLAG = 0x8000,
VM_THROW_LEVEL_SHIFT = 16,
VM_THROW_STATE_MASK = 0xff
};
static VALUE
vm_throw_start(const rb_execution_context_t *ec, rb_control_frame_t *const reg_cfp, enum ruby_tag_type state,
const int flag, const rb_num_t level, const VALUE throwobj)
{
const rb_control_frame_t *escape_cfp = NULL;
const rb_control_frame_t * const eocfp = RUBY_VM_END_CONTROL_FRAME(ec); /* end of control frame pointer */
if (flag != 0) {
/* do nothing */
}
else if (state == TAG_BREAK) {
int is_orphan = 1;
const VALUE *ep = GET_EP();
const rb_iseq_t *base_iseq = GET_ISEQ();
escape_cfp = reg_cfp;
while (base_iseq->body->type != ISEQ_TYPE_BLOCK) {
if (escape_cfp->iseq->body->type == ISEQ_TYPE_CLASS) {
escape_cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(escape_cfp);
ep = escape_cfp->ep;
base_iseq = escape_cfp->iseq;
}
else {
ep = VM_ENV_PREV_EP(ep);
base_iseq = base_iseq->body->parent_iseq;
escape_cfp = rb_vm_search_cf_from_ep(ec, escape_cfp, ep);
VM_ASSERT(escape_cfp->iseq == base_iseq);
}
}
if (VM_FRAME_LAMBDA_P(escape_cfp)) {
/* lambda{... break ...} */
is_orphan = 0;
state = TAG_RETURN;
}
else {
ep = VM_ENV_PREV_EP(ep);
while (escape_cfp < eocfp) {
if (escape_cfp->ep == ep) {
const rb_iseq_t *const iseq = escape_cfp->iseq;
const VALUE epc = escape_cfp->pc - iseq->body->iseq_encoded;
const struct iseq_catch_table *const ct = iseq->body->catch_table;
unsigned int i;
if (!ct) break;
for (i=0; i < ct->size; i++) {
const struct iseq_catch_table_entry * const entry = &ct->entries[i];
if (entry->type == CATCH_TYPE_BREAK &&
entry->iseq == base_iseq &&
entry->start < epc && entry->end >= epc) {
if (entry->cont == epc) { /* found! */
is_orphan = 0;
}
break;
}
}
break;
}
escape_cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(escape_cfp);
}
}
if (is_orphan) {
rb_vm_localjump_error("break from proc-closure", throwobj, TAG_BREAK);
}
}
else if (state == TAG_RETRY) {
rb_num_t i;
const VALUE *ep = VM_ENV_PREV_EP(GET_EP());
for (i = 0; i < level; i++) {
ep = VM_ENV_PREV_EP(ep);
}
escape_cfp = rb_vm_search_cf_from_ep(ec, reg_cfp, ep);
}
else if (state == TAG_RETURN) {
const VALUE *current_ep = GET_EP();
const VALUE *target_lep = VM_EP_LEP(current_ep);
int in_class_frame = 0;
int toplevel = 1;
escape_cfp = reg_cfp;
while (escape_cfp < eocfp) {
const VALUE *lep = VM_CF_LEP(escape_cfp);
if (!target_lep) {
target_lep = lep;
}
if (lep == target_lep &&
VM_FRAME_RUBYFRAME_P(escape_cfp) &&
escape_cfp->iseq->body->type == ISEQ_TYPE_CLASS) {
in_class_frame = 1;
target_lep = 0;
}
if (lep == target_lep) {
if (VM_FRAME_LAMBDA_P(escape_cfp)) {
toplevel = 0;
if (in_class_frame) {
/* lambda {class A; ... return ...; end} */
goto valid_return;
}
else {
const VALUE *tep = current_ep;
while (target_lep != tep) {
if (escape_cfp->ep == tep) {
/* in lambda */
goto valid_return;
}
tep = VM_ENV_PREV_EP(tep);
}
}
}
else if (VM_FRAME_RUBYFRAME_P(escape_cfp)) {
switch (escape_cfp->iseq->body->type) {
case ISEQ_TYPE_TOP:
case ISEQ_TYPE_MAIN:
if (toplevel) goto valid_return;
break;
case ISEQ_TYPE_EVAL:
case ISEQ_TYPE_CLASS:
toplevel = 0;
break;
default:
break;
}
}
}
if (escape_cfp->ep == target_lep && escape_cfp->iseq->body->type == ISEQ_TYPE_METHOD) {
goto valid_return;
}
escape_cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(escape_cfp);
}
rb_vm_localjump_error("unexpected return", throwobj, TAG_RETURN);
valid_return:;
/* do nothing */
}
else {
rb_bug("isns(throw): unsupport throw type");
}
ec->tag->state = state;
return (VALUE)THROW_DATA_NEW(throwobj, escape_cfp, state);
}
enum ruby_tag_type {
RUBY_TAG_NONE = 0x0,
RUBY_TAG_RETURN = 0x1,
RUBY_TAG_BREAK = 0x2,
RUBY_TAG_NEXT = 0x3,
RUBY_TAG_RETRY = 0x4,
RUBY_TAG_REDO = 0x5,
RUBY_TAG_RAISE = 0x6,
RUBY_TAG_THROW = 0x7,
RUBY_TAG_FATAL = 0x8,
RUBY_TAG_MASK = 0xf
};
#define TAG_NONE RUBY_TAG_NONE
#define TAG_RETURN RUBY_TAG_RETURN
#define TAG_BREAK RUBY_TAG_BREAK
#define TAG_NEXT RUBY_TAG_NEXT
#define TAG_RETRY RUBY_TAG_RETRY
#define TAG_REDO RUBY_TAG_REDO
#define TAG_RAISE RUBY_TAG_RAISE
#define TAG_THROW RUBY_TAG_THROW
#define TAG_FATAL RUBY_TAG_FATAL
#define TAG_MASK RUBY_TAG_MASK
static inline struct vm_throw_data *
THROW_DATA_NEW(VALUE val, const rb_control_frame_t *cf, VALUE st)
{
return (struct vm_throw_data *)rb_imemo_new(imemo_throw_data, val, (VALUE)cf, st, 0);
}
/*! THROW_DATA */
struct vm_throw_data {
VALUE flags;
VALUE reserved;
const VALUE throw_obj;
const struct rb_control_frame_struct *catch_frame;
VALUE throw_state;
};
VALUE
rb_imemo_new(enum imemo_type type, VALUE v1, VALUE v2, VALUE v3, VALUE v0)
{
VALUE flags = T_IMEMO | (type << FL_USHIFT);
return newobj_of(v0, flags, v1, v2, v3, TRUE);
}
static inline VALUE
newobj_of(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3, int wb_protected)
{
rb_objspace_t *objspace = &rb_objspace;
VALUE obj;
#if GC_DEBUG_STRESS_TO_CLASS
if (UNLIKELY(stress_to_class)) {
long i, cnt = RARRAY_LEN(stress_to_class);
const VALUE *ptr = RARRAY_CONST_PTR(stress_to_class);
for (i = 0; i < cnt; ++i) {
if (klass == ptr[i]) rb_memerror();
}
}
#endif
if (!(during_gc ||
ruby_gc_stressful ||
gc_event_hook_available_p(objspace)) &&
(obj = heap_get_freeobj_head(objspace, heap_eden)) != Qfalse) {
return newobj_init(klass, flags, v1, v2, v3, wb_protected, objspace, obj);
}
else {
return wb_protected ?
newobj_slowpath_wb_protected(klass, flags, v1, v2, v3, objspace) :
newobj_slowpath_wb_unprotected(klass, flags, v1, v2, v3, objspace);
}
}
static VALUE
newobj_slowpath_wb_protected(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3, rb_objspace_t *objspace)
{
return newobj_slowpath(klass, flags, v1, v2, v3, objspace, TRUE);
}
static inline VALUE
newobj_slowpath(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3, rb_objspace_t *objspace, int wb_protected)
{
VALUE obj;
if (UNLIKELY(during_gc || ruby_gc_stressful)) {
if (during_gc) {
dont_gc = 1;
during_gc = 0;
rb_bug("object allocation during garbage collection phase");
}
if (ruby_gc_stressful) {
if (!garbage_collect(objspace, FALSE, FALSE, FALSE, GPR_FLAG_NEWOBJ)) {
rb_memerror();
}
}
}
obj = heap_get_freeobj(objspace, heap_eden);
newobj_init(klass, flags, v1, v2, v3, wb_protected, objspace, obj);
gc_event_hook(objspace, RUBY_INTERNAL_EVENT_NEWOBJ, obj);
return obj;
}
static inline VALUE
newobj_init(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3, int wb_protected, rb_objspace_t *objspace, VALUE obj)
{
GC_ASSERT(BUILTIN_TYPE(obj) == T_NONE);
GC_ASSERT((flags & FL_WB_PROTECTED) == 0);
/* OBJSETUP */
RBASIC(obj)->flags = flags;
RBASIC_SET_CLASS_RAW(obj, klass);
RANY(obj)->as.values.v1 = v1;
RANY(obj)->as.values.v2 = v2;
RANY(obj)->as.values.v3 = v3;
#if RGENGC_CHECK_MODE
GC_ASSERT(RVALUE_MARKED(obj) == FALSE);
GC_ASSERT(RVALUE_MARKING(obj) == FALSE);
GC_ASSERT(RVALUE_OLD_P(obj) == FALSE);
GC_ASSERT(RVALUE_WB_UNPROTECTED(obj) == FALSE);
if (flags & FL_PROMOTED1) {
if (RVALUE_AGE(obj) != 2) rb_bug("newobj: %s of age (%d) != 2.", obj_info(obj), RVALUE_AGE(obj));
}
else {
if (RVALUE_AGE(obj) > 0) rb_bug("newobj: %s of age (%d) > 0.", obj_info(obj), RVALUE_AGE(obj));
}
if (rgengc_remembered(objspace, (VALUE)obj)) rb_bug("newobj: %s is remembered.", obj_info(obj));
#endif
#if USE_RGENGC
if (UNLIKELY(wb_protected == FALSE)) {
MARK_IN_BITMAP(GET_HEAP_WB_UNPROTECTED_BITS(obj), obj);
}
#endif
#if RGENGC_PROFILE
if (wb_protected) {
objspace->profile.total_generated_normal_object_count++;
#if RGENGC_PROFILE >= 2
objspace->profile.generated_normal_object_count_types[BUILTIN_TYPE(obj)]++;
#endif
}
else {
objspace->profile.total_generated_shady_object_count++;
#if RGENGC_PROFILE >= 2
objspace->profile.generated_shady_object_count_types[BUILTIN_TYPE(obj)]++;
#endif
}
#endif
#if GC_DEBUG
RANY(obj)->file = rb_source_location_cstr(&RANY(obj)->line);
GC_ASSERT(!SPECIAL_CONST_P(obj)); /* check alignment */
#endif
objspace->total_allocated_objects++;
gc_report(5, objspace, "newobj: %s\n", obj_info(obj));
#if RGENGC_OLD_NEWOBJ_CHECK > 0
{
static int newobj_cnt = RGENGC_OLD_NEWOBJ_CHECK;
if (!is_incremental_marking(objspace) &&
flags & FL_WB_PROTECTED && /* do not promote WB unprotected objects */
! RB_TYPE_P(obj, T_ARRAY)) { /* array.c assumes that allocated objects are new */
if (--newobj_cnt == 0) {
newobj_cnt = RGENGC_OLD_NEWOBJ_CHECK;
gc_mark_set(objspace, obj);
RVALUE_AGE_SET_OLD(objspace, obj);
rb_gc_writebarrier_remember(obj);
}
}
}
#endif
check_rvalue_consistency(obj);
return obj;
}
#define RANY(o) ((RVALUE*)(o))
typedef struct RVALUE {
union {
struct {
VALUE flags; /* always 0 for freed obj */
struct RVALUE *next;
} free;
struct RBasic basic;
struct RObject object;
struct RClass klass;
struct RFloat flonum;
struct RString string;
struct RArray array;
struct RRegexp regexp;
struct RHash hash;
struct RData data;
struct RTypedData typeddata;
struct RStruct rstruct;
struct RBignum bignum;
struct RFile file;
struct RMatch match;
struct RRational rational;
struct RComplex complex;
union {
rb_cref_t cref;
struct vm_svar svar;
struct vm_throw_data throw_data;
struct vm_ifunc ifunc;
struct MEMO memo;
struct rb_method_entry_struct ment;
const rb_iseq_t iseq;
rb_env_t env;
struct rb_imemo_alloc_struct alloc;
rb_ast_t ast;
} imemo;
struct {
struct RBasic basic;
VALUE v1;
VALUE v2;
VALUE v3;
} values;
} as;
#if GC_DEBUG
const char *file;
int line;
#endif
} RVALUE;
THROW_DATA_NEW
ってのをやるとRubyが管理しているヒープのページを取ってきて、そこにv1, v2, v3をセットするようです。ざっと見た感じだと以下のような構造になりそうです
struct vm_throw_data {
VALUE flags; /* RBasic flags */
VALUE reserved; /* RBasic klass, 多分0? */
const VALUE throw_obj; /* as.values.v1, throwobj (YARVスタックの一番上にあったもの) */
const struct rb_control_frame_struct *catch_frame; /* as.values.v2, escape_cfp (脱出先のcfp) */
VALUE throw_state; /* as.values.v3, state (TAG_RETURN) */
};
脱出先のcfpはreg_cfp->ep
からlepを探し、そのepを持つcfpを探しているようです。まあブロックを持つ親メソッドのフレームになりそうですね。
ここでimemoを作る前にec->tag->state = TAG_RETURN;
も行なわれていることに注意する。
THROW_EXCEPTION
は何をするか
#define OPT_CALL_THREADED_CODE 0
#define THROW_EXCEPTION(exc) return (VALUE)(exc)
ここでvm_exec_core
はstruct vm_throw_data *
を返すようですね。
vm_exec_core
からstruct vm_throw_data *
が返された時の処理
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
vm_loop_start:
result = vm_exec_core(ec, initial);
VM_ASSERT(ec->tag == &_tag);
if ((state = _tag.state) != TAG_NONE) {
err = (struct vm_throw_data *)result;
_tag.state = TAG_NONE;
goto exception_handler;
}
}
ec->tag->state = TAG_RETURN
が行なわれているので、struct vm_throw_data *err;
にstruct vm_throw_data *
が渡され、ec->tag->state
をTAG_NONE
に戻した上でgoto exception_handler
されます。
exception_handler:
cont_pc = cont_sp = 0;
catch_iseq = NULL;
while (ec->cfp->pc == 0 || ec->cfp->iseq == 0) {
if (UNLIKELY(VM_FRAME_TYPE(ec->cfp) == VM_FRAME_MAGIC_CFUNC)) {
EXEC_EVENT_HOOK(ec, RUBY_EVENT_C_RETURN, ec->cfp->self,
rb_vm_frame_method_entry(ec->cfp)->def->original_id,
rb_vm_frame_method_entry(ec->cfp)->called_id,
rb_vm_frame_method_entry(ec->cfp)->owner, Qnil);
RUBY_DTRACE_CMETHOD_RETURN_HOOK(ec,
rb_vm_frame_method_entry(ec->cfp)->owner,
rb_vm_frame_method_entry(ec->cfp)->def->original_id);
}
rb_vm_pop_frame(ec);
}
cfp = ec->cfp;
epc = cfp->pc - cfp->iseq->body->iseq_encoded;
escape_cfp = NULL;
if (state == TAG_BREAK || state == TAG_RETURN) {
escape_cfp = THROW_DATA_CATCH_FRAME(err);
if (cfp == escape_cfp) {
if (state == TAG_RETURN) {
if (!VM_FRAME_FINISHED_P(cfp)) {
THROW_DATA_CATCH_FRAME_SET(err, cfp + 1);
THROW_DATA_STATE_SET(err, state = TAG_BREAK);
}
else {
ct = cfp->iseq->body->catch_table;
if (ct) for (i = 0; i < ct->size; i++) {
entry = &ct->entries[i];
if (entry->start < epc && entry->end >= epc) {
if (entry->type == CATCH_TYPE_ENSURE) {
catch_iseq = entry->iseq;
cont_pc = entry->cont;
cont_sp = entry->sp;
break;
}
}
}
if (catch_iseq == NULL) {
ec->errinfo = Qnil;
result = THROW_DATA_VAL(err);
THROW_DATA_CATCH_FRAME_SET(err, cfp + 1);
hook_before_rewind(ec, ec->cfp, TRUE, state, err);
rb_vm_pop_frame(ec);
goto finish_vme;
}
}
/* through */
}
else {
/* TAG_BREAK */
#if OPT_STACK_CACHING
initial = THROW_DATA_VAL(err);
#else
*ec->cfp->sp++ = THROW_DATA_VAL(err);
#endif
ec->errinfo = Qnil;
goto vm_loop_start;
}
}
}
ec->cfp->pc == 0 || ec->cfp->iseq == 0
でひたすらrb_vm_pop_frame
しているwhileループはよくわかりませんが、Rubyで定義されたブロックを実行している場合はiseqが存在しpcも動いているはずなのでとりあえず今回は無視します。
後半の分岐に関しては、TAG_RETURN
の方にひっかかりそうですが、throw時点でのec->cfp
はthrowがあるフレームになっていそうなので、cfp == escape_cfp
を満たさずスルーされそうです。
その後に進みます。
else {
ct = cfp->iseq->body->catch_table;
if (ct) for (i = 0; i < ct->size; i++) {
entry = &ct->entries[i];
if (entry->start < epc && entry->end >= epc) {
if (entry->type == CATCH_TYPE_ENSURE) {
catch_iseq = entry->iseq;
cont_pc = entry->cont;
cont_sp = entry->sp;
break;
}
}
}
}
ここでthrowしたフレームのcatch tableにensureがないか走査されます。上の方のdisasmを見る限りではcatch type: ensure
はないので、これもスルーでしょう。したがって、catch_iseq
はNULLのままのはず。
if (catch_iseq != NULL) { /* found catch table */
/* enter catch scope */
const int arg_size = 1;
rb_iseq_check(catch_iseq);
cfp->sp = vm_base_ptr(cfp) + cont_sp;
cfp->pc = cfp->iseq->body->iseq_encoded + cont_pc;
/* push block frame */
cfp->sp[0] = (VALUE)err;
vm_push_frame(ec, catch_iseq, VM_FRAME_MAGIC_RESCUE,
cfp->self,
VM_GUARDED_PREV_EP(cfp->ep),
0, /* cref or me */
catch_iseq->body->iseq_encoded,
cfp->sp + arg_size /* push value */,
catch_iseq->body->local_table_size - arg_size,
catch_iseq->body->stack_max);
state = 0;
ec->tag->state = TAG_NONE;
ec->errinfo = Qnil;
fprintf(stderr, "found catch;\n");
goto vm_loop_start;
}
else {
hook_before_rewind(ec, ec->cfp, FALSE, state, err);
if (VM_FRAME_FINISHED_P(ec->cfp)) {
rb_vm_pop_frame(ec);
ec->errinfo = (VALUE)err;
EC_TMPPOP_TAG();
EC_JUMP_TAG(ec, state);
}
else {
rb_vm_pop_frame(ec);
goto exception_handler;
}
}
static void
hook_before_rewind(rb_execution_context_t *ec, const rb_control_frame_t *cfp, int will_finish_vm_exec, int state, struct vm_throw_data *err)
{
if (state == TAG_RAISE && RBASIC_CLASS(err) == rb_eSysStackError) {
return;
}
switch (VM_FRAME_TYPE(ec->cfp)) {
case VM_FRAME_MAGIC_METHOD:
RUBY_DTRACE_METHOD_RETURN_HOOK(ec, 0, 0);
EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err));
THROW_DATA_CONSUMED_SET(err);
break;
case VM_FRAME_MAGIC_BLOCK:
if (VM_FRAME_BMETHOD_P(ec->cfp)) {
EXEC_EVENT_HOOK(ec, RUBY_EVENT_B_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err));
if (!will_finish_vm_exec) {
/* kick RUBY_EVENT_RETURN at invoke_block_from_c() for bmethod */
EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_RETURN, ec->cfp->self,
rb_vm_frame_method_entry(ec->cfp)->def->original_id,
rb_vm_frame_method_entry(ec->cfp)->called_id,
rb_vm_frame_method_entry(ec->cfp)->owner,
frame_return_value(err));
}
THROW_DATA_CONSUMED_SET(err);
}
else {
EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_B_RETURN, ec->cfp->self, 0, 0, 0, frame_return_value(err));
THROW_DATA_CONSUMED_SET(err);
}
break;
case VM_FRAME_MAGIC_CLASS:
EXEC_EVENT_HOOK_AND_POP_FRAME(ec, RUBY_EVENT_END, ec->cfp->self, 0, 0, 0, Qnil);
break;
}
}
#define EXEC_EVENT_HOOK_AND_POP_FRAME(ec_, flag_, self_, id_, called_id_, klass_, data_) \
EXEC_EVENT_HOOK_ORIG(ec_, flag_, ruby_vm_event_flags, self_, id_, called_id_, klass_, data_, 1)
#define EXEC_EVENT_HOOK_ORIG(ec_, flag_, vm_flags_, self_, id_, called_id_, klass_, data_, pop_p_) do { \
const rb_event_flag_t flag_arg_ = (flag_); \
if (UNLIKELY(vm_flags_ & (flag_arg_))) { \
/* defer evaluating the other arguments */ \
rb_exec_event_hook_orig(ec_, flag_arg_, self_, id_, called_id_, klass_, data_, pop_p_); \
} \
} while (0)
static inline void
rb_exec_event_hook_orig(rb_execution_context_t *ec, const rb_event_flag_t flag,
VALUE self, ID id, ID called_id, VALUE klass, VALUE data, int pop_p)
{
struct rb_trace_arg_struct trace_arg;
VM_ASSERT(rb_ec_vm_ptr(ec)->event_hooks.events == ruby_vm_event_flags);
VM_ASSERT(rb_ec_vm_ptr(ec)->event_hooks.events & flag);
trace_arg.event = flag;
trace_arg.ec = ec;
trace_arg.cfp = ec->cfp;
trace_arg.self = self;
trace_arg.id = id;
trace_arg.called_id = called_id;
trace_arg.klass = klass;
trace_arg.data = data;
trace_arg.path = Qundef;
trace_arg.klass_solved = 0;
rb_exec_event_hooks(&trace_arg, pop_p);
}
void
rb_exec_event_hooks(rb_trace_arg_t *trace_arg, int pop_p)
{
rb_execution_context_t *ec = trace_arg->ec;
rb_vm_t *vm = rb_ec_vm_ptr(ec);
if (trace_arg->event & RUBY_INTERNAL_EVENT_MASK) {
if (ec->trace_arg && (ec->trace_arg->event & RUBY_INTERNAL_EVENT_MASK)) {
/* skip hooks because this thread doing INTERNAL_EVENT */
}
else {
rb_trace_arg_t *prev_trace_arg = ec->trace_arg;
ec->trace_arg = trace_arg;
exec_hooks_unprotected(ec, vm, &vm->event_hooks, trace_arg);
ec->trace_arg = prev_trace_arg;
}
}
else {
if (ec->trace_arg == NULL && /* check reentrant */
trace_arg->self != rb_mRubyVMFrozenCore /* skip special methods. TODO: remove it. */) {
const VALUE errinfo = ec->errinfo;
const VALUE old_recursive = ec->local_storage_recursive_hash;
int state = 0;
ec->local_storage_recursive_hash = ec->local_storage_recursive_hash_for_trace;
ec->errinfo = Qnil;
ec->trace_arg = trace_arg;
state = exec_hooks_protected(ec, vm, &vm->event_hooks, trace_arg);
if (!state) {
ec->errinfo = errinfo;
}
ec->trace_arg = NULL;
ec->local_storage_recursive_hash_for_trace = ec->local_storage_recursive_hash;
ec->local_storage_recursive_hash = old_recursive;
if (state) {
if (pop_p) {
if (VM_FRAME_FINISHED_P(ec->cfp)) {
ec->tag = ec->tag->prev;
}
rb_vm_pop_frame(ec);
}
EC_JUMP_TAG(ec, state);
}
}
}
}
hook_before_rewind
はTracePointのフックとかを呼びつつ、ブロックの場合はframeをpopするようです。ここで多分throwしたフレームを抜ける。
その後VM_FRAME_FINISHED_P
で分岐するようですが、どういうフレームにVM_FRAME_FLAG_FINISH
が立つのか調べるのはちょっと面倒くさいですね。とりあえずinvoke_block
にはVM_FRAME_FLAG_FINISH
が立つようなのでそちらを見ます。
(ここで下書きが終わっていた)