この記事で参照するコード/実行例はRuby trunk (r61423 / Ruby 2.5.0-rc1以降 2.5.0未満)のものを使っている。
背景
Aが呼ぶBがyieldした時にそのブロックで呼ばれるyield時に使われるblock_handlerはBのcfp->ep[-1]になる気がするけど何故Aに渡されたブロックが使われるのか全然わからなかったが、まあ動いてるっぽいので帰ってから考えることにした
— asm volatile("int3"); (@k0kubun) 2017年10月5日
def a
b do
yield
end
end
def b(&block)
block.call
end
a { 1 } #=> 1
これ(yield
が)なんで動くのっていう話。yield
がマニュアル通り「メソッドに渡されたブロック」を評価するものと解釈したら自明では?と思うかもしれませんが、中途半端にVMのコードを読んだ理解だと納得できなかったのです。
(余談だけど、こういうケースでちょっと不安なのでa(&block)
と定義してb do ... end
の中でblock.call
したいんだけど、コードの可読性のためにした変更なのにrubocopにyield
にしろと怒られたのはちょっとイラッと来た)
まあ面倒なので2か月くらい放置していたのですが、今日VMをいじっていたら以下のコードが動かなくなりました。
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
先ほどのyield
の挙動の理解でいくと、bl.call
時に一旦Proc#call
のためのCの関数に入り、Cの世界からRubyのproc do ... end
のISeqを実行するためにVMが起動され、そのVMの中でyield
に到達してinvokeblock
命令により新しいcontrol frameが積まれ同じVMの中でfoo.bar
のブロックが処理される…という挙動が期待できます。(というか、何もいじらなければそうなるはず) この場合、return result
でメソッドを抜けるので、caller(0)
には到達しません。
ところが、実際にはbl.call
でVMを起動する時のISeqがfoo.bar
のあるブロックになってしまい、そこでfoo.bar
を呼び出した後普通に次の行に抜け、本来大域脱出により到達しないはずのcaller(0)
の呼び出しに到達してしまいました。これが本来内部的にどういう挙動になっているかを理解していないので、どこが間違ってるのかわからず直せない、という状態です。
この記事はこれらの内部的な挙動を理解するためのメモです。
前提知識
ブロックにブロックを渡すことはできるが、yieldはそれを参照しない
冒頭の疑問に関しては、(1)自分のRubyの挙動の理解があやふやである、(2)内部でどのように実現されているかわからない、といった問題があったが、(1)に関してはRubyのスクリプトをいくつか適当に実行してみれば理解できる。この見出しがそれによって導き出された結論である。
def a
b do |&block|
block.call
end
end
def b(&block)
block.call { 1 }
end
a #=> 1
なんとこれは動作するのである。bが呼び出すブロックは冒頭の例ではaに渡されたブロックを実行しており、なおかつこの例ではaにブロックが渡っていないにも関わらず、だ。まあでも見掛け上これは動作してほしい気はする。
一方、
def a
b do |&block|
yield
end
end
def b(&block)
block.call { 1 }
end
a
# Traceback (most recent call last):
# 3: from a.rb:11:in `<main>'
# 2: from a.rb:2:in `a'
# 1: from a.rb:8:in `b'
# a.rb:3:in `block in a': no block given (yield) (LocalJumpError)
これは動かないのだ。つまり、冒頭の例とも比較して考えると、yield
というのはそもそも現在のcontrol frameに渡されているブロックを呼び出すのではなく、そのブロックを内包しているメソッドに渡されたブロックを呼び出すようになっている。
yieldではブロックにブロックを渡せない
block.call
ではブロックに対しブロックを渡せるのは見た。ではyield
ではできるのか?
def a
b do |&block|
block.call
end
end
def b
yield { 1 }
end
a
# a.rb:8: syntax error, unexpected '{'
# yield { 1 }
# ^
# a.rb:11: syntax error, unexpected end-of-input, expecting keyword_end
コンパイルも通らない。
従って、ブロックA(#a
内のブロック)がAに渡されたブロックB(#b
内のブロック)を呼び出すケースというのは、Aの呼び出しがブロックBつきblock.call
になっており、かつAもblock.call
でブロックBを呼び出す場合しか存在しない。
「ブロックにブロックを渡すことはできるが、yieldはそれを参照しない」という前提から、ブロックAの呼び出しの際に直接渡されるブロックBのProcオブジェクトは必ず引数(ブロックパラメータ)から参照されることになり、わざわざブロックAのcontrol frameにつっこんでおく必要はそもそもないのである。
じゃあ、ブロックAのcontrol frameのblock_handlerのところには#a
に渡されたブロックを突っ込んでおく余地がありそうだが、実際どのように渡されているかを見ていく。
yieldされるISeqはどのようにinvokeblock
に渡されるか
一番簡単なケース
def a
yield
end
a { 1 } #=> 1
まあ一番簡単なのはこれですね。
== disasm: #<ISeq:<main>@a.rb:0>========================================
== catch table
| catch type: break st: 0010 ed: 0015 sp: 0000 cont: 0015
== disasm: #<ISeq:block in <main>@a.rb:5>===============================
== catch table
| catch type: redo st: 0001 ed: 0002 sp: 0000 cont: 0001
| catch type: next st: 0001 ed: 0002 sp: 0000 cont: 0002
|------------------------------------------------------------------------
0000 nop ( 5)[Bc]
0001 putobject_OP_INT2FIX_O_1_C_ [Li]
0002 leave [Br]
|------------------------------------------------------------------------
0000 putspecialobject 1 ( 1)[Li]
0002 putobject :a
0004 putiseq a
0006 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>
0009 pop
0010 putself ( 5)[Li]
0011 send <callinfo!mid:a, argc:0, FCALL>, <callcache>, block in <main>
0015 leave
== disasm: #<ISeq:a@a.rb:1>=============================================
0000 invokeblock <callinfo!argc:0, ARGS_SIMPLE> ( 2)[LiCa]
0002 leave [Re]
invokeblock
で実行されることを一応確認。
invokeblock
は block_handler
をどのように見つけるか
/**
@c method/iterator
@e yield(args)
@j yield を実行する。
*/
DEFINE_INSN
invokeblock
(CALL_INFO ci)
(...)
(VALUE val) // inc += 1 - ci->orig_argc;
{
struct rb_calling_info calling;
calling.argc = ci->orig_argc;
calling.block_handler = VM_BLOCK_HANDLER_NONE;
calling.recv = GET_SELF();
val = vm_invoke_block(ec, GET_CFP(), &calling, ci);
if (val == Qundef) {
RESTORE_REGS();
NEXT_INSN();
}
}
static VALUE
vm_invoke_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci)
{
VALUE block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);
VALUE type = GET_ISEQ()->body->local_iseq->body->type;
int is_lambda = FALSE;
if ((type != ISEQ_TYPE_METHOD && type != ISEQ_TYPE_CLASS) ||
block_handler == VM_BLOCK_HANDLER_NONE) {
rb_vm_localjump_error("no block given (yield)", Qnil, 0);
}
again:
switch (vm_block_handler_type(block_handler)) {
case block_handler_type_iseq:
{
const struct rb_captured_block *captured = VM_BH_TO_ISEQ_BLOCK(block_handler);
return vm_invoke_iseq_block(ec, reg_cfp, calling, ci, is_lambda, captured);
}
case block_handler_type_ifunc:
{
const struct rb_captured_block *captured = VM_BH_TO_IFUNC_BLOCK(block_handler);
return vm_invoke_ifunc_block(ec, reg_cfp, calling, ci, captured);
}
case block_handler_type_proc:
is_lambda = block_proc_is_lambda(VM_BH_TO_PROC(block_handler));
block_handler = vm_proc_to_block_handler(VM_BH_TO_PROC(block_handler));
goto again;
case block_handler_type_symbol:
return vm_invoke_symbol_block(ec, reg_cfp, calling, ci, VM_BH_TO_SYMBOL(block_handler));
}
VM_UNREACHABLE(vm_invoke_block: unreachable);
return Qnil;
}
/* cfp から block_handler を取り出す */
PUREFUNC(static inline VALUE VM_CF_BLOCK_HANDLER(const rb_control_frame_t * const cfp));
static inline VALUE
VM_CF_BLOCK_HANDLER(const rb_control_frame_t * const cfp)
{
const VALUE *ep = VM_CF_LEP(cfp);
return VM_ENV_BLOCK_HANDLER(ep);
}
/* cfp->ep から、一番直近の VM_ENV_FLAG_LOCAL (0x02) が立っているepを見つける */
PUREFUNC(static inline const VALUE *VM_CF_LEP(const rb_control_frame_t * const cfp));
static inline const VALUE *
VM_CF_LEP(const rb_control_frame_t * const cfp)
{
return VM_EP_LEP(cfp->ep);
}
/* ep[0] に VM_ENV_FLAG_LOCAL (0x02) が立っているepが見付かるまで ep[-1] から前のepを遡る */
PUREFUNC(static inline const VALUE *VM_EP_LEP(const VALUE *));
static inline const VALUE *
VM_EP_LEP(const VALUE *ep)
{
while (!VM_ENV_LOCAL_P(ep)) {
ep = VM_ENV_PREV_EP(ep);
}
return ep;
}
/* ep[0] に VM_ENV_FLAG_LOCAL (0x02) が立っていたら1を返す */
static inline int
VM_ENV_LOCAL_P(const VALUE *ep)
{
return VM_ENV_FLAGS(ep, VM_ENV_FLAG_LOCAL) ? 1 : 0;
}
static inline unsigned long
VM_ENV_FLAGS(const VALUE *ep, long flag)
{
VALUE flags = ep[VM_ENV_DATA_INDEX_FLAGS];
VM_ASSERT(FIXNUM_P(flags));
return flags & flag;
}
#define VM_ENV_DATA_INDEX_ME_CREF (-2) /* ep[-2] */
#define VM_ENV_DATA_INDEX_SPECVAL (-1) /* ep[-1] */
#define VM_ENV_DATA_INDEX_FLAGS ( 0) /* ep[ 0] */
#define VM_ENV_DATA_INDEX_ENV ( 1) /* ep[ 1] */
#define VM_ENV_DATA_INDEX_ENV_PROC ( 2) /* ep[ 2] */
enum {
/* env flag */
VM_ENV_FLAG_LOCAL = 0x0002,
VM_ENV_FLAG_ESCAPED = 0x0004,
VM_ENV_FLAG_WB_REQUIRED = 0x0008
};
/* ep[-1] に格納されているポインタから 0x03 のビットを取り除いて返す */
static inline const VALUE *
VM_ENV_PREV_EP(const VALUE *ep)
{
VM_ASSERT(VM_ENV_LOCAL_P(ep) == 0);
return GC_GUARDED_PTR_REF(ep[VM_ENV_DATA_INDEX_SPECVAL]);
}
#define GC_GUARDED_PTR_REF(p) VM_TAGGED_PTR_REF((p), 0x03)
#define VM_TAGGED_PTR_REF(v, mask) ((void *)((v) & ~mask))
/* ep[-1] を参照して返す */
static inline VALUE
VM_ENV_BLOCK_HANDLER(const VALUE *ep)
{
VM_ASSERT(VM_ENV_LOCAL_P(ep));
return ep[VM_ENV_DATA_INDEX_SPECVAL];
}
冒頭のツイートをした時はVM_CF_LEP
のことを覚えていたがこの記事を書き始めた時は忘れていた…
さてVALUE block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);
に行についてだが、各関数にコメントをつけた通り、以下のような手順でblock_handler
を取り出す。
-
cfp->ep
をep
とし、ep[0]
にVM_ENV_FLAG_LOCAL
(0x02) が立っているepを見つけるまで、ep = ep[-1]
を繰り返し、lepとする -
lep[-1]
をblock_handler
とする
このコードを見る限りだと、ep[-1]
(ep[VM_ENV_DATA_INDEX_SPECVAL]
)には以下の値が入っていそうに見える (予想)
-
ep[0]
にVM_ENV_FLAG_LOCAL
が立っている場合:-
block_handler
が入っている
-
-
ep[0]
にVM_ENV_FLAG_LOCAL
が立っていない場合:- 一つ前のepが入っている
どのようなフレームにVM_ENV_FLAG_LOCAL
が立つか
前に書いた記事にある程度書いてあった。
- rb_iseq_eval_main
VM_FRAME_MAGIC_EVAL | VM_FRAME_FLAG_FINISH
- rb_iseq_eval
VM_FRAME_MAGIC_TOP | VM_ENV_FLAG_LOCAL | VM_FRAME_FLAG_FINISH
- vm_call_iseq_setup_normal
VM_FRAME_MAGIC_METHOD | VM_ENV_FLAG_LOCAL
- vm_call_cfunc_with_frame
VM_FRAME_MAGIC_CFUNC | VM_FRAME_FLAG_CFRAME | VM_ENV_FLAG_LOCAL
ブロックのフレームに立つのかどうかが気になるが、とりあえずこの節の理解には不要なのでスキップ。Rubyのメソッドにはvm_call_iseq_setup_normal
が使われるのでこのフレームのepはlepということになる。
なので、vm_call_iseq_setup_normal
が呼ばれる時にブロックハンドラーっぽい奴がそのフレームのep[-1]
に積まれれば動きそう。
Rubyのメソッド呼び出しではどのようにblock_handler
が作られるか
上述したISeqに書かれているsend命令の定義を見ていく
/**
@c method/iterator
@e invoke method.
@j メソッド呼び出しを行う。ci に必要な情報が格納されている。
*/
DEFINE_INSN
send
(CALL_INFO ci, CALL_CACHE cc, ISEQ blockiseq)
(...)
(VALUE val) // inc += - (int)(ci->orig_argc + ((ci->flag & VM_CALL_ARGS_BLOCKARG) ? 1 : 0));
{
struct rb_calling_info calling;
vm_caller_setup_arg_block(ec, reg_cfp, &calling, ci, blockiseq, FALSE);
vm_search_method(ci, cc, calling.recv = TOPN(calling.argc = ci->orig_argc));
CALL_METHOD(&calling, ci, cc);
}
#define CALL_METHOD(calling, ci, cc) do { \
VALUE v = (*(cc)->call)(ec, GET_CFP(), (calling), (ci), (cc)); \
if (v == Qundef) { \
RESTORE_REGS(); \
NEXT_INSN(); \
} \
else { \
val = v; \
} \
} while (0)
/* cc->call は最初 vm_call_general になっている */
static VALUE
vm_call_general(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
return vm_call_method(ec, reg_cfp, calling, ci, cc);
}
static inline VALUE
vm_call_method(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
VM_ASSERT(callable_method_entry_p(cc->me));
if (cc->me != NULL) {
switch (METHOD_ENTRY_VISI(cc->me)) {
case METHOD_VISI_PUBLIC: /* likely */
return vm_call_method_each_type(ec, cfp, calling, ci, cc);
case METHOD_VISI_PRIVATE:
if (!(ci->flag & VM_CALL_FCALL)) {
enum method_missing_reason stat = MISSING_PRIVATE;
if (ci->flag & VM_CALL_VCALL) stat |= MISSING_VCALL;
cc->aux.method_missing_reason = stat;
CI_SET_FASTPATH(cc, vm_call_method_missing, 1);
return vm_call_method_missing(ec, cfp, calling, ci, cc);
}
return vm_call_method_each_type(ec, cfp, calling, ci, cc);
case METHOD_VISI_PROTECTED:
if (!(ci->flag & VM_CALL_OPT_SEND)) {
if (!rb_obj_is_kind_of(cfp->self, cc->me->defined_class)) {
cc->aux.method_missing_reason = MISSING_PROTECTED;
return vm_call_method_missing(ec, cfp, calling, ci, cc);
}
else {
/* caching method info to dummy cc */
struct rb_call_cache cc_entry;
cc_entry = *cc;
cc = &cc_entry;
VM_ASSERT(cc->me != NULL);
return vm_call_method_each_type(ec, cfp, calling, ci, cc);
}
}
return vm_call_method_each_type(ec, cfp, calling, ci, cc);
default:
rb_bug("unreachable");
}
}
else {
return vm_call_method_nome(ec, cfp, calling, ci, cc);
}
}
static VALUE
vm_call_method_each_type(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
switch (cc->me->def->type) {
case VM_METHOD_TYPE_ISEQ:
CI_SET_FASTPATH(cc, vm_call_iseq_setup, TRUE);
return vm_call_iseq_setup(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_NOTIMPLEMENTED:
case VM_METHOD_TYPE_CFUNC:
CI_SET_FASTPATH(cc, vm_call_cfunc, TRUE);
return vm_call_cfunc(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_ATTRSET:
CALLER_SETUP_ARG(cfp, calling, ci);
rb_check_arity(calling->argc, 1, 1);
cc->aux.index = 0;
CI_SET_FASTPATH(cc, vm_call_attrset, !((ci->flag & VM_CALL_ARGS_SPLAT) || (ci->flag & VM_CALL_KWARG)));
return vm_call_attrset(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_IVAR:
CALLER_SETUP_ARG(cfp, calling, ci);
rb_check_arity(calling->argc, 0, 0);
cc->aux.index = 0;
CI_SET_FASTPATH(cc, vm_call_ivar, !(ci->flag & VM_CALL_ARGS_SPLAT));
return vm_call_ivar(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_MISSING:
cc->aux.method_missing_reason = 0;
CI_SET_FASTPATH(cc, vm_call_method_missing, TRUE);
return vm_call_method_missing(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_BMETHOD:
CI_SET_FASTPATH(cc, vm_call_bmethod, TRUE);
return vm_call_bmethod(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_ALIAS:
cc->me = aliased_callable_method_entry(cc->me);
VM_ASSERT(cc->me != NULL);
return vm_call_method_each_type(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_OPTIMIZED:
switch (cc->me->def->body.optimize_type) {
case OPTIMIZED_METHOD_TYPE_SEND:
CI_SET_FASTPATH(cc, vm_call_opt_send, TRUE);
return vm_call_opt_send(ec, cfp, calling, ci, cc);
case OPTIMIZED_METHOD_TYPE_CALL:
CI_SET_FASTPATH(cc, vm_call_opt_call, TRUE);
return vm_call_opt_call(ec, cfp, calling, ci, cc);
default:
rb_bug("vm_call_method: unsupported optimized method type (%d)",
cc->me->def->body.optimize_type);
}
case VM_METHOD_TYPE_UNDEF:
break;
case VM_METHOD_TYPE_ZSUPER:
return vm_call_zsuper(ec, cfp, calling, ci, cc, RCLASS_ORIGIN(cc->me->owner));
case VM_METHOD_TYPE_REFINED: {
const rb_cref_t *cref = rb_vm_get_cref(cfp->ep);
VALUE refinements = cref ? CREF_REFINEMENTS(cref) : Qnil;
VALUE refinement;
const rb_callable_method_entry_t *ref_me;
refinement = find_refinement(refinements, cc->me->owner);
if (NIL_P(refinement)) {
goto no_refinement_dispatch;
}
ref_me = rb_callable_method_entry(refinement, ci->mid);
if (ref_me) {
if (cc->call == vm_call_super_method) {
const rb_control_frame_t *top_cfp = current_method_entry(ec, cfp);
const rb_callable_method_entry_t *top_me = rb_vm_frame_method_entry(top_cfp);
if (top_me && rb_method_definition_eq(ref_me->def, top_me->def)) {
goto no_refinement_dispatch;
}
}
cc->me = ref_me;
if (ref_me->def->type != VM_METHOD_TYPE_REFINED) {
return vm_call_method(ec, cfp, calling, ci, cc);
}
}
else {
cc->me = NULL;
return vm_call_method_nome(ec, cfp, calling, ci, cc);
}
no_refinement_dispatch:
if (cc->me->def->body.refined.orig_me) {
cc->me = refined_method_callable_without_refinement(cc->me);
}
else {
VALUE klass = RCLASS_SUPER(cc->me->defined_class);
cc->me = klass ? rb_callable_method_entry(klass, ci->mid) : NULL;
}
return vm_call_method(ec, cfp, calling, ci, cc);
}
}
rb_bug("vm_call_method: unsupported method type (%d)", cc->me->def->type);
}
static VALUE
vm_call_iseq_setup(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
const rb_iseq_t *iseq = def_iseq_ptr(cc->me->def);
const int param_size = iseq->body->param.size;
const int local_size = iseq->body->local_table_size;
const int opt_pc = vm_callee_setup_arg(ec, calling, ci, cc, def_iseq_ptr(cc->me->def), cfp->sp - calling->argc, param_size, local_size);
return vm_call_iseq_setup_2(ec, cfp, calling, ci, cc, opt_pc, param_size, local_size);
}
static inline VALUE
vm_call_iseq_setup_2(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc,
int opt_pc, int param_size, int local_size)
{
if (LIKELY(!(ci->flag & VM_CALL_TAILCALL))) {
return vm_call_iseq_setup_normal(ec, cfp, calling, ci, cc, opt_pc, param_size, local_size);
}
else {
return vm_call_iseq_setup_tailcall(ec, cfp, calling, ci, cc, opt_pc);
}
}
static inline VALUE
vm_call_iseq_setup_normal(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc,
int opt_pc, int param_size, int local_size)
{
const rb_callable_method_entry_t *me = cc->me;
const rb_iseq_t *iseq = def_iseq_ptr(me->def);
VALUE *argv = cfp->sp - calling->argc;
VALUE *sp = argv + param_size;
cfp->sp = argv - 1 /* recv */;
vm_push_frame(ec, iseq, VM_FRAME_MAGIC_METHOD | VM_ENV_FLAG_LOCAL, calling->recv,
calling->block_handler, (VALUE)me,
iseq->body->iseq_encoded + opt_pc, sp,
local_size - param_size,
iseq->body->stack_max);
return Qundef;
}
static inline rb_control_frame_t *
vm_push_frame(rb_execution_context_t *ec,
const rb_iseq_t *iseq,
VALUE type,
VALUE self,
VALUE specval,
VALUE cref_or_me,
const VALUE *pc,
VALUE *sp,
int local_size,
int stack_max)
{
rb_control_frame_t *const cfp = ec->cfp - 1;
int i;
vm_check_frame(type, specval, cref_or_me, iseq);
VM_ASSERT(local_size >= 0);
/* check stack overflow */
CHECK_VM_STACK_OVERFLOW0(cfp, sp, local_size + stack_max);
ec->cfp = cfp;
/* setup new frame */
cfp->pc = (VALUE *)pc;
cfp->iseq = (rb_iseq_t *)iseq;
cfp->self = self;
cfp->block_code = NULL;
/* setup vm value stack */
/* initialize local variables */
for (i=0; i < local_size; i++) {
*sp++ = Qnil;
}
/* setup ep with managing data */
VM_ASSERT(VM_ENV_DATA_INDEX_ME_CREF == -2);
VM_ASSERT(VM_ENV_DATA_INDEX_SPECVAL == -1);
VM_ASSERT(VM_ENV_DATA_INDEX_FLAGS == -0);
*sp++ = cref_or_me; /* ep[-2] / Qnil or T_IMEMO(cref) or T_IMEMO(ment) */
*sp++ = specval /* ep[-1] / block handler or prev env ptr */;
*sp = type; /* ep[-0] / ENV_FLAGS */
cfp->ep = sp;
cfp->sp = sp + 1;
#if VM_DEBUG_BP_CHECK
cfp->bp_check = sp + 1;
#endif
if (VMDEBUG == 2) {
SDR();
}
return cfp;
}
cc->call
の初期値がvm_call_general
になっていることは以前の記事で解説したので割愛。
すると、specval
という引数名の意味はよくわからないがこれが "block handler or prev env ptr" として ep[-1]
にセットされるようだ。
で、このパス(sendによるRubyのメソッドの呼び出し)だとcalling->block_handler
が渡される。
send命令でcalling->block_handler
はどのように作られるか
send命令のコードを見ると、名前的にvm_caller_setup_arg_block
が怪しい。
static void
vm_caller_setup_arg_block(const rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
struct rb_calling_info *calling, const struct rb_call_info *ci, rb_iseq_t *blockiseq, const int is_super)
{
if (ci->flag & VM_CALL_ARGS_BLOCKARG) {
VALUE block_code = *(--reg_cfp->sp);
if ((ci->flag & VM_CALL_ARGS_BLOCKARG_BLOCKPARAM) &&
!VM_ENV_FLAGS(VM_CF_LEP(reg_cfp), VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM)) {
calling->block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);
}
else if (NIL_P(block_code)) {
calling->block_handler = VM_BLOCK_HANDLER_NONE;
}
else if (SYMBOL_P(block_code) && rb_method_basic_definition_p(rb_cSymbol, idTo_proc)) {
const rb_cref_t *cref = vm_env_cref(reg_cfp->ep);
if (cref && !NIL_P(cref->refinements)) {
VALUE ref = cref->refinements;
VALUE func = rb_hash_lookup(ref, block_code);
if (NIL_P(func)) {
/* TODO: limit cached funcs */
func = rb_func_proc_new(refine_sym_proc_call, block_code);
rb_hash_aset(ref, block_code, func);
}
block_code = func;
}
calling->block_handler = block_code;
}
else {
calling->block_handler = vm_to_proc(block_code);
}
}
else if (blockiseq != NULL) { /* likely */
struct rb_captured_block *captured = VM_CFP_TO_CAPTURED_BLOCK(reg_cfp);
captured->code.iseq = blockiseq;
calling->block_handler = VM_BH_FROM_ISEQ_BLOCK(captured);
}
else {
if (is_super) {
calling->block_handler = GET_BLOCK_HANDLER();
}
else {
calling->block_handler = VM_BLOCK_HANDLER_NONE;
}
}
}
ていうかそもそもよく見るとsend命令の第三オペランドはISEQ blockiseq
である。これがrb_iseq_t *blockiseq
としてこの関数に渡ってくる。
なお、send命令のci->flag
はISeqのdisasmから確認できるようになっている。フラグは以下のものがセットされる
#define VM_CALL_ARGS_SPLAT (0x01 << VM_CALL_ARGS_SPLAT_bit)
#define VM_CALL_ARGS_BLOCKARG (0x01 << VM_CALL_ARGS_BLOCKARG_bit)
#define VM_CALL_ARGS_BLOCKARG_BLOCKPARAM (0x01 << VM_CALL_ARGS_BLOCKARG_BLOCKPARAM_bit)
#define VM_CALL_FCALL (0x01 << VM_CALL_FCALL_bit)
#define VM_CALL_VCALL (0x01 << VM_CALL_VCALL_bit)
#define VM_CALL_ARGS_SIMPLE (0x01 << VM_CALL_ARGS_SIMPLE_bit)
#define VM_CALL_BLOCKISEQ (0x01 << VM_CALL_BLOCKISEQ_bit)
#define VM_CALL_KWARG (0x01 << VM_CALL_KWARG_bit)
#define VM_CALL_KW_SPLAT (0x01 << VM_CALL_KW_SPLAT_bit)
#define VM_CALL_TAILCALL (0x01 << VM_CALL_TAILCALL_bit)
#define VM_CALL_SUPER (0x01 << VM_CALL_SUPER_bit)
#define VM_CALL_OPT_SEND (0x01 << VM_CALL_OPT_SEND_bit)
そして上の方に書いた結果を再度引用すると、
0011 send <callinfo!mid:a, argc:0, FCALL>, <callcache>, block in <main>
となっており、VM_CALL_ARGS_BLOCKARG
がない。というかこれはfoo(&block)
みたいな奴向けだった気がする。今回は放置。
というわけなので、sendの第三オペランドblockiseq
の中身がある限りはelse if (blockiseq != NULL) { /* likely */
で止まる。block in <main>
とあるとおり、これはブロックがあれば渡りそうである。確かに"likely"っぽい。
もう一度該当部分を引用する。
else if (blockiseq != NULL) { /* likely */
struct rb_captured_block *captured = VM_CFP_TO_CAPTURED_BLOCK(reg_cfp);
captured->code.iseq = blockiseq;
calling->block_handler = VM_BH_FROM_ISEQ_BLOCK(captured);
}
static struct rb_captured_block *
VM_CFP_TO_CAPTURED_BLOCK(const rb_control_frame_t *cfp)
{
VM_ASSERT(!VM_CFP_IN_HEAP_P(GET_EC(), cfp));
return (struct rb_captured_block *)&cfp->self;
}
static inline VALUE
VM_BH_FROM_ISEQ_BLOCK(const struct rb_captured_block *captured)
{
VALUE block_handler = VM_TAGGED_PTR_SET(captured, 0x01);
VM_ASSERT(VM_BH_ISEQ_BLOCK_P(block_handler));
return block_handler;
}
#define VM_TAGGED_PTR_SET(p, tag) ((VALUE)(p) | (tag))
struct rb_captured_block {
VALUE self;
const VALUE *ep;
union {
const rb_iseq_t *iseq;
const struct vm_ifunc *ifunc;
VALUE val;
} code;
};
(VALUE)&cfp->self | 0x01
がblock_handler
の実態のようです。そして((struct rb_captured_block *)&cfp->self)->code.iseq
にsend命令の第三オペランドのblockiseq
がセットされるようですね。
で、cfp->self
にはcalling->recv
がセットされる(かつ、send命令中でcalling.recv = TOPN(calling.argc = ci->orig_argc)
が入る)んだけど、それは&cfp->self
とは多分関係ない。
ここで、cfpの中身の定義を見てみる。
typedef struct rb_control_frame_struct {
const VALUE *pc; /* cfp[0] */
VALUE *sp; /* cfp[1] */
const rb_iseq_t *iseq; /* cfp[2] */
VALUE self; /* cfp[3] / block[0] */
const VALUE *ep; /* cfp[4] / block[1] */
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */
#if VM_DEBUG_BP_CHECK
VALUE *bp_check; /* cfp[6] */
#endif
} rb_control_frame_t;
cfp[3]
~cfp[5]
の構造がstruct rb_captured_block
に似ている。コメントにblock[0]
~block[2]
というのが書いてあるが、これは多分rb_captured_block
にキャストされることを意味しているのだと思う。
つまり、&cfp->self
というのは、control frameの構造体のうち後半だけを持ってくることを意味しており、これをstruct rb_captured_block *
に変換してcode.iseq
にセットするのはどういうことかというと、呼び出し側のcfpのcfp[5]
(cfp->block_code
)にセットされることを意味する。
従って、yield
するメソッドは、yield
するメソッドを呼び出したフレームの後半部分をblock_handler
経由で参照しに来るということだ。
rb_block_handler_type には何があるか
#a
を呼び出す時にsend命令でcalling->block_handler
がvm_push_frame
でspecval
としてep[-1]
にセットされ、そのcalling->block_handler
は呼び出し側のcfpの後半を差しており、後半の最後のフィールドであるcfp->block_code
にはsend命令第三オペランドのblockiseq
がセットされることまではわかった。
普通のRubyのメソッドのepはlepであり、VM_CF_BLOCK_HANDLER
は直近のlepからep->[-1]
をblock_handler
として取り出すので、block_handler
が適切に渡りそうなこともわかった。
が、以下のvm_invoke_block
のこの後の挙動を理解するには、typeを知る必要がある。
static VALUE
vm_invoke_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci)
{
VALUE block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);
VALUE type = GET_ISEQ()->body->local_iseq->body->type;
int is_lambda = FALSE;
if ((type != ISEQ_TYPE_METHOD && type != ISEQ_TYPE_CLASS) ||
block_handler == VM_BLOCK_HANDLER_NONE) {
rb_vm_localjump_error("no block given (yield)", Qnil, 0);
}
again:
switch (vm_block_handler_type(block_handler)) {
case block_handler_type_iseq:
{
const struct rb_captured_block *captured = VM_BH_TO_ISEQ_BLOCK(block_handler);
return vm_invoke_iseq_block(ec, reg_cfp, calling, ci, is_lambda, captured);
}
case block_handler_type_ifunc:
{
const struct rb_captured_block *captured = VM_BH_TO_IFUNC_BLOCK(block_handler);
return vm_invoke_ifunc_block(ec, reg_cfp, calling, ci, captured);
}
case block_handler_type_proc:
is_lambda = block_proc_is_lambda(VM_BH_TO_PROC(block_handler));
block_handler = vm_proc_to_block_handler(VM_BH_TO_PROC(block_handler));
goto again;
case block_handler_type_symbol:
return vm_invoke_symbol_block(ec, reg_cfp, calling, ci, VM_BH_TO_SYMBOL(block_handler));
}
VM_UNREACHABLE(vm_invoke_block: unreachable);
return Qnil;
}
enum rb_block_handler_type {
block_handler_type_iseq,
block_handler_type_ifunc,
block_handler_type_symbol,
block_handler_type_proc
};
static inline enum rb_block_handler_type
vm_block_handler_type(VALUE block_handler)
{
if (VM_BH_ISEQ_BLOCK_P(block_handler)) {
return block_handler_type_iseq;
}
else if (VM_BH_IFUNC_P(block_handler)) {
return block_handler_type_ifunc;
}
else if (SYMBOL_P(block_handler)) {
return block_handler_type_symbol;
}
else {
VM_ASSERT(rb_obj_is_proc(block_handler));
return block_handler_type_proc;
}
}
static inline int
VM_BH_ISEQ_BLOCK_P(VALUE block_handler)
{
if ((block_handler & 0x03) == 0x01) {
#if VM_CHECK_MODE > 0
struct rb_captured_block *captured = VM_TAGGED_PTR_REF(block_handler, 0x03);
VM_ASSERT(imemo_type_p(captured->code.val, imemo_iseq));
#endif
return 1;
}
else {
return 0;
}
}
static inline int
VM_BH_IFUNC_P(VALUE block_handler)
{
if ((block_handler & 0x03) == 0x03) {
#if VM_CHECK_MODE > 0
struct rb_captured_block *captured = (void *)(block_handler & ~0x03);
VM_ASSERT(imemo_type_p(captured->code.val, imemo_ifunc));
#endif
return 1;
}
else {
return 0;
}
}
#define SYMBOL_P(x) RB_SYMBOL_P(x)
#define RB_SYMBOL_P(x) (RB_STATIC_SYM_P(x)||RB_DYNAMIC_SYM_P(x))
#define RB_STATIC_SYM_P(x) (((VALUE)(x)&~((~(VALUE)0)<<RUBY_SPECIAL_SHIFT)) == RUBY_SYMBOL_FLAG)
#define RB_DYNAMIC_SYM_P(x) (!RB_SPECIAL_CONST_P(x) && RB_BUILTIN_TYPE(x) == (RUBY_T_SYMBOL))
VALUE
rb_obj_is_proc(VALUE proc)
{
if (rb_typeddata_is_kind_of(proc, &proc_data_type)) {
return Qtrue;
}
else {
return Qfalse;
}
}
以下のように分類できそうです。
- block_handler_type_iseq
- ポインタのタグの0x01, 0x02のうち、0x01 (
VM_BH_FROM_ISEQ_BLOCK
でセットされる) だけが立っている - 多分
struct rb_captured_block *
にキャストしてiseqを取り出せるtype
- ポインタのタグの0x01, 0x02のうち、0x01 (
- block_handler_type_ifunc
- ポインタのタグの0x01, 0x02のうち、0x01と0x02両方が立っている
-
struct rb_captured_block
の構造から察するにconst struct vm_ifunc
という奴が入っている-
struct vm_ifunc
には任意の関数ポインタと任意のデータとフラグとargcとかが入りそう
-
- block_handler_type_symbol
- Symbolオブジェクトっぽい
- block_handler_type_proc
- Procオブジェクトっぽい
ところで0x01
とか0x03
がマジックナンバーすぎて可読性が低めなので、grepしやすいようにそのうち直しておきたいですね…
block_handler_type_iseqの時にiseqが呼び出される流れ
static VALUE
vm_invoke_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci)
{
VALUE block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);
VALUE type = GET_ISEQ()->body->local_iseq->body->type;
int is_lambda = FALSE;
if ((type != ISEQ_TYPE_METHOD && type != ISEQ_TYPE_CLASS) ||
block_handler == VM_BLOCK_HANDLER_NONE) {
rb_vm_localjump_error("no block given (yield)", Qnil, 0);
}
again:
switch (vm_block_handler_type(block_handler)) {
case block_handler_type_iseq:
{
const struct rb_captured_block *captured = VM_BH_TO_ISEQ_BLOCK(block_handler);
return vm_invoke_iseq_block(ec, reg_cfp, calling, ci, is_lambda, captured);
}
case block_handler_type_ifunc:
{
const struct rb_captured_block *captured = VM_BH_TO_IFUNC_BLOCK(block_handler);
return vm_invoke_ifunc_block(ec, reg_cfp, calling, ci, captured);
}
case block_handler_type_proc:
is_lambda = block_proc_is_lambda(VM_BH_TO_PROC(block_handler));
block_handler = vm_proc_to_block_handler(VM_BH_TO_PROC(block_handler));
goto again;
case block_handler_type_symbol:
return vm_invoke_symbol_block(ec, reg_cfp, calling, ci, VM_BH_TO_SYMBOL(block_handler));
}
VM_UNREACHABLE(vm_invoke_block: unreachable);
return Qnil;
}
static inline const struct rb_captured_block *
VM_BH_TO_ISEQ_BLOCK(VALUE block_handler)
{
struct rb_captured_block *captured = VM_TAGGED_PTR_REF(block_handler, 0x03);
VM_ASSERT(VM_BH_ISEQ_BLOCK_P(block_handler));
return captured;
}
#define VM_TAGGED_PTR_REF(v, mask) ((void *)((v) & ~mask))
/* ruby iseq -> ruby block */
static VALUE
vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
struct rb_calling_info *calling, const struct rb_call_info *ci,
int is_lambda, const struct rb_captured_block *captured)
{
const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq);
const int arg_size = iseq->body->param.size;
VALUE * const rsp = GET_SP() - calling->argc;
int opt_pc = vm_callee_setup_block_arg(ec, calling, ci, iseq, rsp, is_lambda ? arg_setup_method : arg_setup_block);
SET_SP(rsp);
vm_push_frame(ec, iseq,
VM_FRAME_MAGIC_BLOCK | (is_lambda ? VM_FRAME_FLAG_LAMBDA : 0),
captured->self,
VM_GUARDED_PREV_EP(captured->ep), 0,
iseq->body->iseq_encoded + opt_pc,
rsp + arg_size,
iseq->body->local_table_size - arg_size, iseq->body->stack_max);
return Qundef;
}
はい、captured->code.iseq
が参照されて新しいcontrol frameがpushされました。めでたしめでたし。
冒頭の例
既に記事が大分長くなって疲れたが、以下のコードの挙動を追いかけることにする。
def a
b do
yield
end
end
def b(&block)
block.call
end
a { 1 } #=> 1
まず、僕の疑問の元になっている理解について整理しておくが、yield
が呼び出される瞬間以下のようなコールスタックになっていると考えている。
- main
- #a
- #b
- block in #a
- #b
- #a
で、invokeblock
がVM_CF_BLOCK_HANDLER
でblock_handler
を取り出す時、
- "block in #a"のフレームがlepではないと仮定すると
-
VM_CF_LEP
の際に直近の普通のメソッド呼び出しフレームである#b
のcfp->ep
をlepとして取り出し、そのep[-1]
(#b
に渡されたブロック)を参照してしまうのではないか
-
- "block in #a"のフレームがlepであると仮定すると
-
block.call
でブロックを渡していないので、no block givenになるのではないか
-
ということを考えている。
まあ実際の挙動から逆算して、以下のことが容易に推察できる
- 予想1: blockを実行するフレームはlepではない (
cfp->ep[0]
にVM_ENV_FLAG_LOCAL
フラグが立っていない) - 予想2: "block in #a" のフレームの
cfp->ep[-1]
(前のep)には #b のフレームを飛び越えて #a のフレームのepが入っている
これまでの調査から、mainのフレームのcfp->block_code
には{ 1 }
のblockiseq
が入っており、それを含むcfp後半を差すstruct rb_captured_block *
が #a のフレームの cfp->ep[-1]
に入っていることが期待でき、また #a のcfp->ep
はlepのはずなので、「予想1」と「予想2」が両方正しければVM_CF_BLOCK_HANDLER
は正しくstruct rb_captured_block *
をblock_handler
として取り出し、そこから{ 1 }
のiseqを参照できるはずである。
なので、これら2つの予想が正しいことを確認していく。
blockを実行するフレームはlepか
Proc#call
の場合について書く。VM_METHOD_TYPE_OPTIMIZED
かつOPTIMIZED_METHOD_TYPE_CALL
で定義されていることを念頭に置きつつ、gdbで追いかけている時に通ったパスを見ておくと、
static VALUE
vm_call_method_each_type(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
switch (cc->me->def->type) {
case VM_METHOD_TYPE_ISEQ:
CI_SET_FASTPATH(cc, vm_call_iseq_setup, TRUE);
return vm_call_iseq_setup(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_NOTIMPLEMENTED:
case VM_METHOD_TYPE_CFUNC:
CI_SET_FASTPATH(cc, vm_call_cfunc, TRUE);
return vm_call_cfunc(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_ATTRSET:
CALLER_SETUP_ARG(cfp, calling, ci);
rb_check_arity(calling->argc, 1, 1);
cc->aux.index = 0;
CI_SET_FASTPATH(cc, vm_call_attrset, !((ci->flag & VM_CALL_ARGS_SPLAT) || (ci->flag & VM_CALL_KWARG)));
return vm_call_attrset(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_IVAR:
CALLER_SETUP_ARG(cfp, calling, ci);
rb_check_arity(calling->argc, 0, 0);
cc->aux.index = 0;
CI_SET_FASTPATH(cc, vm_call_ivar, !(ci->flag & VM_CALL_ARGS_SPLAT));
return vm_call_ivar(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_MISSING:
cc->aux.method_missing_reason = 0;
CI_SET_FASTPATH(cc, vm_call_method_missing, TRUE);
return vm_call_method_missing(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_BMETHOD:
CI_SET_FASTPATH(cc, vm_call_bmethod, TRUE);
return vm_call_bmethod(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_ALIAS:
cc->me = aliased_callable_method_entry(cc->me);
VM_ASSERT(cc->me != NULL);
return vm_call_method_each_type(ec, cfp, calling, ci, cc);
case VM_METHOD_TYPE_OPTIMIZED:
switch (cc->me->def->body.optimize_type) {
case OPTIMIZED_METHOD_TYPE_SEND:
CI_SET_FASTPATH(cc, vm_call_opt_send, TRUE);
return vm_call_opt_send(ec, cfp, calling, ci, cc);
case OPTIMIZED_METHOD_TYPE_CALL:
CI_SET_FASTPATH(cc, vm_call_opt_call, TRUE);
return vm_call_opt_call(ec, cfp, calling, ci, cc);
default:
rb_bug("vm_call_method: unsupported optimized method type (%d)",
cc->me->def->body.optimize_type);
}
case VM_METHOD_TYPE_UNDEF:
break;
case VM_METHOD_TYPE_ZSUPER:
return vm_call_zsuper(ec, cfp, calling, ci, cc, RCLASS_ORIGIN(cc->me->owner));
case VM_METHOD_TYPE_REFINED: {
const rb_cref_t *cref = rb_vm_get_cref(cfp->ep);
VALUE refinements = cref ? CREF_REFINEMENTS(cref) : Qnil;
VALUE refinement;
const rb_callable_method_entry_t *ref_me;
refinement = find_refinement(refinements, cc->me->owner);
if (NIL_P(refinement)) {
goto no_refinement_dispatch;
}
ref_me = rb_callable_method_entry(refinement, ci->mid);
if (ref_me) {
if (cc->call == vm_call_super_method) {
const rb_control_frame_t *top_cfp = current_method_entry(ec, cfp);
const rb_callable_method_entry_t *top_me = rb_vm_frame_method_entry(top_cfp);
if (top_me && rb_method_definition_eq(ref_me->def, top_me->def)) {
goto no_refinement_dispatch;
}
}
cc->me = ref_me;
if (ref_me->def->type != VM_METHOD_TYPE_REFINED) {
return vm_call_method(ec, cfp, calling, ci, cc);
}
}
else {
cc->me = NULL;
return vm_call_method_nome(ec, cfp, calling, ci, cc);
}
no_refinement_dispatch:
if (cc->me->def->body.refined.orig_me) {
cc->me = refined_method_callable_without_refinement(cc->me);
}
else {
VALUE klass = RCLASS_SUPER(cc->me->defined_class);
cc->me = klass ? rb_callable_method_entry(klass, ci->mid) : NULL;
}
return vm_call_method(ec, cfp, calling, ci, cc);
}
}
rb_bug("vm_call_method: unsupported method type (%d)", cc->me->def->type);
}
static VALUE
vm_call_opt_call(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
rb_proc_t *proc;
int argc;
VALUE *argv;
CALLER_SETUP_ARG(cfp, calling, ci);
argc = calling->argc;
argv = ALLOCA_N(VALUE, argc);
GetProcPtr(calling->recv, proc);
MEMCPY(argv, cfp->sp - argc, VALUE, argc);
cfp->sp -= argc + 1;
return rb_vm_invoke_proc(ec, proc, argc, argv, calling->block_handler);
}
VALUE
rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc,
int argc, const VALUE *argv, VALUE passed_block_handler)
{
VALUE self = vm_block_self(&proc->block);
vm_block_handler_verify(passed_block_handler);
if (proc->is_from_method) {
return vm_invoke_bmethod(ec, proc, self, argc, argv, passed_block_handler);
}
else {
return vm_invoke_proc(ec, proc, self, argc, argv, passed_block_handler);
}
}
static VALUE
vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self,
int argc, const VALUE *argv, VALUE passed_block_handler)
{
VALUE val = Qundef;
enum ruby_tag_type state;
volatile int stored_safe = ec->safe_level;
EC_PUSH_TAG(ec);
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
ec->safe_level = proc->safe_level;
val = invoke_block_from_c_proc(ec, proc, self, argc, argv, passed_block_handler, proc->is_lambda);
}
EC_POP_TAG();
ec->safe_level = stored_safe;
if (state) {
EC_JUMP_TAG(ec, state);
}
return val;
}
static inline VALUE
invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc,
VALUE self, int argc, const VALUE *argv,
VALUE passed_block_handler, int is_lambda)
{
const struct rb_block *block = &proc->block;
again:
switch (vm_block_type(block)) {
case block_type_iseq:
return invoke_iseq_block_from_c(ec, &block->as.captured, self, argc, argv, passed_block_handler, NULL, is_lambda);
case block_type_ifunc:
return vm_yield_with_cfunc(ec, &block->as.captured, self, argc, argv, passed_block_handler);
case block_type_symbol:
return vm_yield_with_symbol(ec, block->as.symbol, argc, argv, passed_block_handler);
case block_type_proc:
is_lambda = block_proc_is_lambda(block->as.proc);
block = vm_proc_block(block->as.proc);
goto again;
}
VM_UNREACHABLE(invoke_block_from_c_proc);
return Qundef;
}
static inline VALUE
invoke_iseq_block_from_c(rb_execution_context_t *ec, const struct rb_captured_block *captured,
VALUE self, int argc, const VALUE *argv, VALUE passed_block_handler,
const rb_cref_t *cref, int is_lambda)
{
const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq);
int i, opt_pc;
VALUE type = VM_FRAME_MAGIC_BLOCK | (is_lambda ? VM_FRAME_FLAG_LAMBDA : 0);
rb_control_frame_t *cfp = ec->cfp;
VALUE *sp = cfp->sp;
const rb_callable_method_entry_t *me = ec->passed_bmethod_me;
ec->passed_bmethod_me = NULL;
stack_check(ec);
CHECK_VM_STACK_OVERFLOW(cfp, argc);
cfp->sp = sp + argc;
for (i=0; i<argc; i++) {
sp[i] = argv[i];
}
opt_pc = vm_yield_setup_args(ec, iseq, argc, sp, passed_block_handler,
(is_lambda ? arg_setup_method : arg_setup_block));
cfp->sp = sp;
if (me == NULL) {
return invoke_block(ec, iseq, self, captured, cref, type, opt_pc);
}
else {
return invoke_bmethod(ec, iseq, self, captured, me, type, opt_pc);
}
}
/* C -> Ruby: block */
static inline VALUE
invoke_block(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, const struct rb_captured_block *captured, const rb_cref_t *cref, VALUE type, int opt_pc)
{
int arg_size = iseq->body->param.size;
vm_push_frame(ec, iseq, type | VM_FRAME_FLAG_FINISH, self,
VM_GUARDED_PREV_EP(captured->ep),
(VALUE)cref, /* cref or method */
iseq->body->iseq_encoded + opt_pc,
ec->cfp->sp + arg_size,
iseq->body->local_table_size - arg_size,
iseq->body->stack_max);
return vm_exec(ec);
}
typeはVM_FRAME_MAGIC_BLOCK | (is_lambda ? VM_FRAME_FLAG_LAMBDA : 0) | VM_FRAME_FLAG_FINISH
っぽい。VM_ENV_FLAG_LOCAL
がないのでこれはlepではなさそう。
blockのフレームのcfp->ep[-1]
にはどのフレームのep
が入るか
vm_push_frame
第五引数specval
が入るわけだけど、↑のコードを見るとVM_GUARDED_PREV_EP(captured->ep)
という奴が渡されるように見える。
#define VM_GUARDED_PREV_EP(ep) GC_GUARDED_PTR(ep)
#define GC_GUARDED_PTR(p) VM_TAGGED_PTR_SET((p), 0x01)
#define VM_TAGGED_PTR_SET(p, tag) ((VALUE)(p) | (tag))
VM_GUARDED_PREV_EP
はcaptured->ep
に0x01
のタグ打ってるだけだった。
captured
はinvoke_block_from_c_proc
内で&block->as.captured
として作られている模様。そのblock
は&proc->block
で、そのproc
(rb_proc_t
)はvm_call_opt_call
内でGetProcPtr(calling->recv, proc);
で作られてそう。
#define GetProcPtr(obj, ptr) \
GetCoreDataFromValue((obj), rb_proc_t, (ptr))
#define GetCoreDataFromValue(obj, type, ptr) ((ptr) = CoreDataFromValue((obj), type))
#define CoreDataFromValue(obj, type) (type*)DATA_PTR(obj)
#define DATA_PTR(dta) (RDATA(dta)->data)
#define RDATA(obj) (R_CAST(RData)(obj))
#define R_CAST(st) (struct st*)
struct RData {
struct RBasic basic;
void (*dmark)(void*);
void (*dfree)(void*);
void *data;
};
つまりGetProcPtr(calling->recv, proc)
というのはproc = (rb_proc_t*)(((struct RData*)(calling->recv))->data)
なんだけど、このcalling
はblock.call
時のものなのでProc
オブジェクトがおそらく入っている。
でまあ例のコードがブロックパラメータを取っているので、ブロックパラメータのProcオブジェクトがどう作られるかを見ておく。
ブロックパラメータのProcオブジェクトはどう作られるか
今更だが今回のコードのISeqをダンプしておく
== disasm: #<ISeq:<main>@a.rb:0>========================================
== catch table
| catch type: break st: 0020 ed: 0025 sp: 0000 cont: 0025
== disasm: #<ISeq:block in <main>@a.rb:11>==============================
== catch table
| catch type: redo st: 0001 ed: 0002 sp: 0000 cont: 0001
| catch type: next st: 0001 ed: 0002 sp: 0000 cont: 0002
|------------------------------------------------------------------------
0000 nop ( 11)[Bc]
0001 putobject_OP_INT2FIX_O_1_C_ [Li]
0002 leave [Br]
|------------------------------------------------------------------------
0000 putspecialobject 1 ( 1)[Li]
0002 putobject :a
0004 putiseq a
0006 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>
0009 pop
0010 putspecialobject 1 ( 7)[Li]
0012 putobject :b
0014 putiseq b
0016 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>
0019 pop
0020 putself ( 11)[Li]
0021 send <callinfo!mid:a, argc:0, FCALL>, <callcache>, block in <main>
0025 leave
== disasm: #<ISeq:a@a.rb:1>=============================================
== catch table
| catch type: break st: 0000 ed: 0005 sp: 0000 cont: 0005
== disasm: #<ISeq:block in a@a.rb:2>====================================
== catch table
| catch type: redo st: 0001 ed: 0003 sp: 0000 cont: 0001
| catch type: next st: 0001 ed: 0003 sp: 0000 cont: 0003
|------------------------------------------------------------------------
0000 nop ( 2)[Bc]
0001 invokeblock <callinfo!argc:0, ARGS_SIMPLE> ( 3)[Li]
0003 leave [Br]
|------------------------------------------------------------------------
0000 putself ( 2)[LiCa]
0001 send <callinfo!mid:b, argc:0, FCALL>, <callcache>, block in a
0005 leave [Re]
== disasm: #<ISeq:b@a.rb:7>=============================================
local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: 0, kw: -1@-1, kwrest: -1])
[ 1] block<Block>
0000 getblockparam block, 0 ( 8)[LiCa]
0003 opt_send_without_block <callinfo!mid:call, argc:0, ARGS_SIMPLE>, <callcache>
0006 leave [Re]
getblockparamが入ったのはこのコミット: https://github.com/ruby/ruby/commit/5ee9513a7104078d9d2f51aecc354ae67f1ba002
VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM
というのはそのフレームでgetblockparam
やsetblockparam
が一度でも実行されるとセットされる奴なので、最初はrb_vm_bh_to_procval(th, VM_ENV_BLOCK_HANDLER(ep))
がブロックパラメータ取得に使われることがわかる。
0000 getblockparam block, 0
の通りlevelが0なので、VM_ENV_BLOCK_HANDLER(ep)
のep
は #b のフレームのepということになる。
static inline VALUE
VM_ENV_BLOCK_HANDLER(const VALUE *ep)
{
VM_ASSERT(VM_ENV_LOCAL_P(ep));
return ep[VM_ENV_DATA_INDEX_SPECVAL];
}
#define VM_ENV_DATA_INDEX_SPECVAL (-1) /* ep[-1] */
VALUE
rb_vm_bh_to_procval(const rb_execution_context_t *ec, VALUE block_handler)
{
if (block_handler == VM_BLOCK_HANDLER_NONE) {
return Qnil;
}
else {
switch (vm_block_handler_type(block_handler)) {
case block_handler_type_iseq:
case block_handler_type_ifunc:
return rb_vm_make_proc(ec, VM_BH_TO_CAPT_BLOCK(block_handler), rb_cProc);
case block_handler_type_symbol:
return rb_sym_to_proc(VM_BH_TO_SYMBOL(block_handler));
case block_handler_type_proc:
return VM_BH_TO_PROC(block_handler);
default:
VM_UNREACHABLE(rb_vm_bh_to_procval);
}
}
}
static inline const struct rb_captured_block *
VM_BH_TO_CAPT_BLOCK(VALUE block_handler)
{
struct rb_captured_block *captured = VM_TAGGED_PTR_REF(block_handler, 0x03);
VM_ASSERT(VM_BH_IFUNC_P(block_handler) || VM_BH_ISEQ_BLOCK_P(block_handler));
return captured;
}
static inline VALUE
rb_vm_make_proc(const rb_execution_context_t *ec, const struct rb_captured_block *captured, VALUE klass)
{
return rb_vm_make_proc_lambda(ec, captured, klass, 0);
}
VALUE
rb_vm_make_proc_lambda(const rb_execution_context_t *ec, const struct rb_captured_block *captured, VALUE klass, int8_t is_lambda)
{
VALUE procval;
if (!VM_ENV_ESCAPED_P(captured->ep)) {
rb_control_frame_t *cfp = VM_CAPTURED_BLOCK_TO_CFP(captured);
vm_make_env_object(ec, cfp);
}
VM_ASSERT(VM_EP_IN_HEAP_P(ec, captured->ep));
VM_ASSERT(imemo_type_p(captured->code.val, imemo_iseq) ||
imemo_type_p(captured->code.val, imemo_ifunc));
procval = vm_proc_create_from_captured(klass, captured,
imemo_type(captured->code.val) == imemo_iseq ? block_type_iseq : block_type_ifunc,
(int8_t)ec->safe_level, FALSE, is_lambda);
return procval;
}
static VALUE
vm_proc_create_from_captured(VALUE klass,
const struct rb_captured_block *captured,
enum rb_block_type block_type,
int8_t safe_level, int8_t is_from_method, int8_t is_lambda)
{
VALUE procval = rb_proc_alloc(klass);
rb_proc_t *proc = RTYPEDDATA_DATA(procval);
VM_ASSERT(VM_EP_IN_HEAP_P(GET_EC(), captured->ep));
/* copy block */
RB_OBJ_WRITE(procval, &proc->block.as.captured.self, captured->self);
RB_OBJ_WRITE(procval, &proc->block.as.captured.code.val, captured->code.val);
rb_vm_block_ep_update(procval, &proc->block, captured->ep);
vm_block_type_set(&proc->block, block_type);
proc->safe_level = safe_level;
proc->is_from_method = is_from_method;
proc->is_lambda = is_lambda;
return procval;
}
void
rb_vm_block_ep_update(VALUE obj, const struct rb_block *dst, const VALUE *ep)
{
*((const VALUE **)&dst->as.captured.ep) = ep;
RB_OBJ_WRITTEN(obj, Qundef, VM_ENV_ENVVAL(ep));
}
rb_proc_alloc
でProcオブジェクトが作られる。struct RData
として見た時のdataフィールドはvm_proc_create_from_captured
内のrb_proc_t *proc
になるだろう。
rb_vm_block_ep_update
内でcaptured->ep
を&proc->block.as.captured.ep
にセットしているから、blockのフレームのcfp->ep[-1]
にはこのcaptured->ep
が入るはずである。
で、rb_vm_block_ep_update
内でのcaptured->ep
が何かについて考える。captured
はVM_BH_TO_CAPT_BLOCK
内でVM_TAGGED_PTR_REF(block_handler, 0x03)
によってblock_handler
から取り出されたものであるから、これはVM_ENV_BLOCK_HANDLER
内で #b のフレームのepからep[-1]
として作られたものである。ここにはこれまでの調査から #a のcfpの後半を指すstruct rb_captured_block *
が入っている(かつそのcode.iseq
にblockiseq
が入っている)はずで、つまりそこからcaptured->ep
を参照すると #a のフレームのepが入っていることになる。
まとめると、挙動はgetblockparamのlevel次第ではあるが、少なくともlevel=0の場合は、ブロックパラメータを引数に持つフレームのepからep[-1]
を参照したcaptured
のcaptured->ep
がブロックパラメータから作られるProcオブジェクトのdata->block.as.captured.ep
となり、かつそれは #a のフレームのepになっているので、block in #aのフレームのep[-1]
には #a のフレームのepがセットされるということだ。
なので、block in #a のフレームのcfpからVM_CF_LEP(cfp)
をやると #a のフレームのepが取得され、そのep[-1]
から #a に渡されたブロックのiseqが参照できる struct rb_captured_block *
が参照できるというわけである。
その他
長くなったので一旦この記事を保存するが、後で以下の内容を追記するかもしれない
- ブロックのISeqがどのように
Proc#call
で実行されるかの詳細 -
proc do ... end
を呼び出した時の環境や元のepはどこに保存されるか