この章ではRubyスタック七本のうち最後の大物、BLOCKが登場する。
これが終われば評価器の内部状態についてはわかったも同然だ。
イテレータの仕組みはいったいどうなっているのか。 まず次のような小さいプログラムで考えてみよう。
▼ソースプログラム
iter_method() do 9 # ブロックを探す目印 end
用語を確認しておく。このプログラムで言うとiter_methodが
イテレータメソッド、do〜endがイテレータブロックだ。
このプログラムの構文木をダンプしたらこうなった。
▼対応する構文木
NODE_ITER
nd_iter:
NODE_FCALL
nd_mid = 9617 (iter_method)
nd_args = (null)
nd_var = (null)
nd_body:
NODE_LIT
nd_lit = 9:Fixnum
イテレータブロックに書いた9を手書かりにブロックを探してみると、
NODE_ITERがイテレータブロックを表しているようだ、とわかる。それと
iter_methodを呼び出すNODE_FCALLがそのNODE_ITERの「下」にある。つ
まりイテレータメソッドの呼び出しよりもイテレータブロックのノードのほう
が先にある。ということは、ブロックはイテレータメソッドを呼び出す前に、
別のノードで積まれるらしい。
また、デバッガでコードの流れを追って確かめてみると、イテレータの起動は
このNODE_ITER NODE_CALLにNODE_YIELDを加えた三段階に分かれていることが
わかった。それは即ち
NODE_ITER)NODE_CALL)yield(NODE_YIELD)である。
ではまず第一段階のブロックを積むノード、
NODE_ITERから見ていくことにしよう。
▼rb_eval()−NODE_ITER(簡約版)
case NODE_ITER:
{
iter_retry:
PUSH_TAG(PROT_FUNC);
PUSH_BLOCK(node->nd_var, node->nd_body);
state = EXEC_TAG();
if (state == 0) {
PUSH_ITER(ITER_PRE);
result = rb_eval(self, node->nd_iter);
POP_ITER();
}
else if (_block.tag->dst == state) {
state &= TAG_MASK;
if (state == TAG_RETURN || state == TAG_BREAK) {
result = prot_tag->retval;
}
}
POP_BLOCK();
POP_TAG();
switch (state) {
case 0:
break;
case TAG_RETRY:
goto iter_retry;
case TAG_BREAK:
break;
case TAG_RETURN:
return_value(result);
/* fall through */
default:
JUMP_TAG(state);
}
}
break;
元のコードにはfor文のサポートが入っていたのでそれを削除してある。タグ
関係を除くと、ITERとBLOCKのプッシュ・ポップだけだ。あとはNODE_FCALLを
普通にrb_eval()しているだけなのだから、このITERとBLOCKがメソッドを
イテレータにするための必要条件である。
BLOCKのプッシュが必要になるのはまあいいとして、ITERは何のためにあるの
だろうか。実はITERの意味を考えるにはBLOCKを使うほうの身になって考
えてみる必要がある。
例えば今まさにメソッドが呼び出されたとしよう。そしてruby_blockが存在し
た。しかしBLOCKはメソッド呼び出しの区切りと関係なく積まれるので、ブロッ
クが存在するからと言ってそれが自分のために積まれたブロックであるかどう
かはわからない。もしかすると自分の前のメソッドのために積まれたブロック
かもしれないではないか(図1)。

図1: FRAMEとBLOCKは一対一対応ではない
そこでブロックがどのメソッドのために積まれたのか判別するために
ITERを使うわけだ。なぜBLOCKをFRAMEごとに積まないかと言うと、
BLOCKを積むのはちょっと重いからである。どのくらい重いかは、
実際に見て確かめてみよう。
PUSH_BLOCK()
PUSH_BLOCK()の引数はブロックパラメータ(の構文木)とブロック
本体である。
▼PUSH_BLOCK() POP_BLOCK()
592 #define PUSH_BLOCK(v,b) do { \
593 struct BLOCK _block; \
594 _block.tag = new_blktag(); \
595 _block.var = v; \
596 _block.body = b; \
597 _block.self = self; \
598 _block.frame = *ruby_frame; \
599 _block.klass = ruby_class; \
600 _block.frame.node = ruby_current_node;\
601 _block.scope = ruby_scope; \
602 _block.prev = ruby_block; \
603 _block.iter = ruby_iter->iter; \
604 _block.vmode = scope_vmode; \
605 _block.flags = BLOCK_D_SCOPE; \
606 _block.dyna_vars = ruby_dyna_vars; \
607 _block.wrapper = ruby_wrapper; \
608 ruby_block = &_block
610 #define POP_BLOCK() \
611 if (_block.tag->flags & (BLOCK_DYNAMIC)) \
612 _block.tag->flags |= BLOCK_ORPHAN; \
613 else if (!(_block.scope->flags & SCOPE_DONT_RECYCLE)) \
614 rb_gc_force_recycle((VALUE)_block.tag); \
615 ruby_block = _block.prev; \
616 } while (0)
(eval.c)
確認すると、BLOCKとは「作成した時点での環境のスナップショット」だ。
その証拠にCREFとBLOCK以外のスタックフレーム六本が保存されている。
CREFはruby_frame->cbaseで代替できるので積む必要がない。
またプッシュの仕組みについては三点確認したい。BLOCKもスタック上にベ
タ置き確保されていること。BLOCKにはこの時点でのFRAMEがまるごとコピー
されていること。BLOCKは他の多くのスタックフレーム構造体と違って前の
BLOCKへのポインタ(prev)を持つこと。
POP_BLOCK()でいろいろ使われているフラグは後でProcの実装を見てから
まとめて見ないとわからないので、今は説明しない。
さてBLOCKは重い、という話だが、確かに少し重そうだ。
new_blktag()は
中を見るとmalloc()しているし、大量にメンバを格納する。ただ最終的な判
断をするのはPUSH_ITER()も見比べてからにしよう。
PUSH_ITER()▼PUSH_ITER() POP_ITER()
773 #define PUSH_ITER(i) do { \
774 struct iter _iter; \
775 _iter.prev = ruby_iter; \
776 _iter.iter = (i); \
777 ruby_iter = &_iter
779 #define POP_ITER() \
780 ruby_iter = _iter.prev; \
781 } while (0)
(eval.c)
こちらは見るからに軽そうだ。使うのはスタック領域だけだし、メンバも二つ
しかない。これならFRAMEごとに積んでもたいしたことはなさそうである。
ブロックを積んだら次はイテレータ(である)メソッドを呼ぶことになる。そ
こでもちょっとした仕掛けが必要だ。rb_call0()の冒頭にruby_iterの
値を変化させるコードがあったのを覚えているだろうか。ここだ。
▼rb_call0()−ITER_CURに遷移
4498 switch (ruby_iter->iter) {
4499 case ITER_PRE:
4500 itr = ITER_CUR;
4501 break;
4502 case ITER_CUR:
4503 default:
4504 itr = ITER_NOT;
4505 break;
4506 }
(eval.c)
先程NODE_ITERでITER_PREを積んだので、このコードでruby_iterが
ITER_CURになる。これで初めてメソッドはイテレータに「なる」わけだ。
またスタックの状態を図示すると図2のようになっている。

図2: イテレータ呼び出し時のRubyスタックの様子
ruby_iterの値が真偽(自分の/自分のではない)の二つではなく三段階に分
かれているのは、ブロックを積んでからイテレータメソッドが起動するまでに
少し隙間があるからだ。例えばイテレータメソッドの引数の評価がはさまった
りする。その中にはメソッドの呼び出しが入っていることもあるはずなので、
その評価中に今積んだブロックを自分のものと勘違いされて使われてしまう可
能性がある。だからイテレータになる……ITER_CURにするのは、起動が完了す
る寸前のrb_call0()の中でなければならない。
▼処理される順番
method(arg) { block } # ブロックを積む method(arg) { block } # 引数の評価 method(arg) { block } # メソッド呼び出し
例えば前章『メソッド』でNODE_CALLのハンドラにBEGIN_CALLARGSとい
うマクロがあった。これがまさに三段階ITERを活用しているところである。
ちょっと戻って見てみよう。
BEGIN_CALLARGS END_CALLARGS▼BEGIN_CALLARGS END_CALLARGS
1812 #define BEGIN_CALLARGS do {\
1813 struct BLOCK *tmp_block = ruby_block;\
1814 if (ruby_iter->iter == ITER_PRE) {\
1815 ruby_block = ruby_block->prev;\
1816 }\
1817 PUSH_ITER(ITER_NOT)
1819 #define END_CALLARGS \
1820 ruby_block = tmp_block;\
1821 POP_ITER();\
1822 } while (0)
(eval.c)
ruby_iterがITER_PREのときはruby_blockを一つどけるようになっている。
このコードが活躍するのは例えば以下のような場合だ。
obj.m1 { nil }.m2 { nil }
この式の評価順は
m2のブロックをプッシュm1のブロックをプッシュm1呼び出しm2呼び出し
となる。だからBEGIN_CALLARGSがないとm1がm2のブロックを呼び出して
しまう。
また、もう一つイテレータがつながったとしてもその場合は
BEGIN_CALLARGSの数も一緒に増えるから問題ない。
イテレータ起動の第三段階、つまり最後の段階はブロックの起動である。
▼rb_eval()−NODE_YIELD
2579 case NODE_YIELD:
2580 if (node->nd_stts) {
2581 result = avalue_to_yvalue(rb_eval(self, node->nd_stts));
2582 }
2583 else {
2584 result = Qundef; /* no arg */
2585 }
2586 SET_CURRENT_SOURCE();
2587 result = rb_yield_0(result, 0, 0, 0);
2588 break;
(eval.c)
nd_sttsがyieldの引数である。avalue_to_yvalue()は多重代入のところ
でちょっと触れただけだが、無視しておいて問題ない。動作の核心はそんなも
のではなくrb_yield_0()だ。この関数もまた長いので、思いきり簡略化して
載せる。方法は今まで使ってきたものばかりだ。
trace_func関係を削るmassign()と同じく引数pcallがある。pcall=0と仮定して定数疊み込みをかける
またさらに今回は以下の「読みやすさ最適化オプション」もオンにした。
ここまでやるとかなり短くなる。
▼rb_yield_0()(簡約版)
static VALUE
rb_yield_0(val, self, klass, /* pcall=0 */)
VALUE val, self, klass;
{
volatile VALUE result = Qnil;
volatile VALUE old_cref;
volatile VALUE old_wrapper;
struct BLOCK * volatile block;
struct SCOPE * volatile old_scope;
struct FRAME frame;
int state;
PUSH_VARS();
PUSH_CLASS();
block = ruby_block;
frame = block->frame;
frame.prev = ruby_frame;
ruby_frame = &(frame);
old_cref = (VALUE)ruby_cref;
ruby_cref = (NODE*)ruby_frame->cbase;
old_wrapper = ruby_wrapper;
ruby_wrapper = block->wrapper;
old_scope = ruby_scope;
ruby_scope = block->scope;
ruby_block = block->prev;
ruby_dyna_vars = new_dvar(0, 0, block->dyna_vars);
ruby_class = block->klass;
self = block->self;
/* ブロック引数をセット */
massign(self, block->var, val, pcall);
PUSH_ITER(block->iter);
/* ブロック本体を実行 */
result = rb_eval(self, block->body);
POP_ITER();
POP_CLASS();
/* ……ruby_dyna_varsを回収する…… */
POP_VARS();
ruby_block = block;
ruby_frame = ruby_frame->prev;
ruby_cref = (NODE*)old_cref;
ruby_wrapper = old_wrapper;
ruby_scope = old_scope;
return result;
}
見ての通り、ほとんどのスタックフレームをruby_blockに記憶していたものと
すりかえている。単純な退避・復帰をしているものはいいとして、その他の
注意すべきフレームの扱いを見ていこう。
FRAMEstruct FRAME frame; frame = block->frame; /* 構造体まるごとコピー */ frame.prev = ruby_frame; /* この二行で…… */ ruby_frame = &(frame); /* ……frameがプッシュされる */
他のフレームと違い、FRAMEは記憶しているものそのままではなく新しい
FRAMEを複製して作るようだ。つまり図3のようになる。

図3: コピーしたフレームを積む
ここまでのコードを見てくると、FRAMEは「再利用」されることは
まずないようだ。FRAMEを積むときはいつでも新しいFRAMEを作っている。
BLOCK
block = ruby_block;
:
ruby_block = block->prev;
:
ruby_block = block;
一番わけのわからないのがBLOCKのこの動作である。退避しているんだかポッ
プしているんだかよくわからない。第一文と第三文が対になっていて最終的に
は元に戻る、というのは理解できるが、第二文はいったいどういう結果につな
がるのだろう。
いろいろ考えた結論を一言で言うと、「ブロックを積んだ時のruby_blockに戻
る」である。イテレータとはようするに以前のフレームに戻る構文だ
から、スタックフレームの状態をブロックを作った時点に戻せばいいわけだ。
そしてブロックを作ったときのruby_blockの値は、block->prevであったに
違いない。だからprevに入っているのだ。
また「常にruby_block先頭の一つを起動すると仮定してしまっていいのだろう
か」という疑問に対しては、「rb_yield_0()側としてはそう仮定してよい」と
言うしかない。起動すべきブロックをruby_blockの一番上に積んでおくのはブ
ロックを準備する側の仕事であって、rb_yield_0()の仕事ではないからだ。
その一例が前章でやったBEGIN_CALLARGSである。イテレータ呼び出しがカスケー
ドするとブロックが二段積まれて、使うべきでないブロックがスタックの先頭
に来てしまう。だからわざわざチェックを入れて横にどけているのだった。
VARS
そういえばまだPUSH_VARS()とPOP_VARS()の中身は見ていなかったような
気がする。それもここで見ておこう。
▼PUSH_VARS() POP_VARS()
619 #define PUSH_VARS() do { \
620 struct RVarmap * volatile _old; \
621 _old = ruby_dyna_vars; \
622 ruby_dyna_vars = 0
624 #define POP_VARS() \
625 if (_old && (ruby_scope->flags & SCOPE_DONT_RECYCLE)) { \
626 if (RBASIC(_old)->flags) /* 再利用されていないなら */ \
627 FL_SET(_old, DVAR_DONT_RECYCLE); \
628 } \
629 ruby_dyna_vars = _old; \
630 } while (0)
(eval.c)
これも新しい構造体を積むわけではないので「退避・復帰」と言うほうが近い。
実際にrb_yield_0()ではPUSH_VARS()は値を退避するために使われているだけ
である。実際にruby_dyna_varsを準備しているのはこの行だ。
ruby_dyna_vars = new_dvar(0, 0, block->dyna_vars);
BLOCKに記憶しておいたdyna_varsを取り出してセットする。ついでにエントリ
を一つ付けておく。第二部でやったruby_dyna_varsの構造を思い出してほしい
のだが、ここで生成しているようなidが0のRVarmapはブロックスコープの区切
りとして使われるのだった。
ただ実はパーサと評価器ではruby_dyna_varsに格納されるリンクの形が微妙に
違う。現在のブロックでブロックローカル変数の代入を行う関数
dvar_asgn_curr()を見てみよう。
▼dvar_asgn_curr()
737 static inline void
738 dvar_asgn_curr(id, value)
739 ID id;
740 VALUE value;
741 {
742 dvar_asgn_internal(id, value, 1);
743 }
699 static void
700 dvar_asgn_internal(id, value, curr)
701 ID id;
702 VALUE value;
703 int curr;
704 {
705 int n = 0;
706 struct RVarmap *vars = ruby_dyna_vars;
707
708 while (vars) {
709 if (curr && vars->id == 0) {
710 /* first null is a dvar header */
711 n++;
712 if (n == 2) break;
713 }
714 if (vars->id == id) {
715 vars->val = value;
716 return;
717 }
718 vars = vars->next;
719 }
720 if (!ruby_dyna_vars) {
721 ruby_dyna_vars = new_dvar(id, value, 0);
722 }
723 else {
724 vars = new_dvar(id, value, ruby_dyna_vars->next);
725 ruby_dyna_vars->next = vars;
726 }
727 }
(eval.c)
最後のif文が変数の追加だ。そこに注目すると、常にruby_dyna_varsの
「次」にリンクを割り込ませていることがわかる。
つまり図4のようになるのだ。

図4: ruby_dyna_varsの構造
パーサのときとの違いは二点だ。まずスコープの切れめを示すヘッダ(id=0)が
リンクの手元側に付く。また本鎖からぶらさがっているリンクがない。
即ちruby_dyna_varsは常にまっすぐな一本のリストを形成する。
この二点はもちろん関連性がある。リストを一本にするためには、パーサでは
途中にぶらさげていたエントリをリストの途中に挿入できるようにしなければ
いけない。しかしもしヘッダが奥に付いているとスコープの最初の一個がうま
く挿入できないのである(図5)。
このような操作をするためには頭に戻って(そもそもそれが難しい)リンクを
全部たどるか、prevリンクを付けないとならない。前者は面倒なうえにスピー
ドが落ちるし、後者はRVarmapに隙間がないので無理だ。

図5: うまくエントリを挿入できない
先程はジャンプタグ関係を消して見せたが、rb_yield_0()のジャンプ
にはこれまでにない工夫がある。どうして工夫が必要になるのか、
その原因を先に言っておこう。以下のプログラムを見てもらいたい。
[0].each do break end # breakで抜ける場所
このように、ブロックからbreakした場合はブロックを積んだメソッドに抜け
ないといけないはずである。それは実際にはどういうことだろうか。イテレー
タを起動しているときの(動的)コールグラフを見て考えてみよう。
rb_eval(NODE_ITER) .... catch(TAG_BREAK)
rb_eval(NODE_CALL) .... catch(TAG_BREAK)
rb_eval(NODE_YIELD)
rb_yield_0
rb_eval(NODE_BREAK) .... throw(TAG_BREAK)
ブロックを積んだのはNODE_ITERなのだから、breakではNODE_ITERまで
戻るべきだろう。ところがNODE_ITERより前にNODE_CALLがTAG_BREAKを
待ち構えている。メソッド越しのbreakをエラーにするためである。これは
困った。なんとかしてNODE_ITERまで一気に抜けないといけない。
しかも実は「NODE_ITERに戻る」でもまだまずい。イテレータがネストして
いたらNODE_ITERも複数存在することがあり、現在のブロックに対応するのが
一番最初のNODE_ITERとも限らない。つまり「いま起動中のブロックを積んだ
NODE_ITER」だけを限定して戻らなければならないのだ。
そこでどうしているのか見てみよう。
▼rb_yield_0()−タグ関係
3826 PUSH_TAG(PROT_NONE);
3827 if ((state = EXEC_TAG()) == 0) {
/* ……本体を評価する…… */
3838 }
3839 else {
3840 switch (state) {
3841 case TAG_REDO:
3842 state = 0;
3843 CHECK_INTS;
3844 goto redo;
3845 case TAG_NEXT:
3846 state = 0;
3847 result = prot_tag->retval;
3848 break;
3849 case TAG_BREAK:
3850 case TAG_RETURN:
3851 state |= (serial++ << 8);
3852 state |= 0x10;
3853 block->tag->dst = state;
3854 break;
3855 default:
3856 break;
3857 }
3858 }
3859 POP_TAG();
(eval.c)
TAG_BREAKとTAG_RETURNのところが肝心だ。
まずserialはrb_yield_0()のスタティック変数なので、rb_yield_0()の呼び出
しごとに違う値が得られることになる。「serial」は「シリアルナンバー」の
serialだ。
8ビット左シフトしているのはTAG_xxxxの値を避けるためのようだ。TAG_xxxxは
0x1〜0x8なので4ビットあれば済む。そして0x10のbit orは、serialの
オーバーフロー対策だと思われる。32ビットマシンだとserialは24ビット分
(1600万回分)
しかないので最近のマシンなら10秒かからずにオーバーフローさせられる。
そうするとその回は下位24ビットに0が並ぶことになるので、もし0x10が
なかったらstateがTAG_xxxxと同じ値になってしまう(図6参照)。

図6: block->tag->dst
さて、これでtag->dstはTAG_xxxxとも違う、しかも呼び出しごとにユニークな
値になった。そうするとこれまでのような普通のswitchでは受け取れなくなる
ので、ジャンプを止めるほうもそれなりの工夫が必要になるはずである。
それはどこかと言うと、rb_eval:NODE_ITERのここだ。
▼rb_eval()−NODE_ITER(ジャンプを止める)
case NODE_ITER:
{
state = EXEC_TAG();
if (state == 0) {
/* ……イテレータを起動…… */
}
else if (_block.tag->dst == state) {
state &= TAG_MASK;
if (state == TAG_RETURN || state == TAG_BREAK) {
result = prot_tag->retval;
}
}
}
対応しているNODE_ITERとrb_yield_0()ではblockは同じものを指しているはず
なので、rb_yield_0()でセットしたtag->dstがここに出てくることになる。そ
うすると対応するNODE_ITERだけでうまくジャンプを止められるわけだ。
現在評価中のメソッドがイテレータであるかどうか、つまりブロックが
あるかどうか、はrb_block_given_p()で確認できる。ここまでを読めば
実装はわかるだろう。
▼rb_block_given_p()
3726 int
3727 rb_block_given_p()
3728 {
3729 if (ruby_frame->iter && ruby_block)
3730 return Qtrue;
3731 return Qfalse;
3732 }
(eval.c)
問題ないと思う。今回話題にしたかったのは実はもう一つのチェック用
関数、rb_f_block_given_p()のほうだ。
▼rb_f_block_given_p()
3740 static VALUE
3741 rb_f_block_given_p()
3742 {
3743 if (ruby_frame->prev && ruby_frame->prev->iter && ruby_block)
3744 return Qtrue;
3745 return Qfalse;
3746 }
(eval.c)
こちらはRubyのblock_given?の実体である。rb_block_given_p()と比較すると
ruby_frameのprevを調べているところが違うようだ。どうしてだろう。
ブロックを積む仕組みを考えるとrb_block_given_p()のように現在の
ruby_frameを調べるのが正しい。しかしRubyレベルからblock_given?を呼んだ
場合は、block_given?それ自体がメソッドなのでFRAMEが一段余計に積まれて
いる。だからもう一段前を調べる必要があるのだ。
Proc
Procオブジェクトを実装の観点から言うと「Rubyレベルに持ち出せる
BLOCK」である。Rubyレベルに持ち出せる、ということは自由度が上がる反
面、いつどこで使われるか全くわからなくなるということでもある。そのこと
がどう影響を与えているか注目して実装を見ていこう。
Procオブジェクトの生成
ProcオブジェクトはProc.newで作るのだった。その実体はproc_new()である。
▼proc_new()
6418 static VALUE
6419 proc_new(klass)
6420 VALUE klass;
6421 {
6422 volatile VALUE proc;
6423 struct BLOCK *data, *p;
6424 struct RVarmap *vars;
6425
6426 if (!rb_block_given_p() && !rb_f_block_given_p()) {
6427 rb_raise(rb_eArgError,
"tried to create Proc object without a block");
6428 }
6429
/* (A)struct RDataとstruct BLOCKをまとめて確保する */
6430 proc = Data_Make_Struct(klass, struct BLOCK,
blk_mark, blk_free, data);
6431 *data = *ruby_block;
6432
6433 data->orig_thread = rb_thread_current();
6434 data->wrapper = ruby_wrapper;
6435 data->iter = data->prev?Qtrue:Qfalse;
/* (B)本質的な初期化はここまでで完了 */
6436 frame_dup(&data->frame);
6437 if (data->iter) {
6438 blk_copy_prev(data);
6439 }
6440 else {
6441 data->prev = 0;
6442 }
6443 data->flags |= BLOCK_DYNAMIC;
6444 data->tag->flags |= BLOCK_DYNAMIC;
6445
6446 for (p = data; p; p = p->prev) {
6447 for (vars = p->dyna_vars; vars; vars = vars->next) {
6448 if (FL_TEST(vars, DVAR_DONT_RECYCLE)) break;
6449 FL_SET(vars, DVAR_DONT_RECYCLE);
6450 }
6451 }
6452 scope_dup(data->scope);
6453 proc_save_safe_level(proc);
6454
6455 return proc;
6456 }
(eval.c)
Procオブジェクトの作成自体は意外と簡単である。(A)から(B)の
間でProcオブジェクトの領域が確保され、初期化も終わる。
Data_Make_Struct()はmalloc()とData_Wrap_Struct()を同時にやる単純な
マクロだ。
問題はその後だ。
frame_dup()blk_copy_prev()FL_SET(vars, DVAR_DONT_RECYCLE)scope_dup()この四つの目的は全部同じである。それは
POPされても回収されないようにする
である。ここで、「全部」というのはprevまで含めて全部だ。そこに積んであ
るスタックフレームを全部malloc()してコピーして複製を作る。VARSは普通だ
とPOPと同時にrb_gc_force_recycle()で強制回収されるのだが、それも
DVAR_DONT_RECYCLEフラグを付けて停止させる。などなどだ。実に思いきった
ことをする。
どうしてこんな凄まじいことをしないといけないのだろうか。それは、イテレー
タブロックと違ってProcは作成元のメソッドよりも長生きできるからだ。そ
してメソッドが終了するということはマシンスタックに確保されるFRAMEや
ITERや、SCOPEのlocal_varsが無効になるということで、無効になった
メモリを後から使ったらどういう結果になるかは簡単に予想できる
(解答例:困ったことになる)。
それでもせめて複数のProcで同じFRAMEを使うとかそういうことはできないか、
と考えてみたのだが、old_frameなどのようにローカル変数にポインタを退避
しているところがあるのでうまくいきそうにない。どうせ苦労するのなら例え
ば最初から全部malloc()で割り当てるようにする、などの工夫に労力を使うほ
うがよさそうだ。
それにしても、これだけ凄いことをしているのによくあんな速さで動くなあ、 と筆者はしみじみ思うのだ。実にいい時代になったものである。
先程は一言で「フレームを全部複製」と片付けてしまったが、それではあんま りなのでもう少し詳しく見ておこう。ポイントは次の二点である。
ではまず各スタックフレームの記憶形式のまとめから始めよう。
| フレーム | 記憶形式 | prevポインタ | |||
FRAME | スタック | あり | |||
SCOPE | スタック | なし | |||
local_tbl | ヒープ | ||||
local_vars | スタック | ||||
VARS | ヒープ | なし | |||
BLOCK | スタック | あり |
CLASS CREF ITERはこのさい必要ない。CLASSは一般のRubyオブジェクトなので
間違ってもrb_gc_force_recycle()したりはしない(できない)し、CREFと
ITERはその時々の値をFRAMEに格納してしまえばもう用済みだからである。
この表にある四つのフレームが重要なのは、あとから何度も変更したり参照
したりする必要があるからだ。残りの三つはそうではない。
それでどうやって全部複製するかという話だ。どうやって、と言ってももちろ
ん「malloc()で」とかいうことではない。どうやって「全部」複製するかとい
うところが問題なのだ。というのは、表を見てほしいのだが、prevポインタが
ないフレームがある。つまりリンクを辿れない。それならどうやって全部複製
するのだろうか。
これにはなかなか巧妙な手口が使われている。SCOPEを例に取ろう。
先程SCOPEを複製するのにscope_dup()という関数を使っていたので、
まずそれを見てみよう。
▼scope_dup()先頭のみ
6187 static void
6188 scope_dup(scope)
6189 struct SCOPE *scope;
6190 {
6191 ID *tbl;
6192 VALUE *vars;
6193
6194 scope->flags |= SCOPE_DONT_RECYCLE;
(eval.c)
見ての通りSCOPE_DONT_RECYCLEを付ける。
そこで次にPOP_SCOPE()の定義を見てみると、
▼POP_SCOPE()先頭のみ
869 #define POP_SCOPE() \
870 if (ruby_scope->flags & SCOPE_DONT_RECYCLE) { \
871 if (_old) scope_dup(_old); \
872 } \
(eval.c)
ポップするときに現在のSCOPE(ruby_scope)にSCOPE_DONT_RECYCLEフ
ラグが立っていたら、その一つ前のSCOPE(_old)もscope_dup()する、
とある。つまりこれにもSCOPE_DONT_RECYCLEが付く。こうやって一つ一つポッ
プするところでフラグを伝播させていくわけだ(図7)。

図7: フラグの伝播
VARSもprevポインタがないので同じ手法を使ってDVAR_DONT_RECYCLEという
フラグを伝播させている。
次に第二点、「なぜ全部複製するのか」を考えてみよう。Procを作ればその
SCOPEのローカル変数を後から参照できるのはわかるが、だからと言って何も
その前のSCOPEまで含めて全部コピーしてしまうことはないのではないだろうか。
正直に言うと、筆者はこの答えがわからなくて三日ほどどうやってこの節を書 いたらいいか悩んでいたのだが、ついさっき答えがわかった。次のプログラム を見てほしい。
def get_proc
Proc.new { nil }
end
env = get_proc { p 'ok' }
eval("yield", env)
これはまだ説明していない機能だが、evalの第二引数にProcオブジェクトを渡
すとその環境で文字列を評価できるのである。
というのはつまり、ここまで読んできてくれた読者ならばわかると思うが、
Proc(つまりBLOCK)から各種環境を取り出してプッシュして評価してく
れるということである。そうするともちろんBLOCKも積んでくれるので、そ
のBLOCKをまたProcにできる。そうしたらそのProcを使ってまた
evalして……とやれば、Rubyレベルからruby_blockのほとんどの情報に
好き放題アクセスできることになる。それが、スタックをまるごと全部
複製しないといけない理由だ。
Procの起動
次は生成したProcオブジェクトの起動について見てみる。Rubyからは
Proc#callで起動できるから、その実体を追っていけばいい。Proc#callの実体
はproc_call()だ。
▼proc_call()
6570 static VALUE
6571 proc_call(proc, args)
6572 VALUE proc, args; /* OK */
6573 {
6574 return proc_invoke(proc, args, Qtrue, Qundef);
6575 }
(eval.c)
proc_invoke()に委譲。invokeを辞書で索くと「(神などに)救いを求め
て呼び掛ける」などと書いてあるのだがプログラミングの文脈だと「起動する」
とだいたい同じ意味で使うことが多いようだ。例えば"Invoking gcc"と言うよう
に。日本語にするなら「起動」「発動」あたりがいいのではなかろうか。
そのproc_invoke()のプロトタイプはと言うと、
proc_invoke(VALUE proc, VALUE args, int pcall, VALUE self)
となっているが、先程見たところによるとpcall=Qtrue、
self=Qundefなのでこの二つは定数畳み込みで潰してしまう。
▼proc_invoke(簡約版)
static VALUE
proc_invoke(proc, args, /* pcall=Qtrue */, /* self=Qundef */)
VALUE proc, args;
VALUE self;
{
struct BLOCK * volatile old_block;
struct BLOCK _block;
struct BLOCK *data;
volatile VALUE result = Qnil;
int state;
volatile int orphan;
volatile int safe = ruby_safe_level;
volatile VALUE old_wrapper = ruby_wrapper;
struct RVarmap * volatile old_dvars = ruby_dyna_vars;
/*(A)procからBLOCKを取り出しdataに代入する */
Data_Get_Struct(proc, struct BLOCK, data);
/*(B)blk_orphan */
orphan = blk_orphan(data);
ruby_wrapper = data->wrapper;
ruby_dyna_vars = data->dyna_vars;
/*(C)dataからBLOCKを積む */
old_block = ruby_block;
_block = *data;
ruby_block = &_block;
/*(D)ITER_CURに遷移する */
PUSH_ITER(ITER_CUR);
ruby_frame->iter = ITER_CUR;
PUSH_TAG(PROT_NONE);
state = EXEC_TAG();
if (state == 0) {
proc_set_safe_level(proc);
/*(E)ブロック起動 */
result = rb_yield_0(args, self, 0, pcall);
}
POP_TAG();
POP_ITER();
if (ruby_block->tag->dst == state) {
state &= TAG_MASK; /* ターゲット指定ジャンプ */
}
ruby_block = old_block;
ruby_wrapper = old_wrapper;
ruby_dyna_vars = old_dvars;
ruby_safe_level = safe;
switch (state) {
case 0:
break;
case TAG_BREAK:
result = prot_tag->retval;
break;
case TAG_RETURN:
if (orphan) { /* orphan procedure */
localjump_error("return from proc-closure", prot_tag->retval);
}
/* fall through */
default:
JUMP_TAG(state);
}
return result;
}
肝心なところはC、D、Eの三つだ。
(C)NODE_ITERでは構文木からBLOCKを作って積んだが、今回はProcから
BLOCKを取り出して積む。
(D)rb_call0()ではITER_PREを経由してITER_CURにしたが、今回はいきなり
ITER_CURに突入する。
(E)普通のイテレータならメソッド呼び出しがはさまってから
yieldが起こりrb_yield_0()に行くわけだが、今回は問答無用で
rb_yield_0()を呼び、積んだばかりのブロックを起動する。
つまりイテレータではNODE_ITER〜rb_call0()〜NODE_YIELDと三個所に分け
てやっていた作業をまとめて一気にやってしまうわけだ。
最後に(B)のblk_orphan()の意味について話しておこう。orphanは「孤児」
という意味で、「Procを作成したメソッドが終了している」状態を判定するた
めの関数である。例えばBLOCKが使っているSCOPEが既にポップされていたら終
了していると判断すればよい。
Proc前章でメソッドの引数とパラメータについていろいろ話したが、ブロック 引数の話がなかった。簡単にではあるが、ここでその完結編をやろう。
def m(&block) end
これは「ブロックパラメータ」だ。これの実現方法は非常に簡単である。mがイ
テレータならばもうBLOCKが積まれているはずなので、それをProc化して(こ
の場合なら)blockというローカル変数に代入すれば済む。ブロックをProcに
するには先程やったばかりのproc_new()を呼ぶだけでよい。どうしてそれ
でいいのかちょっとわかりにくいかもしれないが、Proc.newだろうとmだろう
と「メソッドが呼び出されていて、BLOCKが積まれている」という状況に変わ
りはないはずだ。だからCレベルからproc_new()を呼べばいつでもブロックを
Proc化できる。
またmがイテレータでないなら単にnilを代入すればいい。
次にブロックを渡すほうを。
m(&block)
こちらは「ブロック引数」だ。これも簡単で、block(に入っているProcオブ
ジェクト)からBLOCKを取り出して積めばよい。PUSH_BLOCK()と違うのは先
にBLOCKが作ってあるかそうでないかという点だけだ。
ちなみに、この作業をやっている関数はblock_pass()である。気になるならそ
のあたりを見て確かめてほしい。ただし本当にここで言った通りのことしか
していないのでガッカリするかもしれないが……。
御意見・御感想・誤殖の指摘などは 青木峰郎 <aamine@loveruby.net> までお願いします。
『Rubyソースコード完全解説』 はインプレスダイレクトで御予約・御購入いただけます (書籍紹介ページへ飛びます)。
Copyright (c) 2002-2004 Minero Aoki, All rights reserved.