第15章 メソッド

この章ではメソッドの探索と起動について話す。

メソッドの探索

用語

この章ではメソッド呼び出しとメソッド定義の話を両方やる関係で、 実に様々な「引数」が出てくる。そこで紛らわしくならないように ここで厳密に用語を決めてしまうことにしよう。

m(a)          # aは「通常の引数」
m(*list)      # listは「配列引数」
m(&block)     # blockは「ブロック引数」

def m(a)      # aは「通常のパラメータ」
def m(a=nil)  # aは「オプションパラメータ」、nilは「そのデフォルト値」
def m(*rest)  # restは「restパラメータ」
def m(&block) # blockは「ブロックパラメータ」

ようするに渡すときは全部「引数」、受けるほうは全部「パラメータ」で 種類によってそれぞれ形容詞を付ける、ということだ。

ただし、ここで挙げたうち「ブロック引数」と「ブロックパラメータ」に ついては次章『ブロック』で扱うことになる。

調査

▼ソースプログラム

obj.method(7,8)

▼対応する構文木

NODE_CALL
nd_mid = 9049 (method)
nd_recv:
    NODE_VCALL
    nd_mid = 9617 (obj)
nd_args:
    NODE_ARRAY [
    0:
        NODE_LIT
        nd_lit = 7:Fixnum
    1:
        NODE_LIT
        nd_lit = 8:Fixnum
    ]

メソッド呼び出しのノードはNODE_CALLだ。 nd_argsにはNODE_ARRAYのリストとして引数が格納されている。

それとこの他にメソッド呼び出しノードとしてはNODE_FCALLNODE_VCALLとい うのもある。NODE_FCALLが「method(args)」の形式で、NODE_VCALLはローカル 変数と同じ「method」という形式の呼び出しに対応している。実際には FCALLVCALLは一つにまとめることもできるが、VCALLのときには引数を準備 するためのコードが必要ないので、その分のメモリと時間を節約するためだけに 区別されている。

ではrb_eval()でのNODE_CALLのハンドラを見てみよう。

rb_eval()NODE_CALL

2745  case NODE_CALL:
2746    {
2747        VALUE recv;
2748        int argc; VALUE *argv; /* used in SETUP_ARGS */
2749        TMP_PROTECT;
2750
2751        BEGIN_CALLARGS;
2752        recv = rb_eval(self, node->nd_recv);
2753        SETUP_ARGS(node->nd_args);
2754        END_CALLARGS;
2755
2756        SET_CURRENT_SOURCE();
2757        result = rb_call(CLASS_OF(recv),recv,node->nd_mid,argc,argv,0);
2758    }
2759    break;

(eval.c)

問題は三つのマクロ、BEGIN_CALLARGS SETUP_ARGS() END_CALLARGSだろう。 rb_eval()がレシーバの評価でrb_call()がメソッド起動らしいので、この三つ のマクロでは引数の評価をしているんだろうなあ、とはなんとなく想像できる が、実際のところ何をしているのだろうか。BEGIN_CALLARGSEND_CALLARGSは イテレータの話をしてからでないとわかりづらいので次章『ブロック』で 改めて説明する。ここではSETUP_ARGS()についてだけ調査しよう。

SETUP_ARGS()

SETUP_ARGS()はメソッドの引数部分を評価するマクロである。このマクロ内 では、元プログラムのコメントにもあるように、argcargvという変数を 使うのでそれをあらかじめ定義しておかなければならない。また TMP_ALLOC()を使うのでTMP_PROTECTも使っておかなければならない。 だから以下のようにするのが定型である。

int argc; VALUE *argv;   /* used in SETUP_ARGS */
TMP_PROTECT;

SETUP_ARGS(args_node);

args_nodeがメソッドの引数(を表現するノード)で、それを評価した 値の配列に変え、argvに格納する。では見てみよう。

SETUP_ARGS()

1780  #define SETUP_ARGS(anode) do {\
1781      NODE *n = anode;\
1782      if (!n) {\                             引数なし
1783          argc = 0;\
1784          argv = 0;\
1785      }\
1786      else if (nd_type(n) == NODE_ARRAY) {\  通常引数のみ
1787          argc=n->nd_alen;\
1788          if (argc > 0) {\   引数あり
1789              int i;\
1790              n = anode;\
1791              argv = TMP_ALLOC(argc);\
1792              for (i=0;i<argc;i++) {\
1793                  argv[i] = rb_eval(self,n->nd_head);\
1794                  n=n->nd_next;\
1795              }\
1796          }\
1797          else {\            引数なし
1798              argc = 0;\
1799              argv = 0;\
1800          }\
1801      }\
1802      else {\                                 配列引数や
1803          VALUE args = rb_eval(self,n);\      ブロック引数がある
1804          if (TYPE(args) != T_ARRAY)\
1805              args = rb_ary_to_ary(args);\
1806          argc = RARRAY(args)->len;\
1807          argv = ALLOCA_N(VALUE, argc);\
1808          MEMCPY(argv, RARRAY(args)->ptr, VALUE, argc);\
1809      }\
1810  } while (0)

(eval.c)

ちょっと長いが、キッパリと三つに分岐しているので実はたいして恐くない。 それぞれの枝の意味はコメントに入れておいた通りだ。

引数なしのときはどうでもいいとして、残りの二つの枝では似たようなことを やっている。おおまかに言うとやっていることは三段階で、

  1. 引数を入れる領域を確保する
  2. 引数の式を評価
  3. 値を変数領域にコピー

である。コードに書き込んでみるとこうだ (ついでにちょっと整形しておいた)。

/***** else if節、argc!=0 *****/
int i;
n = anode;
argv = TMP_ALLOC(argc);                         /* 1 */
for (i = 0; i < argc; i++) {
    argv[i] = rb_eval(self, n->nd_head);        /* 2,3 */
    n = n->nd_next;
}

/***** else節 *****/
VALUE args = rb_eval(self, n);                  /* 2 */
if (TYPE(args) != T_ARRAY)
    args = rb_ary_to_ary(args);
argc = RARRAY(args)->len;
argv = ALLOCA_N(VALUE, argc);                   /* 1 */
MEMCPY(argv, RARRAY(args)->ptr, VALUE, argc);   /* 3 */

else if側ではTMP_ALLOC()を使っているのにelse側では ALLOCA_N()、つまり普通のalloca()を使っているのはどうしてだろう。 C_ALLOCAな環境ではalloca()malloc()に等しいのだから、危険では ないだろうか。

この点は「else側では引数の値がargsにも入っている」ことが ポイントである。図にすれば図1のようになる。

(anchor)
図1: ヒープにあっても大丈夫

一つでもVALUEがスタック上にあればそこを経由して連鎖的にマークされる。 そのようなVALUEは他のVALUEをスタックに繁ぎ止める錨(anchor)のような 役割を果たす、即ち「anchor VALUE」となる。 else側ではargsがanchor VALUEである。

ちなみにanchor VALUEというのは今作った造語だ。

rb_call()

SETUP_ARGS()はどちらかといえば横道だ。ここからは本筋に戻ろう。メソッド を起動する関数rb_call()である。本物には見付からなかった場合に例外を上 げたりするコードがあるのだが、例によって全部省略する。

rb_call()(簡約版)

static VALUE
rb_call(klass, recv, mid, argc, argv, scope)
    VALUE klass, recv;
    ID    mid;
    int argc;
    const VALUE *argv;
    int scope;
{
    NODE  *body;
    int    noex;
    ID     id = mid;
    struct cache_entry *ent;

    /* メソッドキャッシュを検索 */
    ent = cache + EXPR1(klass, mid);
    if (ent->mid == mid && ent->klass == klass) {
        /* キャッシュにヒットした */
        klass = ent->origin;
        id    = ent->mid0;
        noex  = ent->noex;
        body  = ent->method;
    }
    else {
        /* キャッシュミス。地道に検索 */
        body = rb_get_method_body(&klass, &id, &noex);
    }

    /* ……可視性チェックをする…… */

    return rb_call0(klass, recv, mid, id,
                    argc, argv, body, noex & NOEX_UNDEF);
}

基本的なメソッド探索の方法については第2章『オブジェクト』で話した。スーパー クラスをたどりながらm_tblを検索すればいい。それをやるのが search_method()であった。

原理はその通りなのだがしかし実際に実行する段になるとメソッド呼び出しの たびに何回もハッシュを引いて検索していたのでは速度が遅すぎる。これを改 善するためrubyでは一回呼び出したメソッドはキャッシュされるようになって いる。一回呼ばれたメソッドはすぐにまた呼び出されることが多い、ということ が経験上の事実として知られており、このキャッシュのヒット率は高い。

そのキャッシュを索いているのがrb_call()の前半である。この

ent = cache + EXPR1(klass, mid);

の一行だけでキャッシュが検索されている。仕組みはあとで詳しく見よう。

キャッシュが外れたときはその次のrb_get_method_body()で地道にクラスツリー を検索して、ついでにその結果をキャッシュしておくようになっている。 検索全体の流れを図にすれば図2のような感じだ。

(msearch)
図2: メソッド探索

メソッドキャッシュ

次にメソッドキャッシュの構造を詳しく見てみよう。

▼メソッドキャッシュ

 180  #define CACHE_SIZE 0x800
 181  #define CACHE_MASK 0x7ff
 182  #define EXPR1(c,m) ((((c)>>3)^(m))&CACHE_MASK)
 183
 184  struct cache_entry {            /* method hash table. */
 185      ID mid;                     /* method's id */
 186      ID mid0;                    /* method's original id */
 187      VALUE klass;                /* receiver's class */
 188      VALUE origin;               /* where method defined  */
 189      NODE *method;
 190      int noex;
 191  };
 192
 193  static struct cache_entry cache[CACHE_SIZE];

(eval.c)

仕組みを一言で言えばハッシュテーブルである。ハッシュテーブルの原理とは ようするにテーブル検索を配列のインデクシングに変換することであった。そ の時に必要になるものは三つ。データを格納する配列、キー、そしてハッシュ 関数だ。

まず配列はここではstruct cache_entryの配列である。そしてメソッドはクラ スとメソッド名だけで一意に決まるから、この二つがハッシュ計算のキーとな る。あとはそのキーからキャッシュ配列のインデックス(0x0000x7ff)を生 成するハッシュ関数を作ればよい。それがEXPR1()だ。引数のcはクラスオブ ジェクトで、mはメソッド名(のID)である(図3)。

(mhash)
図3: メソッドキャッシュ

ただしEXPR1()は完全ハッシュ関数でもなんでもないので違うメソッドが偶然 同じインデックスを生成してしまうこともありうる。だがこれはあくまでキャッ シュなので衝突しても問題はない。ただ動作が少し遅くなるだけである。

メソッドキャッシュの効果

ところでメソッドキャッシュは実際どのくらいの効果があるのだろうか。 「……ことが知られている」と言われても納得できない。自分で計測だ。

種類プログラムヒット率
LALR(1)パーサ生成racc ruby.y99.9%
メールスレッド生成とあるメーラ99.1%
ドキュメント生成rd2html rubyrefm.rd97.8%

なんと、実験した三つの例すべてでヒット率95%以上を記録した。 これは凄い。どうやら「……ことが知られている」の効果は 抜群のようだ。

起動

rb_call0()

いろいろあってようやくメソッド起動に辿りついたわけだが、このrb_call0()が またデカい。200行以上ということはページ上では5、6ページになるだろうか。 ビューアならともかく、紙で一気に並べると悲惨なことになるので細かく分割 しながら見ていくことにしよう。まずは概形から。

rb_call0()(概形)

4482  static VALUE
4483  rb_call0(klass, recv, id, oid, argc, argv, body, nosuper)
4484      VALUE klass, recv;
4485      ID    id;
4486      ID    oid;
4487      int argc;                   /* OK */
4488      VALUE *argv;                /* OK */
4489      NODE *body;                 /* OK */
4490      int nosuper;
4491  {
4492      NODE *b2;           /* OK */
4493      volatile VALUE result = Qnil;
4494      int itr;
4495      static int tick;
4496      TMP_PROTECT;
4497
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      }
4507
4508      if ((++tick & 0xff) == 0) {
4509          CHECK_INTS;             /* better than nothing */
4510          stack_check();
4511      }
4512      PUSH_ITER(itr);
4513      PUSH_FRAME();
4514
4515      ruby_frame->last_func = id;
4516      ruby_frame->orig_func = oid;
4517      ruby_frame->last_class = nosuper?0:klass;
4518      ruby_frame->self = recv;
4519      ruby_frame->argc = argc;
4520      ruby_frame->argv = argv;
4521
4522      switch (nd_type(body)) {
              /* ……本処理…… */
4698
4699        default:
4700          rb_bug("unknown node type %d", nd_type(body));
4701          break;
4702      }
4703      POP_FRAME();
4704      POP_ITER();
4705      return result;
4706  }

(eval.c)

まずITERを積んでこのメソッドがイテレータかどうかを最終的に決定する。そ の後のPUSH_FRAME()でその値がすぐに使われるのでPUSH_ITER()はその前にな ければいけない。PUSH_FRAME()はすぐあとで見る。

そして先に「……本処理……」のところの話をすると、ここには 以下のようなノード別に分かれてそれぞれ起動処理をする。

NODE_CFUNCCで定義されたメソッド
NODE_IVARattr_reader
NODE_ATTRSETattr_writer
NODE_SUPERsuper
NODE_ZSUPER引数なしのsuper
NODE_DMETHODUnboundMethodの起動
NODE_BMETHODMethodの起動
NODE_SCOPERubyで定義されたメソッド

本書では説明していないものもあるが、どれもさして重要でないので無視して いい。重要なのはNODE_CFUNCNODE_SCOPE、それにNODE_ZSUPERだけだ。

PUSH_FRAME()

PUSH_FRAME() POP_FRAME()

 536  #define PUSH_FRAME() do {               \
 537      struct FRAME _frame;                \
 538      _frame.prev = ruby_frame;           \
 539      _frame.tmp  = 0;                    \
 540      _frame.node = ruby_current_node;    \
 541      _frame.iter = ruby_iter->iter;      \
 542      _frame.cbase = ruby_frame->cbase;   \
 543      _frame.argc = 0;                    \
 544      _frame.argv = 0;                    \
 545      _frame.flags = FRAME_ALLOCA;        \
 546      ruby_frame = &_frame

 548  #define POP_FRAME()                     \
 549      ruby_current_node = _frame.node;    \
 550      ruby_frame = _frame.prev;           \
 551  } while (0)

(eval.c)

まずFRAMEがスタックベタ置き確保であることを確認したい。ここは module_setup()と同じだ。あとは基本的に普通の初期化をしているだけである。

一つだけ付け加えるなら、FRAME_ALLOCAというフラグがFRAMEの 割り当てかたを示していること、くらいだろうか。FRAME_ALLOCAとは もちろん「スタック上にある」ことを示している。

rb_call0()NODE_CFUNC

ここのコードには本物を見るといろいろ書いてあるのだが、ほとんどが trace_func関係なので実質的なコードは以下の一行で終わりだ。

rb_call0()NODE_CFUNC(簡約版)

case NODE_CFUNC:
  result = call_cfunc(body->nd_cfnc, recv, len, argc, argv);
  break;

ではcall_cfunc()はと言うと……

call_cfunc()(簡約版)

4394  static VALUE
4395  call_cfunc(func, recv, len, argc, argv)
4396      VALUE (*func)();
4397      VALUE recv;
4398      int len, argc;
4399      VALUE *argv;
4400  {
4401      if (len >= 0 && argc != len) {
4402          rb_raise(rb_eArgError, "wrong number of arguments(%d for %d)",
4403                   argc, len);
4404      }
4405
4406      switch (len) {
4407        case -2:
4408          return (*func)(recv, rb_ary_new4(argc, argv));
4409          break;
4410        case -1:
4411          return (*func)(argc, argv, recv);
4412          break;
4413        case 0:
4414          return (*func)(recv);
4415          break;
4416        case 1:
4417          return (*func)(recv, argv[0]);
4418          break;
4419        case 2:
4420          return (*func)(recv, argv[0], argv[1]);
4421          break;
                :
                :
4475        default:
4476          rb_raise(rb_eArgError, "too many arguments(%d)", len);
4477          break;
4478      }
4479      return Qnil;                /* not reached */
4480  }

(eval.c)

このように、引数の数に応じて分岐するだけだ。 ちなみに引数の最大個数は15である。

一つ注意してほしいのは、NODE_CFUNCのときにはSCOPEVARSを積んでいない ことだ。メソッドがCで定義されていればRubyのローカル変数は使わないので 当然と言えば当然である。しかしそれは同時にCから「現在の」ローカル変数 にアクセスすると一つ前のFRAMEのローカル変数が見えてしまうということを 意味している。そして実際にそれをやっているところもある。例えば rb_svareval.c)など。

rb_call0()NODE_SCOPE

NODE_SCOPE即ちRubyで定義したメソッドの起動である。 Rubyの基礎をなす部分だ。

rb_call0()NODE_SCOPE(概形)

4568  case NODE_SCOPE:
4569    {
4570        int state;
4571        VALUE *local_vars;  /* OK */
4572        NODE *saved_cref = 0;
4573
4574        PUSH_SCOPE();
4575
            /* (A)CREFの伝達 */
4576        if (body->nd_rval) {
4577            saved_cref = ruby_cref;
4578            ruby_cref = (NODE*)body->nd_rval;
4579            ruby_frame->cbase = body->nd_rval;
4580        }
            /* (B)ruby_scope->local_varsの初期化 */
4581        if (body->nd_tbl) {
4582            local_vars = TMP_ALLOC(body->nd_tbl[0]+1);
4583            *local_vars++ = (VALUE)body;
4584            rb_mem_clear(local_vars, body->nd_tbl[0]);
4585            ruby_scope->local_tbl = body->nd_tbl;
4586            ruby_scope->local_vars = local_vars;
4587        }
4588        else {
4589            local_vars = ruby_scope->local_vars = 0;
4590            ruby_scope->local_tbl  = 0;
4591        }
4592        b2 = body = body->nd_next;
4593
4594        PUSH_VARS();
4595        PUSH_TAG(PROT_FUNC);
4596
4597        if ((state = EXEC_TAG()) == 0) {
4598            NODE *node = 0;
4599            int i;

                /* ……(C)引数をローカル変数に代入…… */

4666            if (trace_func) {
4667                call_trace_func("call", b2, recv, id, klass);
4668            }
4669            ruby_last_node = b2;
                /* (D)メソッド本体 */
4670            result = rb_eval(recv, body);
4671        }
4672        else if (state == TAG_RETURN) { /* returnで戻った */
4673            result = prot_tag->retval;
4674            state = 0;
4675        }
4676        POP_TAG();
4677        POP_VARS();
4678        POP_SCOPE();
4679        ruby_cref = saved_cref;
4680        if (trace_func) {
4681            call_trace_func("return", ruby_last_node, recv, id, klass);
4682        }
4683        switch (state) {
4684          case 0:
4685            break;
4686
4687          case TAG_RETRY:
4688            if (rb_block_given_p()) {
4689               JUMP_TAG(state);
4690            }
4691            /* fall through */
4692          default:
4693            jump_tag_but_local_jump(state);
4694            break;
4695        }
4696    }
4697    break;

(eval.c)

(A)前章の定数のところで話したCREFの伝達を行う。 つまりメソッドエントリからFRAMEcbaseを移植する。

(B)ここの内容はmodule_setup()でやったのと全く同じである。 SCOPElocal_varsに配列を割り当ててやる。これとPUSH_SCOPE()PUSH_VARS()でローカル変数のスコープ生成が完了した。これ以後は メソッド内部と全く同じ環境でrb_eval()することができる。

(C)受け取った引数をメソッドのパラメータ変数にセットしていく。 パラメータ変数とはようするにローカル変数と同じものだ。引数の 数などがNODE_ARGSで指定されているから地道にセットしてやればいい。 詳しいことはこのあとすぐに説明しよう。そして、

(D)メソッド本体を実行する。当然ながらレシーバ(recv)をselfにする。 つまりrb_eval()の第一引数にする。これでメソッドは完全に起動した。

引数のセット

ではすっとばした引数セット部分を詳しく見ていくが、 その前にまずメソッドの構文木をもう一度見てほしい。

% ruby -rnodedump -e 'def m(a) nil end'
NODE_SCOPE
nd_rval = (null)
nd_tbl = 3 [ _ ~ a ]
nd_next:
    NODE_BLOCK
    nd_head:
        NODE_ARGS
        nd_cnt  = 1
        nd_rest = -1
        nd_opt = (null)
    nd_next:
        NODE_BLOCK
        nd_head:
            NODE_NEWLINE
            nd_file = "-e"
            nd_nth  = 1
            nd_next:
                NODE_NIL
        nd_next = (null)

NODE_ARGSがメソッドのパラメータを指定するノードだ。 いくつかダンプしまくってみたところ、そのメンバは以下のように 使われているようだ。

nd_cnt通常のパラメータの数。
nd_restrestパラメータの変数IDrestパラメータがなければ-1
nd_optオプションパラメータのデフォルト値を表現する構文木が入っている。NODE_BLOCKのリスト。

これだけの情報があれば各パラメータ変数に対応するローカル変数IDが一意に 決まる。まず0と1は常に$_$~だった。その次の2から通常のパラメータ がその数だけ並ぶ。そのまた次にオプションパラメータが並ぶ。オプションパ ラメータの数はNODE_BLOCKの長さでわかる。そのまた後にrestパラメータが来 る。

例えば次のような定義をすると、

def m(a, b, c = nil, *rest)
  lvar1 = nil
end

ローカル変数IDは次のように割り当てられる。

0   1   2   3   4   5      6
$_  $~  a   b   c   rest   lvar1

いいだろうか。ではこれを踏まえてコードを見てみよう。

rb_call0()NODE_SCOPE−引数の代入

4601  if (nd_type(body) == NODE_ARGS) { /* 本体なし */
4602      node = body;           /* NODE_ARGS */
4603      body = 0;              /* メソッド本体 */
4604  }
4605  else if (nd_type(body) == NODE_BLOCK) { /* 本体がある */
4606      node = body->nd_head;  /* NODE_ARGS */
4607      body = body->nd_next;  /* メソッド本体 */
4608  }
4609  if (node) {  /* なんらかのパラメータがある */
4610      if (nd_type(node) != NODE_ARGS) {
4611          rb_bug("no argument-node");
4612      }
4613
4614      i = node->nd_cnt;
4615      if (i > argc) {
4616          rb_raise(rb_eArgError, "wrong number of arguments(%d for %d)",
4617                   argc, i);
4618      }
4619      if (node->nd_rest == -1) {  /* restパラメータがない */
              /* パラメータの数を数える */
4620          int opt = i;   /* パラメータの数(iはnd_cnt) */
4621          NODE *optnode = node->nd_opt;
4622
4623          while (optnode) {
4624              opt++;
4625              optnode = optnode->nd_next;
4626          }
4627          if (opt < argc) {
4628              rb_raise(rb_eArgError,
4629                  "wrong number of arguments(%d for %d)", argc, opt);
4630          }
              /* rb_call0では二回目の代入 */
4631          ruby_frame->argc = opt;
4632          ruby_frame->argv = local_vars+2;
4633      }
4634
4635      if (local_vars) { /* パラメータがある */
4636          if (i > 0) {             /* 通常のパラメータがある */
4637              /* $_と$~の領域をよけるために+2 */
4638              MEMCPY(local_vars+2, argv, VALUE, i);
4639          }
4640          argv += i; argc -= i;
4641          if (node->nd_opt) {      /* オプションパラメータがある */
4642              NODE *opt = node->nd_opt;
4643
4644              while (opt && argc) {
4645                  assign(recv, opt->nd_head, *argv, 1);
4646                  argv++; argc--;
4647                  opt = opt->nd_next;
4648              }
4649              if (opt) {
4650                  rb_eval(recv, opt);
4651              }
4652          }
4653          local_vars = ruby_scope->local_vars;
4654          if (node->nd_rest >= 0) { /* restパラメータがある */
4655              VALUE v;
4656
                  /* 余っている引数を配列化して変数に代入 */
4657              if (argc > 0)
4658                  v = rb_ary_new4(argc,argv);
4659              else
4660                  v = rb_ary_new2(0);
4661              ruby_scope->local_vars[node->nd_rest] = v;
4662          }
4663      }
4664  }

(eval.c)

今までより多めにコメントを入れておいたのでそれを見ながら地道に追っても らえればやっていることはわかるだろう。

一つ気になったのはruby_frameargcargvのことである。restパラメータが ないときだけ更新しているようなのだが、どうしてrestパラメータがないとき だけでいいのだろうか。

この点はargcargvの使い道を考えるとわかる。このメンバは実は 引数を省略したsuperのためにあるのだ。つまり以下のような形式だ。

super

こちらのsuperには現在実行中のメソッドのパラメータをそのまま引き渡す働 きがある。そのときに渡せるようにruby_frame->argvに引数を保存しておくわ けだ。

ここで元の話に戻ると、restパラメータがあるときにはsuperでは元の引数リ ストを渡すほうがなんとなく都合がよさそうな気がする。ないときには、オプ ションパラメータが代入されたあとのもののほうがよさそうだ。

def m(a, b, *rest)
  super     # たぶん5, 6, 7, 8を渡すべき
end
m(5, 6, 7, 8)

def m(a, b = 6)
  super     # たぶん5, 6を渡すべき
end
m(5)

これは「そうでなければならない」というよりは仕様としてどちらがいいか、 という問題である。メソッドにrestパラメータがあるならそのスーパークラス でも同じくrestパラメータがあると考えられるから、まとめた後の値を渡すと 不便になる可能性が非常に高いのだ。

さて、いろいろ言ったがメソッドの起動の話はこれで全ておしまいだ。 あとはこの章の締めくくりとしていま話題にしたsuperの実装を見ておく ことにしよう。

super

superに対応するのはNODE_SUPERNODE_ZSUPERである。 NODE_SUPERが普通のsuperで、 NODE_ZSUPERは引数指定なしのsuperだ。

rb_eval()NODE_SUPER

2780        case NODE_SUPER:
2781        case NODE_ZSUPER:
2782          {
2783              int argc; VALUE *argv; /* used in SETUP_ARGS */
2784              TMP_PROTECT;
2785
                  /*(A)superが禁止されている場合 */
2786              if (ruby_frame->last_class == 0) {
2787                  if (ruby_frame->orig_func) {
2788                      rb_name_error(ruby_frame->last_func,
2789                                    "superclass method `%s' disabled",
2790                                    rb_id2name(ruby_frame->orig_func));
2791                  }
2792                  else {
2793                      rb_raise(rb_eNoMethodError,
                                   "super called outside of method");
2794                  }
2795              }
                  /*(B)引数の準備または評価 */
2796              if (nd_type(node) == NODE_ZSUPER) {
2797                  argc = ruby_frame->argc;
2798                  argv = ruby_frame->argv;
2799              }
2800              else {
2801                  BEGIN_CALLARGS;
2802                  SETUP_ARGS(node->nd_args);
2803                  END_CALLARGS;
2804              }
2805
                  /*(C)まだ謎のPUSH_ITER() */
2806              PUSH_ITER(ruby_iter->iter?ITER_PRE:ITER_NOT);
2807              SET_CURRENT_SOURCE();
2808              result = rb_call(RCLASS(ruby_frame->last_class)->super,
2809                               ruby_frame->self, ruby_frame->orig_func,
2810                               argc, argv, 3);
2811              POP_ITER();
2812          }
2813          break;

(eval.c)

引数指定なしのsuperではruby_frame->argvをそのまま引数にすると 言ったが、それが(B)にそのまま出ている。

(C)rb_call()を呼ぶ直前にPUSH_ITER()している。これもまた詳しくは説 明できないのだが、こうしておくと現在のメソッドに渡されたブロックをそっ くりそのまま次のメソッド(つまりこれから呼ぶスーパークラスのメソッド) に委譲できる。

そして最後に(A)ruby_frame->last_classが0であるときはsuperの 呼び出しを禁止しているようだ。エラーメッセージには 「must be enabled by rb_enable_super()」 とあるので、rb_enable_super()を呼べば無事呼べるようになるらしい。 どうしてだろう。

まずlast_classが0になるときはどういうときか調べてみると、どうも実体が Cで定義されているメソッド(NODE_CFUNC)を実行中だとそうなるようだ。 さらにそういうメソッドをaliasしたり置き換えたりしたときも同じだ。

そこまではわかったのだが、ソースコードを読んでみてもどうしてもその続き がわからない。わからないので仕方なくrubyのメーリングリストのログを 「rb_enable_super」で検索して発見した。そのメールによると、次のような ことらしい。

例えばString.newというメソッドがある。もちろん、文字列を生成するメソッ ドである。String.newT_STRINGな構造体を作る。従ってStringの インスタンスメソッドはレシーバが常にT_STRINGであると期待して書いていい。

さて、String.newsuperObject.newである。Object.newT_OBJECTな構造体を作る。ではString.newを置き換え定義してsuperして しまったらどうなるだろう。

def String.new
  super
end

結果として、構造体はT_OBJECTなのにクラスがString、というオブジェクトが できる。しかしStringのメソッドはT_STRINGの構造体を期待して書いているの で当然落ちる。

それを避けるにはどうしたらいいかと言うと、違う構造体型を期待しているメ ソッドは呼べないようにすればよい。しかしメソッドには「期待する構造体型」 なんて情報は付いていないし、クラスにもない。例えばStringクラスから T_STRINGを取る方法があれば呼ぶ前にチェックできそうだが、そういうこと は現時点ではできない。だから次善の策として「Cで定義したメソッドからの superは禁止」ということにしてあるのだ。それなら、Cレベルのメソッド階層 をきっちり作っておけばとりあえず落とすことはできない。そうして、「絶対 に大丈夫だからsuperさせてほしい」という場合はrb_enable_super()を 呼んでおけばsuperできるようになる。

ようするに問題の本質は構造体型ミスマッチである。 アロケーションフレームワークで起こった問題と同じだ。

それで解決策としてはどうすればいいかと言うと、問題の根源である「クラ スがインスタンスの構造体型を知らない」という点を解決すればよい。だがそ のためには最低でも新しいAPIが必要になるし、もっと徹底的にやるなら互換性 がなくなる。そういうわけでいまのところはまだ最終的な解決策は決まってい ない。


御意見・御感想・誤殖の指摘などは 青木峰郎 <aamine@loveruby.net> までお願いします。

『Rubyソースコード完全解説』 はインプレスダイレクトで御予約・御購入いただけます (書籍紹介ページへ飛びます)。

Copyright (c) 2002-2004 Minero Aoki, All rights reserved.