第16章 ブロック

イテレータ

この章ではRubyスタック七本のうち最後の大物、BLOCKが登場する。 これが終われば評価器の内部状態についてはわかったも同然だ。

全体像

イテレータの仕組みはいったいどうなっているのか。 まず次のような小さいプログラムで考えてみよう。

▼ソースプログラム

iter_method() do
  9   # ブロックを探す目印
end

用語を確認しておく。このプログラムで言うとiter_methodが イテレータメソッド、doendがイテレータブロックだ。 このプログラムの構文木をダンプしたらこうなった。

▼対応する構文木

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_CALLNODE_YIELDを加えた三段階に分かれていることが わかった。それは即ち

  1. ブロックを積む(NODE_ITER
  2. イテレータであるメソッドを呼び出す(NODE_CALL
  3. yieldNODE_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文のサポートが入っていたのでそれを削除してある。タグ 関係を除くと、ITERBLOCKのプッシュ・ポップだけだ。あとはNODE_FCALLを 普通にrb_eval()しているだけなのだから、このITERBLOCKがメソッドを イテレータにするための必要条件である。

BLOCKのプッシュが必要になるのはまあいいとして、ITERは何のためにあるの だろうか。実はITERの意味を考えるにはBLOCKを使うほうの身になって考 えてみる必要がある。

例えば今まさにメソッドが呼び出されたとしよう。そしてruby_blockが存在し た。しかしBLOCKはメソッド呼び出しの区切りと関係なく積まれるので、ブロッ クが存在するからと言ってそれが自分のために積まれたブロックであるかどう かはわからない。もしかすると自分の前のメソッドのために積まれたブロック かもしれないではないか(図1)。

(stacks)
図1: FRAMEBLOCKは一対一対応ではない

そこでブロックがどのメソッドのために積まれたのか判別するために ITERを使うわけだ。なぜBLOCKFRAMEごとに積まないかと言うと、 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とは「作成した時点での環境のスナップショット」だ。 その証拠にCREFBLOCK以外のスタックフレーム六本が保存されている。 CREFruby_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_ITERITER_PREを積んだので、このコードでruby_iterITER_CURになる。これで初めてメソッドはイテレータに「なる」わけだ。 またスタックの状態を図示すると図2のようになっている。

(itertrans)
図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_iterITER_PREのときはruby_blockを一つどけるようになっている。 このコードが活躍するのは例えば以下のような場合だ。

obj.m1 { nil }.m2 { nil }

この式の評価順は

  1. m2のブロックをプッシュ
  2. m1のブロックをプッシュ
  3. メソッドm1呼び出し
  4. メソッドm2呼び出し

となる。だからBEGIN_CALLARGSがないとm1m2のブロックを呼び出して しまう。

また、もう一つイテレータがつながったとしてもその場合は 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_sttsyieldの引数である。avalue_to_yvalue()は多重代入のところ でちょっと触れただけだが、無視しておいて問題ない。動作の核心はそんなも のではなくrb_yield_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に記憶していたものと すりかえている。単純な退避・復帰をしているものはいいとして、その他の 注意すべきフレームの扱いを見ていこう。

FRAME

struct FRAME frame;

frame = block->frame;     /* 構造体まるごとコピー */
frame.prev = ruby_frame;  /* この二行で…… */
ruby_frame = &(frame);    /* ……frameがプッシュされる */

他のフレームと違い、FRAMEは記憶しているものそのままではなく新しい FRAMEを複製して作るようだ。つまり図3のようになる。

(framepush)
図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のようになるのだ。

(dynavarseval)
図4: ruby_dyna_varsの構造

パーサのときとの違いは二点だ。まずスコープの切れめを示すヘッダ(id=0)が リンクの手元側に付く。また本鎖からぶらさがっているリンクがない。 即ちruby_dyna_varsは常にまっすぐな一本のリストを形成する。

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

(insert)
図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_CALLTAG_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_BREAKTAG_RETURNのところが肝心だ。

まずserialrb_yield_0()のスタティック変数なので、rb_yield_0()の呼び出 しごとに違う値が得られることになる。「serial」は「シリアルナンバー」の serialだ。

8ビット左シフトしているのはTAG_xxxxの値を避けるためのようだ。TAG_xxxx0x10x8なので4ビットあれば済む。そして0x10のbit orは、serialの オーバーフロー対策だと思われる。32ビットマシンだとserialは24ビット分 (1600万回分) しかないので最近のマシンなら10秒かからずにオーバーフローさせられる。 そうするとその回は下位24ビットに0が並ぶことになるので、もし0x10が なかったらstateTAG_xxxxと同じ値になってしまう(図6参照)。

(dst)
図6: block->tag->dst

さて、これでtag->dstTAG_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_ITERrb_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_frameprevを調べているところが違うようだ。どうしてだろう。

ブロックを積む仕組みを考えると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()を同時にやる単純な マクロだ。

問題はその後だ。

この四つの目的は全部同じである。それは

である。ここで、「全部」というのはprevまで含めて全部だ。そこに積んであ るスタックフレームを全部malloc()してコピーして複製を作る。VARSは普通だ とPOPと同時にrb_gc_force_recycle()で強制回収されるのだが、それも DVAR_DONT_RECYCLEフラグを付けて停止させる。などなどだ。実に思いきった ことをする。

どうしてこんな凄まじいことをしないといけないのだろうか。それは、イテレー タブロックと違ってProcは作成元のメソッドよりも長生きできるからだ。そ してメソッドが終了するということはマシンスタックに確保されるFRAMEITERや、SCOPElocal_varsが無効になるということで、無効になった メモリを後から使ったらどういう結果になるかは簡単に予想できる (解答例:困ったことになる)。

それでもせめて複数のProcで同じFRAMEを使うとかそういうことはできないか、 と考えてみたのだが、old_frameなどのようにローカル変数にポインタを退避 しているところがあるのでうまくいきそうにない。どうせ苦労するのなら例え ば最初から全部malloc()で割り当てるようにする、などの工夫に労力を使うほ うがよさそうだ。

それにしても、これだけ凄いことをしているのによくあんな速さで動くなあ、 と筆者はしみじみ思うのだ。実にいい時代になったものである。

浮動フレーム

先程は一言で「フレームを全部複製」と片付けてしまったが、それではあんま りなのでもう少し詳しく見ておこう。ポイントは次の二点である。

ではまず各スタックフレームの記憶形式のまとめから始めよう。

フレーム記憶形式prevポインタ
FRAMEスタックあり
SCOPEスタックなし
local_tblヒープ
local_varsスタック
VARSヒープなし
BLOCKスタックあり

CLASS CREF ITERはこのさい必要ない。CLASSは一般のRubyオブジェクトなので 間違ってもrb_gc_force_recycle()したりはしない(できない)し、CREFITERはその時々の値を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)

ポップするときに現在のSCOPEruby_scope)にSCOPE_DONT_RECYCLEフ ラグが立っていたら、その一つ前のSCOPE_old)もscope_dup()する、 とある。つまりこれにもSCOPE_DONT_RECYCLEが付く。こうやって一つ一つポッ プするところでフラグを伝播させていくわけだ(図7)。

(flaginfect)
図7: フラグの伝播

VARSprevポインタがないので同じ手法を使って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=Qtrueself=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_ITERrb_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.