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

Module#class_eval で定義されたクラスメソッドの命令列の cref スタックは、通常の場合と違いlfp_get_special_cref() で返される cref スタックとなる。
lfp_get_special_cref() は他のクラスメソッド定義のやり方のときは値を返さなかったので、Module#class_eval で行われる動的評価に関連した特別なことをやっているように思われる。
今回は、lfp_get_special_cref() で得られる cref が、どこで定義されているのか、どう特別なのか、なぜ特別なのかを調べる。


lfp_get_special_cref() の定義は以下のようになっている。

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)

lfpVALUE の配列を指すポインタだ。
if 文は以下のようなことをやっている。
lfp の指す VALUE から一つ前の VALUE の値が Qnil かどうかをまず調べ、Qnil 以外であれば、その値を struct RValues* values と解釈して、values->basic.klass が 0 かどうかをチェックする。もしこの値が 0 でなければ、この値を NODE * 型として返す。


values->basic.klass に cref が入っているようだが、そもそも lfp とは何だろうか?
YARVアーキテクチャによると、lfp (Local Frame Pointer) については、「ローカル変数には、lfp[-X] という形でアクセスします。これらのインデックスはコンパイル時(正確には parse 時)に決定します。」とある。とりあえず、lfp はローカル変数にアクセスするためのレジスタと考えて良さそうだ。


しかし、lpf[-X] がローカル変数へのアクセス方法だとすると、明らかに内部情報である cref を格納しているらしい lfp[-1] がローカル変数というのはおかしくないだろうか? Ruby のプログラムで定義されるローカル変数と衝突したりしないのだろうか?


VM レベルで Ruby プログラムのローカル変数がどのように表現されているかを調べるため、以下のような Ruby プログラムを VM 命令に変換してみた。

a = 2
b = "hey"

(local1.rb)

変換結果は以下の通り

== disasm: @local1.rb>======================================
local scope table (size: 3, argc: 0)
[ 3] a          [ 2] b          
0000 putobject        2                                               (   1)
0002 setlocal         a
0004 putstring        "hey"                                           (   2)
0006 dup              
0007 setlocal         b
0009 leave            

代入すべきオブジェクト (2 と "hey") をスタックにつんで、変数名を引数に setlocal 命令が呼ばれている。setlocal 命令を見てみよう。

DEFINE_INSN
setlocal
(lindex_t idx)
(VALUE val)
()
{
    (*(GET_LFP() - idx)) = val;
}

(insns.def r12010)

引数が idx となっている。 *(GET_LFP() - idx) の部分は lfp[-idx] と読み替えられるから、このコードはYARVアーキテクチャで説明されているローカル変数へのアクセス方法と一致する。
idx は当然整数でないといけないが、VM 命令列を見たときは、setlocal の引数は a や b などの変数名だった。どうやって変数名をインデックスに変換しているのだろうか?
答えは、変換された VM コードにある。先頭に、local scope table というものがあって、その中で変数名と数字の対応付けがされている。この数字が変数のインデックスに違いない。

== disasm: @local1.rb>======================================
local scope table (size: 3, argc: 0)
[ 3] a          [ 2] b          

 ......

実際にデバッガで確認してみよう。

$ 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:74                        # setlocal は insns.def の74行目に定義されている
Breakpoint 1 at 0x80e2591: file insns.def, line 74.
(gdb) r local1.rb

 ......

Breakpoint 1, th_eval (th=0x89c52e0, initial=0) at insns.def:74
74          (*(GET_LFP() - idx)) = val;
(gdb) p idx
$1 = 3                             # idx == 3
(gdb) p rb_fix2int(val)
$2 = 2                             # Ruby の整数 "2"
(gdb) c
Continuing.

Breakpoint 1, th_eval (th=0x89c52e0, initial=0) at insns.def:74
74          (*(GET_LFP() - idx)) = val;
(gdb) p idx
$3 = 2                             # idx == 2
(gdb) p rb_str_ptr(val)
$4 = 0xb7ef02b8 "hey"              # Ruby の文字列 "hey"
(gdb) 

予想通り、local scope table の数字がローカル変数のインデックスを表わしているようだ。この場合、a が lfp[-3], b が lfp[-2] に、それぞれ格納されている。
a も b も、問題の lfp[-1] とは衝突していないが、これは偶然だろうか?もう一つローカル変数を追加して、そのインデックスがどうなるか見てみよう。

a = 2
b = "hey"
c = "ho"

(local2.rb)

VM 命令

== disasm: @local2.rb>======================================
local scope table (size: 4, argc: 0)
[ 4] a          [ 3] b          [ 2] c          
0000 putobject        2                                               (   1)
0002 setlocal         a
0004 putstring        "hey"                                           (   2)
0006 setlocal         b
0008 putstring        "ho"                                            (   3)
0010 dup              
0011 setlocal         c
0013 leave            

今度は 4, 3, 2 がそれぞれ a, b, c のインデックスとなった。
どうやらインデックス 1 は意図的に Ruby レベルでのローカル変数のインデックスとして使われないようになっているようだ。つまり、lfp[-1] は ruby インタープリタ内部のローカル変数格納用として確保されていると思ってよい。


lfp[-1] がインタープリタで使われるローカル変数だと分かったところで、lfp[-1] を初期化している場所を探そう。


初期化のときに lfp[-1] という書き方を使っている保証はまったくないが、lfpVMレジスタの一つであること、lfp[-X] というのがローカル変数へのアクセスの決まりパターンであることから、lfp[-1] で検索をかけてみる価値はありそうだ。
ということでファイル検索をかけてみると以下のような結果が返ってきた。

>Internal search for "lfp[-1]" in "*.c *.h *.y *.ci *.def"
S:\projects\ruby-trunk\vm.c:323:	(!(cfp->lfp[-1] == Qnil ||
S:\projects\ruby-trunk\vm.c:324:	  BUILTIN_TYPE(cfp->lfp[-1]) == T_VALUES))) {
S:\projects\ruby-trunk\vm.c:861:	val = (struct RValues *)lfp[-1];
S:\projects\ruby-trunk\vm.c:864:	    lfp[-1] = (VALUE)val;
S:\projects\ruby-trunk\vm.c:1050:    if (((VALUE)(values = (void *)lfp[-1])) != Qnil && values->basic.klass) {
S:\projects\ruby-trunk\vm.c:1065:	if (cfp->lfp && cfp->lfp[-1] != Qnil &&
S:\projects\ruby-trunk\vm.c:1066:	    TYPE(cfp->lfp[-1]) != T_VALUES) {
S:\projects\ruby-trunk\vm.c:1067:	    /* dp(cfp->lfp[-1]); */
S:\projects\ruby-trunk\vm.c:1077:    struct RValues *values = (void *) lfp[-1];
>

この中で、値を代入しているのは vm.c:864 の1箇所だけのようだ。ソースコードを見てみよう。

854: static VALUE *
855: lfp_svar(VALUE *lfp, int cnt)
856: {
857:     struct RValues *val;
858:     rb_thread_t *th = GET_THREAD();
859: 
860:     if (th->local_lfp != lfp) {
861:         val = (struct RValues *)lfp[-1];
862:         if ((VALUE)val == Qnil) {
863: 	         val = new_value();
864: 	         lfp[-1] = (VALUE)val;
865:         }
866:     }
867:     else {
868:         val = (struct RValues *)th->local_svar;
869:         if ((VALUE)val == Qnil) {
870:             val = new_value();
871:             th->local_svar = (VALUE)val;
872:         }
873:     }

 ......

(vm.c r12010)

lfp_svar() という関数の中で lfp[-1] への代入が行われている。試しにこの関数にブレークポイントを設定して実行してみよう。

(gdb) b lfp_svar
Breakpoint 7 at 0x80e153f: file vm.c, line 858.
(gdb) r

 ......

Breakpoint 7, lfp_svar (lfp=0xb7e04014, cnt=-1) at vm.c:858
858         rb_thread_t *th = GET_THREAD();
(gdb) 

lfp_svar() にやってきた。さらにステップ実行して、lfp[-1] の初期化のコードを通るか確認しよう。

(gdb) n
860         if (th->local_lfp != lfp) {
(gdb) n
861             val = (struct RValues *)lfp[-1];
(gdb) n
862             if ((VALUE)val == Qnil) {
(gdb) n
863                 val = new_value();
(gdb) n
864                 lfp[-1] = (VALUE)val;
(gdb) n
874         switch (cnt) {
(gdb)

lfp[-1] の初期化部分も実行された。
ここまではOKだが、これが本当に問題の lfp[-1] を初期化している部分かは分からない。それを確認するために、lfp[-1] のアドレスを調べよう。

(gdb) p &lfp[-1]
$41 = (VALUE *) 0xb7da0010
(gdb) 

lfp[-1] のアドレスは 0xb7da0010 だ。それでは問題の箇所での lfp[-1] が同じアドレスを指しているかを確認してみよう。
問題の箇所は前回確認したように、メソッド foo を定義するための VM 命令 definemethod から呼ばれた lfp_get_special_cref() だ。definemethod にブレークポイントを設定して、後はステップで lfp_get_special_cref() まで行こう。
definemethod が定義されているのは insns.def の 842 行目だ。

(gdb) b insns.def:842            # definemethod 命令の本体にブレークポイントを設定
Breakpoint 11 at 0x80e3c74: file insns.def, line 842.
(gdb) c
Continuing.

Breakpoint 11, th_eval (th=0xa04a2e0, initial=0) at insns.def:842
842         eval_define_method(th, obj, id, body, is_singleton,
(gdb) p rb_id2name(id)           # メソッド foo の定義であることを確認
$42 = 0xb7e6b1a8 "foo"
(gdb) s
get_cref (iseq=0xa050980, lfp=0xb7da0014) at vm.c:1116
1116        if *1 != Qnil && values->basic.klass) {
(gdb) p &lfp[-1]
$43 = (VALUE *) 0xb7da0010       # lfp_get_special_cref() 内の lfp[-1] のアドレスを確認
(gdb)


ラッキーなことに lfp_svar() で確認したのと同じアドレスだ。これで、問題の lfp[-1] を初期化しているのは lfp_svar() ということが分かった。


lfp[-1] が初期化されている場所は分かったが、まだ cref まで辿り着いていない。次は cref が初期化されている場所を調べる。
ここでもう一度 lfp_get_special_cref() のソースコードを見てみよう。

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)

返り値である cref は values->basic.klass の値であることがわかる。values は lfp[-1] の値だから、lfp_svar() のところで lfp[-1]->basic.klass の値を代入している場所を探せば良い。
プログラムを再実行して、lfp_svar() まで戻ろう。

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

 ......

Breakpoint 11, th_eval (th=0x8a3e2e0, initial=0) at insns.def:842
842         eval_define_method(th, obj, id, body, is_singleton,    # さっき定義した definemethod へのブレークポイントで停止した。無視して先に進む。
(gdb) c
Continuing.
[New Thread -1208783952 (LWP 13816)]

Breakpoint 7, lfp_svar (lfp=0xb7dac014, cnt=-1) at vm.c:858
858         rb_thread_t *th = GET_THREAD();
(gdb) n
860         if (th->local_lfp != lfp) {
(gdb) n
861             val = (struct RValues *)lfp[-1];
(gdb) n
862             if ((VALUE)val == Qnil) {
(gdb) n
863                 val = new_value();
(gdb) n
864                 lfp[-1] = (VALUE)val;
(gdb) n
874         switch (cnt) {
(gdb) 

lfp[-1] が初期化されている所まで実行した。lfp[-1]->basic.klass の値を調べてみよう。

(gdb) p ((struct RValues*)lfp[-1])->basic.klass
$44 = 0

この値を調べるときには、lfp[-1] を RValues 構造体のポインタでキャストする必要がある。
値は 0 だ。この後どこかで初期化されるらしい。
こういうときは、コードを逐一追っていくよりも、変数にウォッチポイントをかけた方が早い。変数のアドレスを調べて、その内容にウォッチポイントを設定する。

(gdb) p &((struct RValues*)lfp[-1])->basic.klass
$45 = (VALUE *) 0xb7e76f40
(gdb) watch *(VALUE*)0xb7e76f40
Hardware watchpoint 12: *(long unsigned int *) 3085397824
(gdb)

早速実行してみよう。

(gdb) c
Continuing.
Hardware watchpoint 12: *(long unsigned int *) 3085397824

Old value = 0
New value = 3085397800
exec_under (func=0x805bc8d , under=3085397980, self=3085397980, 
    args=3220475080) at eval.c:2078
2078        PUSH_TAG(PROT_NONE);
(gdb) l
2073        
2074        pcref = (NODE **) th_cfp_svar(cfp, -1);
2075        stored_cref = *pcref;
2076        *pcref = th_cref_push(th, under, NOEX_PUBLIC);
2077
2078        PUSH_TAG(PROT_NONE);
2079        if (( state = EXEC_TAG() ) == 0) {
2080            val = (*func) (args);
2081        }
2082        POP_TAG();
(gdb) 

2078 行目でストップしているが、実際に値が変更されているのは一つ前の行のようだ。*pcref への代入が名前からしてそうらしい。確認してみよう。
*pcref への代入でウォッチポイントが動作したとすれば、pcref の値と、ウォッチポイントを設定したアドレスは同じ値のはずだ。調べてみよう。

(gdb) p/d pcref
$48 = 3085397824
(gdb) info watch
Num Type           Disp Enb Address    What
7   breakpoint     keep y   0x080e153f in lfp_svar at vm.c:858
        breakpoint already hit 1 time
11  breakpoint     keep y   0x080e3c74 in th_eval at insns.def:842
        breakpoint already hit 1 time
12  hw watchpoint  keep y              *(long unsigned int *) 3085397824
        breakpoint already hit 1 time
(gdb) 

pcref の値とウォッチポイントの値を10進数で表示してみた。どちらの値も 3085397824 で同じだ。これで *pcref への代入が、問題の値の代入だということが分かった。


さて、問題の変数に値が代入されるところまでは突き止めた。それでは、どんな値が代入されているのだろう。代入している場所のソースコードを見てみよう。

2044:   /* function to call func under the specified class/module context */
2045:   static VALUE
2046:   exec_under(VALUE (*func) (VALUE), VALUE under, VALUE self, VALUE args)
2047:   {
2048:       VALUE val = Qnil;		/* OK */
2049:       rb_thread_t *th = GET_THREAD();
2050:       rb_control_frame_t *cfp = th->cfp;

 ......

2070:       while (!RUBY_VM_NORMAL_ISEQ_P(cfp->iseq)) {
2071:   	cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
2072:       }
2073:       
2074:       pcref = (NODE **) th_cfp_svar(cfp, -1);
2075:       stored_cref = *pcref;
2076:       *pcref = th_cref_push(th, under, NOEX_PUBLIC);

 ......

(eval.c r12010)

関数は exec_under() だ。長い関数なので、無関係なところは省略してある。
代入されている値は th_cref_push(th, under, NOEX_PUBLIC) の返り値だ。th_cref_push() はクラス under をスレッド th の cref スタックに追加したものを返す。この under はクラスのはずだが、どのクラスか調べてみよう。

(gdb) p *(struct RClass*)under
$50 = {basic = {flags = 3, klass = 3085397960}, iv_tbl = 0x8a44490, 
  m_tbl = 0x8a0c270, super = 3085655260}
(gdb) p rb_class2name(under)
$51 = 0xb7e76fa8 "A"
(gdb) 

クラス A だ。これで、cref スタックの最後にクラス A が現れていた理由が分かった。


クラス A であるこの under という変数はなんだろう。この変数は exec_under() の引数として渡されているので、コールスタックを遡って調べてみよう。


結論からいうと、この変数はコールスタックの上の方から次々にそのまま渡されてきている。コールスタックは以下のようになっている。太字の引数が exec_under() の under に相当する引数だ。

rb_mod_module_eval(int argc, VALUE *argv, VALUE mod)
  specific_eval(int argc, VALUE *argv, VALUE klass, VALUE self)
    eval_under(VALUE under, VALUE self, VALUE src, const char *file, int line)
      exec_under(VALUE (*func) (VALUE), VALUE under, VALUE self, VALUE args)

一番上の rb_mod_module_eval() はどこから呼ばれているのか?rb_mod_module_eval で検索をかけると以下のような結果が返ってくる。

>Internal search for "rb_mod_module_eval" in "*.c *.h *.y *.ci *.def"
S:\projects\ruby-trunk\intern.h:234:VALUE rb_mod_module_eval(int, VALUE*, VALUE);
S:\projects\ruby-trunk\struct.c:291:	rb_mod_module_eval(0, 0, st);
S:\projects\ruby-trunk\eval.c:2272:rb_mod_module_eval(int argc, VALUE *argv, VALUE mod)
S:\projects\ruby-trunk\eval.c:2902:    rb_define_method(rb_cModule, "module_eval", rb_mod_module_eval, -1);
S:\projects\ruby-trunk\eval.c:2903:    rb_define_method(rb_cModule, "class_eval", rb_mod_module_eval, -1);
>

最後の2行に注目しよう。"module_eval", "class_eval" という名前のメソッドの本体として使われている。つまり rb_mod_module_eval() は Module#class_eval の実体なのだ。
rb_mod_module_eval() の3番目の引数 mod は、Ruby レベルで言えば、メソッドを受け取るレシーバーオブジェクトにあたる。
ここで最初の Ruby プログラム、test_class_eval.rb を思い出してみよう。

class A
  @@a = "Hello"
end

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

A.foo

class_eval() はオブジェクト A に対して呼ばれている。つまり A がレシーバーオブジェクトで、rb_mod_module_eval() でいう mod だ。


これで筋がつながった。

  1. Module#class_eval の本体である rb_mod_module_eval() はメソッド class_eval のレシーバーであるクラス A をパラメータとして下請けの関数に渡す。
  2. 下請け関数の一つ、exec_under() は、クラス A を現在の cref スタックに追加した新たな cref スタックを、lfp[-1] すなわちローカル変数領域の内部用特別領域に保管する。このローカル変数のスコープが何かについては、今回は調べていない。
  3. A.class_eval に渡された、メソッド foo を定義する文字列は、やがてプログラムとして VM 命令列にコンパイルされる。
  4. この命令列が作られる時点でのスコープ*2では、lfp_special_cref() 経由で得られる、クラス A を含む cref スタックが使われるので、この命令列の cref スタックはクラス A を含むものになる。
  5. のちに A.foo が呼ばれるとき、この命令列の cref スタックを元にクラス変数検索が行われるので、cref スタックの最下位にあるクラス A は最初に検索される。クラス変数 @@a はクラス A で定義されているので、それが検索結果としてかえる。つまり、@@a は A.foo から参照できる。


これで cref スタックのクラス A がどこから来たかは判明した。
この cref スタックがどう特別なのかというのも、スコープの内部ローカル変数領域に保存される cref スタックということで説明がつく。

では、なぜ特別扱いになっているのだろう?


cref スタックを内部ローカル変数領域に保存する関数、exec_under() は rb_mod_module_eval() とrb_obj_instance_eval() からしか呼ばれていない。rb_obj_instance_eval() は Object#instance_eval の本体だから、つまり、cref スタックのこの特別扱いは、動的評価のときにだけ行われることが分かる。
Ruby の動的評価は、リファレンスマニュアルによれば、(クラス、モジュール、インスタンスの)「コンテキストで文字列 expr を評価してその結果を返」す。これの暗黙に意味するところは、普通のコンテキストとは違うコンテキストが使われる、ということだ。
違うコンテキストが使われるものの、それは動的評価メソッドを呼んでいる間だけのもので、本来のコンテキストを、そのためにわざわざ変更したくはない。その、一時的なコンテキストの変更を実現するための特別扱いではないか?
cref スタックだけがコンテキスト*3ではないので、言い切るのは危険だが、こと cref スタックに関しては、これが特別扱いする理由と言っていいように思う。

*1: cref = lfp_get_special_cref(lfp) ) != 0) { (gdb) s lfp_get_special_cref (lfp=0xb7da0014) at vm.c:1050 1050 if (((VALUE)(values = (void *)lfp[-1]

*2:上に出てきた「ローカル変数のスコープ」と同じスコープだ。

*3:そもそも「コンテキスト」の正確な定義は何なんだろう?