クラスメソッドからクラス変数を参照 - class_eval で定義されたクラスメソッドの場合 (ruby 1.9) [その1]

前回「最後のパターン」などと書いたが、ドラえもん巨人の星で分かる通り、「最後」というのは当てにならない。ということで、今回もクラスメソッドとクラス変数の話が続く。


今回は Module#class_eval の中で定義したクラスメソッドの中からクラス変数を参照する場合を調べる。
Rubyリファレンスマニュアルによると、Module#class_eval は「モジュールのコンテキストで文字列 expr を評価してその結果を返」す。
クラスではなくモジュールのコンテキストとなっているが、Class は Module のサブクラスなので同じことだ。以降はクラスのコンテキストと呼ぶことにする。
クラスのコンテキストというのがちょっと分かりにくいが、これは実際にプログラムを見たほうが分かりやすい。

今回調べる Ruby のプログラムは以下の通り。

class A
  @@a = "Hello"
end

A.class_eval(<<EOF)
  def A.foo
    puts @@a
  end
EOF

A.foo

(test_class_eval.rb)

クラスメソッド A.foo を定義する文字列を、ヒアドキュメント形式で作って A.class_eval に渡している。こうすると、A.foo が class A の内部で定義されたのと同じように定義される。これが「クラスのコンテキスト」が意味していることだ。


このプログラムを ruby 1.9 で実行すると以下のような結果になる。

$ ruby test_class_eval.rb
Hello

無事、A.foo の中から @@a が参照できているようだ。さて、内部では何が起こっているのだろうか。


例によって、クラス変数を参照している部分、VM 命令で言えば getclassvariable から始める。getclassvariable にブレークポイントをかけて、クラス A のクラス変数 @@a を参照している部分を調べてみよう。

$ gdb ../ruby-trunk
GNU gdb Red Hat Linux (6.3.0.0-1.21rh)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db library "/lib/libthread_db.so.1".

(gdb) b insns.def:218
Breakpoint 1 at 0x80e29cc: file insns.def, line 218.
(gdb) r test_class_eval.rb
Starting program: /home/g-squid/projects/ruby-trunk/ruby-trunk test_class_eval.rb
Reading symbols from shared object read from target memory...done.
Loaded system supplied DSO at 0xd1c000
[Thread debugging using libthread_db enabled]
[New Thread -1209071936 (LWP 24060)]
[New Thread -1209074768 (LWP 24063)]
[Switching to Thread -1209071936 (LWP 24060)]

Breakpoint 1, th_eval (th=0x95f82e0, initial=0) at insns.def:218
218         VALUE klass = eval_get_cvar_base(th, GET_ISEQ());
(gdb) n
219         val = rb_cvar_get(klass, id);
(gdb) p *(struct RClass*)klass     # オブジェクトのフラグを確認
$1 = {basic = {flags = 3, klass = 3085107140}, iv_tbl = 0x95fe490, 
  m_tbl = 0x95c6270, super = 3085364440}       # flags = 3 なので、特異クラスではない
(gdb) p rb_class2name(klass)            # klass の名前を調べる
$2 = 0xb7e2ffa4 "A"
(gdb) n
373       PUSH(val);
(gdb) p rb_str_ptr(val)
$3 = 0xb7e2ff68 "Hello"

VM 命令 getclassvariable は insnsf.def ファイル (r12010) の 218 行目に定義されているので、そこにブレークポイントを設定して実行した。
eval_get_cvar_base() からは、クラス A が返ってきている。
rb_cvar_get() は、渡されたクラスを起点にクラス変数を探すので、A のクラス変数 @@a の値が val に返ってくることになる。val の値を確認したところ、確かに Ruby の文字列 "Hello" が返っている。
rb_cvar_get() は期待通りに動いているので、eval_get_cvar_base() がどういう仕組みでクラス A を返しているか調べてみよう。


デバッガを最初から走らせて eval_get_cvar_base() の内部を追ってみると、クラスは get_cref() から返った cref の cref->nd_clss であることが分かる。これは cref に積まれたクラスそのものだ。

EVALBODY_HELPER_FUNCTION VALUE
eval_get_cvar_base(rb_thread_t *th, rb_iseq_t *iseq)
{
    NODE *cref = get_cref(iseq, th->cfp->lfp);
    VALUE klass = Qnil;

    if (cref) {
	klass = cref->nd_clss;                   # この値が返る
	if (!cref->nd_next) {
	    rb_warn("class variable access from toplevel");
	}
    }
    if (NIL_P(klass)) {
	rb_raise(rb_eTypeError, "no class variables available");
    }
    return klass;
}


get_cref() では、iseq->cref_stack を cref として返している。

static NODE *
get_cref(rb_iseq_t *iseq, VALUE *lfp)
{
    NODE *cref;
    if (( cref = lfp_get_special_cref(lfp) ) != 0) {   # cref == 0
	/* */
    }
    else if ((cref = iseq->cref_stack) != 0) {       # この cref が返る。
	/* */
    }
    else {
	rb_bug("get_cref: unreachable");
    }
    return cref;
}

(vm.c r12010)

iseq->cref_stack、つまり、命令列 iseq の cref スタックの一番下の cref が返っていることになる。
この値はどこで定義されたものだろうか?


まず iseq が何かを調べよう。iseq->name が iseq の名前を持っている。get_cref() の中までデバッガで実行して、iseq->name の値を調べよう。

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y

 ......

Breakpoint 1, th_eval (th=0x81bf2e0, initial=0) at insns.def:218
218         VALUE klass = eval_get_cvar_base(th, GET_ISEQ());
(gdb) s
eval_get_cvar_base (th=0x81bf2e0, iseq=0x81c5cc0) at vm.c:1236
1236        NODE *cref = get_cref(iseq, th->cfp->lfp);
(gdb) s
get_cref (iseq=0x81c5cc0, lfp=0xb7e2b020) at vm.c:1116
1116        if (( cref = lfp_get_special_cref(lfp) ) != 0) {
(gdb) p rb_str_ptr(iseq->name)
$20 = 0xb7ef5d44 "foo"
(gdb) 

iseq->name は Ruby の String 型、rb_str_ptr() は Ruby の String 型から C の文字列を取り出す関数だ。
この iseq の名前は "foo" だということが分かる。foo は A.class_eval で定義したクラスメソッドの名前なので、この iseq はそのクラスメソッドの本体に違いない。


それでは、この iseq->cref_stack はどこで初期化されているのだろうか?
以前やったのと同じ要領で調べてみる。
まずは prepare_iseq_build() にブレークポイントを設定して、プログラムを最初から走らせる。実行が止まるたびに iseq->name を調べて、"foo" が来るまでこれを繰り返す。

(gdb) b prepare_iseq_build
Breakpoint 2 at 0x80dc529: file iseq.c, line 110.
(gdb) r
 ......
Breakpoint 2, prepare_iseq_build (iseq=0x96fad68, name=3085462740, 
    filename=3085463260, parent=0, type=3, block_opt=0, option=0x81426bc)
    at iseq.c:110
110         rb_thread_t *th = GET_THREAD();
(gdb) p rb_str_ptr(name)
$9 = 0xb7e86cdc "
" (gdb) c Continuing. ...... Breakpoint 2, prepare_iseq_build (iseq=0x9735cc0, name=3085458720, filename=3085459220, parent=0, type=5, block_opt=0, option=0x81426bc) at iseq.c:110 110 rb_thread_t *th = GET_THREAD(); (gdb) p rb_str_ptr(name) $16 = 0xb7e85d28 "foo" (gdb)

7回目に foo が現れた。
このまま cref_stack が初期化されているところまでステップ実行する。

static VALUE
prepare_iseq_build(rb_iseq_t *iseq,
		   VALUE name, VALUE filename,
		   VALUE parent, VALUE type, VALUE block_opt,
		   const rb_compile_option_t *option)
{
    rb_thread_t *th = GET_THREAD();

    iseq->name = name;

 ......

    /* set class nest stack */
    if (type == ISEQ_TYPE_TOP) {
 ......
    }
    else if (type == ISEQ_TYPE_METHOD || type == ISEQ_TYPE_CLASS) {
	iseq->cref_stack = NEW_BLOCK(0); /* place holder */            # ここで初期化
	iseq->cref_stack->nd_file = 0;
    }
    else if (parent) {

 ......

(iseq.c r12010)

見てのとおり、ここでは 0 で初期化しているだけで、実際の値は入らない。実際の値が代入される場所を特定するためにウォッチポイントをかける。やり方は以前出てきたのと同じだ。

(gdb) p &iseq->cref_stack->u1.value
$17 = (VALUE *) 0xb7e85cec
(gdb) watch *(VALUE*)0xb7e85cec
Hardware watchpoint 3: *(long unsigned int *) 3085458668
(gdb) c
Continuing.
Hardware watchpoint 3: *(long unsigned int *) 3085458668

Old value = 0
New value = 3085459420
0x080e8156 in eval_define_method (th=0x972f2e0, obj=3085459420, id=11128, 
    miseq=0x9735cc0, is_singleton=1, cref=0xb7e85f28) at vm.c:1275
1275        COPY_CREF(miseq->cref_stack, cref);
(gdb) 

eval_define_method() で値が代入されているようだ。この cref は引数として渡されたものなので、コールスタックを一つ上がって、この関数の呼び出し元を調べる。

(gdb) up
#1  0x080e3caf in th_eval (th=0x972f2e0, initial=0) at insns.def:842
842         eval_define_method(th, obj, id, body, is_singleton,
(gdb) l
837     definemethod
838     (ID id, ISEQ body, num_t is_singleton)
839     (VALUE obj)
840     ()
841     {
842         eval_define_method(th, obj, id, body, is_singleton,
843                            get_cref(GET_ISEQ(), GET_LFP()));
844     }
845
846
(gdb) 

VM 命令 definemethod から eval_define_method() が呼ばれていて、cref の値は get_cref(GET_ISEQ(), GET_LFP()) の返り値だということが分かる。
ここでは cref の値がどこから来ているのか知りたいので、引き続き、get_cref(GET_ISEQ(), GET_LFP()) の内部動作を追ってみよう。

* get_cref() の返り値を調べる

今度は VM 命令 definemethod にブレークポイントをかけてプログラムを最初から実行しなおす。下の例では、不要になったブレークポイントやウォッチポイントの削除も行っている。

(gdb) b insns.def:842
Breakpoint 4 at 0x80e3c74: file insns.def, line 842.
(gdb) info watch                                                         # ブレークポイント・ウォッチポイントを調べる。
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x080e29cc in th_eval at insns.def:218
2   breakpoint     keep y   0x080dc529 in prepare_iseq_build at iseq.c:110
        breakpoint already hit 8 times
3   hw watchpoint  keep y              *(long unsigned int *) 3085458668
        breakpoint already hit 1 time
4   breakpoint     keep y   0x080e3c74 in th_eval at insns.def:842
(gdb) d 3                                                                # 不要なブレークポイント・ウォッチポイントを削除
(gdb) d 2
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y

 ......

Breakpoint 4, th_eval (th=0x84832e0, initial=0) at insns.def:842
842         eval_define_method(th, obj, id, body, is_singleton,
(gdb) p rb_id2name(id)
$18 = 0xb7e32e48 "synchronize"                                                  # 違うメソッド定義だった。次へ進む。
(gdb) c
Continuing.
[New Thread -1209066576 (LWP 8127)]

Breakpoint 4, th_eval (th=0x84832e0, initial=0) at insns.def:842
842         eval_define_method(th, obj, id, body, is_singleton,
(gdb) p rb_id2name(id)
$19 = 0xb7e321a0 "foo"                                                         # 発見
(gdb) l 
837     definemethod
838     (ID id, ISEQ body, num_t is_singleton)
839     (VALUE obj)
840     ()
841     {
842         eval_define_method(th, obj, id, body, is_singleton,
843                            get_cref(GET_ISEQ(), GET_LFP()));
844     }
845
846
(gdb) 

目的の場所に到達した。ここで 1 ステップすると、get_cref() の中に実行が移る。続けて1行ずつ実行してみよう。

(gdb) s
get_cref (iseq=0x8489980, lfp=0xb7d67014) at vm.c:1116
1116        if ( (cref = lfp_get_special_cref(lfp) ) != 0) {
(gdb) s
lfp_get_special_cref (lfp=0xb7d67014) at vm.c:1050
1050        if (((VALUE)(values = (void *)lfp[-1])) != Qnil && values->basic.klass) {
(gdb) n
1051            return (NODE *)values->basic.klass;
(gdb) n

さっき get_cref を見たときと違って、ここでは lfp_get_special_cref(lfp) が cref を返している。
デバッガの表示では分かりにくいので、get_cref() と lfp_get_special_cref() のソースコードを見てみよう。

static NODE *
get_cref(rb_iseq_t *iseq, VALUE *lfp)
{
    NODE *cref;
    if (( cref = lfp_get_special_cref(lfp) ) != 0) {   # この cref が返る。
	/* */
    }
    else if ((cref = iseq->cref_stack) != 0) {       
	/* */
    }
    else {
	rb_bug("get_cref: unreachable");
    }
    return cref;
}

(vm.c r12010)
static NODE *
lfp_get_special_cref(VALUE *lfp)
{
    struct RValues *values;
    if (((VALUE)(values = (void *)lfp[-1])) != Qnil && values->basic.klass) {
	return (NODE *)values->basic.klass;                                    # この値が返る
    }
    else {
	return 0;
    }
}

(vm.c r12010)

get_cref() は、他の種類のクラスメソッド定義を調べたときに何度も出てきたが、lfp_get_special_cref() が値を返すのは初めてだ。おそらくこれが、Module#class_eval に特有の動作に関係しているに違いない。
今度は、lfp_get_special_cref() が返す cref は何か、この cref はどこから来ているのかを調べる。