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

問題となっている cref は iseq->cref_stack から来ている。どういう経緯でここに Object クラスが設定されたのかを知りたい。


手始めに iseq->cref_stack を初期化しているところを探してみる。cref_stack という文字列でファイル検索をかけると 20 箇所ほどヒットするが、その中でも iseq->cref_stack = NEW_BLOCK... という2つの行がいかにもそれらしい。両方とも iseq.c にあるので、このファイルをエディタで開いてみる。ありがたいことに、2行とも同じ関数内にあった。

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;
    iseq->defined_method_id = 0;
    iseq->filename = filename;
    iseq->iseq_mark_ary = rb_ary_new();
    RBASIC(iseq->iseq_mark_ary)->klass = 0;

    iseq->type = type;
    iseq->arg_rest = 0;
    iseq->arg_block = 0;
    iseq->klass = 0;
    iseq->special_block_builder = GC_GUARDED_PTR_REF(block_opt);
    iseq->cached_special_block_builder = 0;
    iseq->cached_special_block = 0;

    /* set class nest stack */
    if (type == ISEQ_TYPE_TOP) {
	/* toplevel is private */
	iseq->cref_stack = NEW_BLOCK(th->top_wrapper ? th->top_wrapper : rb_cObject);
	iseq->cref_stack->nd_file = 0;
	iseq->cref_stack->nd_visi = NOEX_PRIVATE;
    }
    else if (type == ISEQ_TYPE_METHOD || type == ISEQ_TYPE_CLASS) {
	iseq->cref_stack = NEW_BLOCK(0); /* place holder */
	iseq->cref_stack->nd_file = 0;
    }
......

(iseq.c r12003)

prepare_iseq_build() という関数名と、やっていることからして、iseq を初期化している関数のようだ。
この関数にブレークポイントをかけて、動作を調べてみよう。

(gdb) b prepare_iseq_build
Breakpoint 1 at 0x80dc525: file iseq.c, line 110.
(gdb) r test_out.rb
Starting program: /home/g-squid/projects/ruby-trunk/ruby-trunk test_out.rb
Reading symbols from shared object read from target memory...done.
Loaded system supplied DSO at 0x25a000
[Thread debugging using libthread_db enabled]
[New Thread -1208576320 (LWP 1803)]
[Switching to Thread -1208576320 (LWP 1803)]

Breakpoint 1, prepare_iseq_build (iseq=0x934bd68, name=3085606100, 
    filename=3085606620, parent=0, type=3, block_opt=0, option=0x814279c)
    at iseq.c:110
110         rb_thread_t *th = GET_THREAD();
(gdb) 

実行が止まった。が、この iseq が目的の iseq かどうか分からない。とりあえず実行を続けて、何回この関数が呼ばれるか調べてみると、なんと8回も呼ばれてしまった。さて、このうちのどれが目的の iseq だろう?どうにかして、違いを見つけ出さないといけない。
コードを見てみると、name という変数があるのに気付く。VALUE 型だが、これが iseq の名前だったりしないだろうか。name の中身を調べてみよう。

(gdb) r
warning: cannot close "shared object read from target memory": File in wrong format
Starting program: /home/g-squid/projects/ruby-trunk/ruby-trunk test_out.rb
Reading symbols from shared object read from target memory...done.
Loaded system supplied DSO at 0xbb9000
[Thread debugging using libthread_db enabled]
[New Thread -1208097088 (LWP 1870)]
[Switching to Thread -1208097088 (LWP 1870)]

Breakpoint 1, prepare_iseq_build (iseq=0x9862d68, name=3086085360, 
    filename=3086085880, parent=0, type=3, block_opt=0, option=0x814279c)
    at iseq.c:110
110         rb_thread_t *th = GET_THREAD();
(gdb) p/x *(struct RBasic*)name
$1 = {flags = 0xc007, klass = 0xb7f55b74}
(gdb) 

struct RBasic* でキャストするのは VALUE 型の変数の中身を調べるときの常套手段だ。flags の値が 0xc007 になっている。flags の下位6ビットが組み込みクラスの型を表わすので(RHG 第2章 basic.flagsの用途 を参照)、0x7 が何か調べれば、どのクラスかが分かる。組み込みクラスの ID は ruby.h に T_XXX というマクロで定義されている。

#define T_STRING 0x07

(ruby.h r12003)

String クラスだった。これは本当に名前が文字列で入っていそうだ。

C のレベルで Ruby の String オブジェクトの文字列を見るためには、rb_str_ptr() という関数を使う。

(gdb) p rb_str_ptr(name)
$3 = 0xb7f1ecf8 "
" (gdb)

"

" という文字列が返ってきた。どうやらこれはトップレベルの命令列のようだ。念のため、test_out.rb のアセンブリコードを調べてみよう。

== disasm: @test_out.rb>====================================
0000 putnil           
0001 putnil           
0002 defineclass      :A, , 0                                (   1)
0006 pop              
0007 getinlinecache   , 14                                        (   5)
0010 getconstant      :A
0012 setinlinecache   7
0014 definemethod     :test_out, test_out, 1
0018 getinlinecache   , 25                                        (   9)
0021 getconstant      :A
0023 setinlinecache   18
0025 send             :test_out, 0, nil, 0, 
0031 leave            
== disasm: @test_out.rb>=================================
0000 putstring        "Hello"                                         (   2)
0002 dup              
0003 setclassvariable :@@a
0005 leave            
== disasm: ==================================
0000 putnil                                                           (   6)
0001 getclassvariable :@@a
0003 send             :puts, 1, nil, 8, 
0009 leave     

最初の命令列のところに、

という文字列が見える。確かに、"
" という文字列がトップレベルの iseq の名前ということで正しいようだ。


ところで、目的の命令列はアセンブリコードの一番最後の、getclassvariable :@@a を含む命令列だ。ということは、"test_out" という名前の iseq が目的の命令列に違いない。prepare_iseq_build() 関数が呼ばれるたびに name の文字列を調べてみよう。
何回か繰り返しているうちに、"test_out" という文字列が見つかった。

(gdb) c
Continuing.

Breakpoint 1, prepare_iseq_build (iseq=0x989d550, name=3086082140, 
    filename=3086082400, parent=0, type=5, block_opt=0, option=0x814279c)
    at iseq.c:110
110         rb_thread_t *th = GET_THREAD();
(gdb) p rb_str_ptr(name)
$9 = 0xb7f1e064 "test_out"
(gdb) 

これが目的の iseq に違いない。ステップして cref_block が初期化されている所まで実行してみる。
実行してみると、2番目の NEW_BLOCK の文で初期化されていることが分かった。

    else if (type == ISEQ_TYPE_METHOD || type == ISEQ_TYPE_CLASS) {
	iseq->cref_stack = NEW_BLOCK(0); /* place holder */
	iseq->cref_stack->nd_file = 0;
    }

(iseq.c r12003)

この iseq は test_out メソッド定義のための命令列だから、type の値はおそらく ISEQ_TYPE_METHOD なのだろう。

* iseq->cref_stack の初期化場所を調べる

引き続き、NEW_BLOCK.. の行まで実行して、iseq->cref_stack の値を調べてみる。

(gdb) p/x *iseq->cref_stack
$10 = {flags = 0x4841f, nd_file = 0x9864661, u1 = {node = 0x0, id = 0x0, 
    value = 0x0, cfunc = 0x0, tbl = 0x0}, u2 = {node = 0x0, id = 0x0, 
    argc = 0x0, value = 0x0}, u3 = {node = 0x0, id = 0x0, state = 0x0, 
    entry = 0x0, cnt = 0x0, value = 0x0}}
(gdb) 

iseq->cref_stack の型 NODE は union を多用しているので、どれがクラスの値を持つのかさっぱり見当がつかない。
eval_get_cvar_base() のソースを見てみると、クラスの値は、cref->nd_clss で参照されていることが分かる。

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;
 ......

(vm.c r12003)

しかし、デバッガの出力を見てみても、iseq->cref_stack に nd_clss などというメンバは見当たらない。実は、nd_clss はマクロなのだ。ファイル検索で "nd_clss" を探してみると、node.h の中でこのマクロが定義されているのが分かる。

#define nd_clss  u1.value

これで、u1.value にクラスの値が入っていることが分かった。そこで、さっきの iseq->cref_stack の値を調べてみると、u1.value は 0 になっている。初期化しているようだから、値が 0 になっているのは理解できる。しかし、それでは Object クラスはどこで代入されるのだろう?


このままデバッガで馬鹿正直にコードを追っていっても、Object クラスを代入している箇所に行き当たる保証はない。ソースコードを検索してみても、cref_stack に値を代入している箇所は山ほどある。こういうときには目的の変数にウォッチポイントを設定して、変数の値が変更された瞬間に実行を止めるに限る。

(gdb) p &iseq->cref_stack->u1.value
$23 = (VALUE *) 0xb7e99008
(gdb) watch *(VALUE*)0xb7e99008
Hardware watchpoint 3: *(long unsigned int *) 3085537288
(gdb)

iseq->cref_stack->u1.value に直接ウォッチポイントを設定しないのは、それではウォッチポイントがうまく動作しないからだ。
gdb は賢くて(?)、C の変数名とそのスコープを理解しているので、変数のスコープを抜けると変数名で定義したウォッチポイントは使えなくなってしまう。
直接メモリアドレスを使うとこういう問題は起こらないので、わざわざアドレスに対して直接ウォッチポイントを指定するわけだ。*1
さて、実行してみよう。

(gdb) c
Continuing.
Hardware watchpoint 3: *(long unsigned int *) 3085537288

Old value = 0
New value = 3085794520
0x080e815a in eval_define_method (th=0x9f1c2e0, obj=3085537080, id=11128, 
    miseq=0x9f22550, is_singleton=1, cref=0xb7e990f0) at vm.c:1275
1275        COPY_CREF(miseq->cref_stack, cref);

うまいことウォッチポイントに引っかかってくれたようだ。miseq->cref_stack が目的の cref_stack のようだが、念のためアドレスを確認してみよう。

(gdb) p &miseq->cref_stack->u1.value
$26 = (VALUE *) 0xb7e99008

このアドレスはウォッチポイントで指定したアドレスと同じだ。確かにこれが目的の変数らしい。それでは、この変数に代入された値は何だろうか?

(gdb) p *(struct RBasic*)miseq->cref_stack->u1.value
$31 = {flags = 3, klass = 3085794300}
(gdb) p rb_class2name(miseq->cref_stack->u1.value)
$32 = 0xb7e98e8c "Object"
(gdb) 

flags で値がクラスであることを確かめて、クラス名を取ってくる、いつものパターンだ。
Object クラスが代入されたことが分かる。どうやら問題の箇所に辿り着いたようだ。ソースコードを見てみよう。

eval_define_method(rb_thread_t *th, VALUE obj,
		   ID id, rb_iseq_t *miseq, num_t is_singleton, NODE *cref)
{
    NODE *newbody;
    int noex = cref->nd_visi;
    VALUE klass = cref->nd_clss;

    if (is_singleton) {
	if (FIXNUM_P(obj) || SYMBOL_P(obj)) {
	    rb_raise(rb_eTypeError,
		     "can't define singleton method \"%s\" for %s",
		     rb_id2name(id), rb_obj_classname(obj));
	}

	if (OBJ_FROZEN(obj)) {
	    rb_error_frozen("object");
	}

	klass = rb_singleton_class(obj);
	noex = NOEX_PUBLIC;
    }

    /* dup */
    COPY_CREF(miseq->cref_stack, cref);
    miseq->klass = klass;
.......

(vm.c r12003)

eval_define_method() 関数だ。おそらく、test_out メソッドを定義する過程で Object が代入されているに違いない。問題の cref_stack は、関数の引数で渡された変数 cref のコピーだ。では、cref はどこから来ているのか? デバッガで調べてみよう。

(gdb) up
#1  0x080e3cb3 in th_eval (th=0x9f1c2e0, 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) 

up コマンドでコールスタックを一つ遡る。l コマンドでソースを表示する。
先ほどの cref の値は get_cref(GET_ISEQ(), GET_LFP()) で返っているようだ。get_cref() はこの場合、GET_ISEQ() で返った iseq の cref_stack を返す。GET_ISEQ() で返る iseq は definemethod 命令(837 行目にある)が属する命令列だ。ここでもう一度 test_out.rb のアセンブリコードを見てみよう。

== disasm: @test_out.rb>====================================
0000 putnil           
0001 putnil           
0002 defineclass      :A, , 0                                (   1)
0006 pop              
0007 getinlinecache   , 14                                        (   5)
0010 getconstant      :A
0012 setinlinecache   7
0014 definemethod     :test_out, test_out, 1
0018 getinlinecache   , 25                                        (   9)
0021 getconstant      :A
0023 setinlinecache   18
0025 send             :test_out, 0, nil, 0, 
0031 leave            
== disasm: @test_out.rb>=================================
0000 putstring        "Hello"                                         (   2)
0002 dup              
0003 setclassvariable :@@a
0005 leave            
== disasm: ==================================
0000 putnil                                                           (   6)
0001 getclassvariable :@@a
0003 send             :puts, 1, nil, 8, 
0009 leave     

definemethod は

命令列、すなわちトップレベルで呼ばれている。つまり、問題の Object クラスを持つ cref はトップレベルの命令列の cref_stack から来ていることになる。
そこで、
命令列の cref_stack がどのように初期化されている調べてみよう。
prepare_iseq_build() 関数で cref_stack が初期化されることはさっき調べたので、この関数にブレークポイントを設定して、
命令列の場合の動作を調べてみる。
今度は test_out 命令列のときと違って、cref_block は最初の NEW_BLOCK... で初期化された。

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();
.................
    /* set class nest stack */
    if (type == ISEQ_TYPE_TOP) {
	/* toplevel is private */
	iseq->cref_stack = NEW_BLOCK(th->top_wrapper ? th->top_wrapper : rb_cObject);
	iseq->cref_stack->nd_file = 0;
	iseq->cref_stack->nd_visi = NOEX_PRIVATE;
    }
    else if (type == ISEQ_TYPE_METHOD || type == ISEQ_TYPE_CLASS) {
	iseq->cref_stack = NEW_BLOCK(0); /* place holder */

(iseq.c r12003)

最初の NEW_BLOCK の文が呼ばれているので、type が ISEQ_TYPE_TOP なことが分かる。そして、th->top_wrapper の値が 0 なので、rb_cObject がクラスとして使われている。rb_cObject は、Cレベルで Object クラスを表わす変数だ。ここでようやく Object クラスがでてきた。
つまり、トップレベルの命令列の場合は、th->top_wrapper が定義されてない限り、cref_stack に Object クラスを使うことになる。わざわざトップレベルの場合だけ条件分けしてあるから、これは仕様だ。


ここで最初の質問に戻ろう。

一つしかないクラスネスト(つまりネストしていない)、しかもそのクラスが Object というのはどういうことか。

警告メッセージから推して、クラスネストのトップレベルのクラスが Object であることが想像できるが、これはあくまでも想像だ。真実を知るためには、この cref に Object クラスが代入される現場(コード)を直接押さえるのが確実だ。

これまでソースを調べた結果、答えは以下のようになる。


クラス外で定義されたクラスメソッドの cref はそのメソッドが定義された「場所」のクラスを持つ。A.test_out はトップレベルで定義されているので、そのクラスは Object ということになる(クラス A ではない)。したがって、クラスがネストせずそのクラスが Object というのは仕様どおり。


いちおうこれで結論が出たわけだが、このコードでクラス変数にアクセスできないというのは、やっぱり納得できない。

class A
  @@a = "Hello"
end

def A.test_out
  puts @@a
end

A.test_out

A.test_out はクラス A のメソッドなのだから*2、クラス A のクラス変数である @@a にアクセスできると期待するのは自然だと思う。


これを実現しようとすると、これまでのクラス変数のスコープに関するルールである「クラス変数は、それが現れた場所のクラスに属する」というルールを根本的に変える必要があるが、それをやる価値があると思う。2.0 あたりでどうでしょう、まつもとさん?

*1:この方法では、そのメモリが free されて再利用されたときに問題が起こるので、この方法がいつでもうまくいくわけではない。

*2:厳密に言えば間違い。クラスメソッドは特異クラスのメソッドだ。しかし、クラスメソッドはそのクラスのメソッドではないというのは、かなりややこしい話だ。