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

今日は、クラス内で定義されたクラスメソッドからクラス変数を参照する場合の、ruby 1.9 内部の動作を調べてみる。調べる Ruby のプログラムは以下の通り。

class A
  @@a = "Hello"
  def A.test_in
    puts @@a
  end
end

A.test_in  # --> Hello

(test_in.rb)


puts @@a の行でどうやって @@a の内容を取ってきているかを調べるのが目的だ。


ruby 1.9 から YARV が使われている。YARV は Yet Another Ruby Virtual Machine という名前が示すとおり、RubyVM だ。VM だから、独自のインストラクションセットを定義している。
ruby 1.9 の内部動作をデバッガで追いかけるときは、YARV のインストラクションハンドラにブレークポイントを置いて調べはじめると分かりやすい。
以下、実際にやってみる。


まず初めに、Ruby のプログラムを YARVアセンブラに変換する。
筆者がざっと調べたかぎりでは、こういうことをやるコマンドラインツールは提供されてないようだ。*1しかし、YARVruby クラスライブラリ (VM) が必要な機能を提供しているので、自分で Ruby スクリプトを書くのは難しくない。例えば以下のようになる。

#!/usr/bin/env ruby

code = ""
while (ln = gets)
  code += ln
end

iseq = VM::InstructionSequence.compile(code, ARGF.filename)

puts iseq.disasm

(disasm)

このスクリプトを test_in.rb に対して実行してみよう。

$ disasm test_in.rb
== disasm: @test_in.rb>=====================================
local scope table (size: 1, argc: 0)

0000 putnil           
0001 putnil           
0002 defineclass      :A, , 0                                (   1)
0006 pop              
0007 getinlinecache   , 14                                        (   8)
0010 getconstant      :A
0012 setinlinecache   7
0014 send             :test_in, 0, nil, 0, 
0020 leave            
== disasm: @test_in.rb>==================================
local scope table (size: 1, argc: 0)

0000 putstring        "Hello"                                         (   2)
0002 setclassvariable :@@a
0004 getinlinecache   , 11                                        (   3)
0007 getconstant      :A
0009 setinlinecache   4
0011 definemethod     :test_in, test_in, 1
0015 putnil           
0016 leave            
== disasm: ====================================
local scope table (size: 1, argc: 0)

0000 putnil                                                           (   4)
0001 getclassvariable :@@a
0003 send             :puts, 1, nil, 8, 
0009 leave            
$ 

test_in.rb のアセンブラコードが得られた。
puts @@a に対応する部分を探してみる。一番終わりの部分がそうらしい。getclassvariable :@@a というのが、そのものずばり、@@a の値を取ってくる命令に違いない。それでは、VM の getclassvariable 命令を処理する部分にブレークポイントをかけてみよう。


YARV の命令は insns.def というファイルに定義してある。getclassvariable の定義は以下のようになっている。

/**
  @c variable
  @e get class variable id of klass as val.
  @j klass のクラス変数 id を得る。
 */
DEFINE_INSN
getclassvariable
(ID id)
()
(VALUE val)
{
    VALUE klass = eval_get_cvar_base(th, GET_ISEQ());
    val = rb_cvar_get(klass, id);
}

(insns.def r11842)

C に似ている独自の文法で書かれている。YARV の作者のささださんが書かれた「YARVアーキテクチャ」に文法についての説明がある。
関数宣言に相当する部分は独自だが、本体は C そのもののようだ。
getclassvariable は C の関数ではないので直接ブレークポイントは設定できないが、行番号を直接指定すれば insns.def に対してブレークポイントを設定することができる。
eval_get_cvar_base() の行は insnsf.def の 218 行目にあたる。そこで、gdb を起動して以下のようにブレークポイントを設定する。

$ 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 0x80e2923: file insns.def, line 218.
(gdb) r test_in.rb       # Ruby プログラム test_in.rb を実行
Starting program: /home/minada/projects/ruby-trunk/ruby-trunk test_in.rb
Reading symbols from shared object read from target memory...done.
Loaded system supplied DSO at 0x111000
[Thread debugging using libthread_db enabled]
[New Thread -1208494400 (LWP 16684)]
[New Thread -1208497232 (LWP 16687)]
[Switching to Thread -1208494400 (LWP 16684)]

Breakpoint 1, th_eval (th=0x8ef7258, initial=0) at insns.def:218
218         VALUE klass = eval_get_cvar_base(th, GET_ISEQ());
(gdb)

../ruby-trunk はデバッグオプション付きでビルドした ruby 1.9 だ。
insns.def の 218 行目にブレークポイントを設定し test_in.rb を実行して、目的の行で実行が中断しているのが分かる。


ここで getclassvariable を定義しているコードを見直してみよう。

{
    VALUE klass = eval_get_cvar_base(th, GET_ISEQ());
    val = rb_cvar_get(klass, id);
}

(insns.def r11842)

eval_get_cvar_base() でクラスを取って、それを id とともに rb_cvar_get() に渡して目的の値を得ているようだ。id はクラス変数 @@a を表わす ID。test_in.rb をコンパイルしたマシンコードから渡されている。
eval_get_cvar_base() については以前調べた。このプログラムの場合では、eval_get_cvar_base() は GET_ISEQ() で返された命令列が属するクラススコープのクラスを返す。どのクラスが返っているのか、実際に調べてみよう。
クラスの名前を調べるのに rb_class2name() という関数が使えるが、一つ落とし穴がある。クラスが特異クラスだった場合、特異クラスの名前ではなく、そのスーパークラスの名前を返すのだ。だから、rb_class2name() を呼ぶ前に、クラスが特異クラスでないことを確認しておく必要がある。
あるクラスが特異クラスであるかどうかは、クラスオブジェクトの flags を見れば分かる。特異クラスの場合には、flags の FL_SINGLETON ビットを立てることになっている。FL_SINGLETON ビットは 0x800 だ。

(gdb) n   # klass の値を得るため1行実行
219         val = rb_cvar_get(klass, id);
(gdb) p/x ((struct RClass*)klass)->basic.flags
$1 = 0x3
(gdb) 

0x3 は普通のクラスを表わすので、このクラスは特異クラスではない。ちなみに、特異クラスだった場合は flags の値は 0x803 になる。
普通のクラスなので、クラス名を調べるのに rb_class2name() が使える。

(gdb) p rb_class2name(klass)
$2 = 0xb7ec9eec "A"

プログラムで定義したクラス A が返っていることが分かる。


この結果はなかなか興味深い。というのは、Ruby のクラスメソッド定義はクラスのオブジェクトへの特異メソッド定義に他ならず、特異メソッドを定義するときには内部的に特異クラスが作られるからだ。
クラスメソッド A.test_in が定義された時点で、このメソッドが属する特異クラスが存在するはずだが、eval_get_cvar_base() はそれを返さない。
先に種明かしをしてしまうと、これがクラスメソッドの定義方法の違いによる、クラス変数参照結果の違いにからんでくる。詳しい説明は他のクラスメソッド定義を調べるときにするので、今は、test_in.rb では eval_get_cvar_base() はクラス A を返す、ということで先に進む。


これで、rb_cvar_get() に渡されている引数が、クラス A とクラス変数 @@a の ID だということが分かった。次は、rb_cvar_get() で何が起こっているか調べる。

VALUE
rb_cvar_get(VALUE klass, ID id)
{
    VALUE value, tmp;

    tmp = klass;
    CVAR_LOOKUP(&value, value);
    rb_name_error(id,"uninitialized class variable %s in %s",
		  rb_id2name(id), rb_class2name(tmp));
    return Qnil;		/* not reached */
}

(variable.c r11842)

動作の大部分がマクロ CVAR_LOOKUP に委譲されている。マクロは gdb で追っていけないので、マクロを展開したバージョンを自分で作ってみた。

VALUE
rb_cvar_get(VALUE klass, ID id)
{
    VALUE value, tmp;

    tmp = klass;
    do {
        if (RCLASS(klass)->iv_tbl && st_lookup(RCLASS(klass)->iv_tbl,id,(&value))) {
            return (value);
        }
        if (FL_TEST(klass, FL_SINGLETON) ) {
            VALUE obj = rb_iv_get(klass, "__attached__");
            switch (TYPE(obj)) {
              case T_MODULE:
              case T_CLASS:
                klass = obj;
                break;
              default:
                klass = RCLASS(klass)->super;
                break;
            }
        }
        else {
            klass = RCLASS(klass)->super;
        }
        while (klass) {
            if (RCLASS(klass)->iv_tbl && st_lookup(RCLASS(klass)->iv_tbl,id,(&value))) {
                return (value);
            }
            klass = RCLASS(klass)->super;
        }
    } while(0);
    rb_name_error(id,"uninitialized class variable %s in %s",
		  rb_id2name(id), rb_class2name(tmp));
    return Qnil;		/* not reached */
}

(variable.c r11842改)

このコードをデバッガで追っていくと、最初の if (RCLASS(klass)->iv_tbl && ... 文が真になって、そこで値が返っていることが分かる。この if 文を調べてみよう。

        if (RCLASS(klass)->iv_tbl && st_lookup(RCLASS(klass)->iv_tbl,id,(&value))) {
            return (value);
        }

(variable.c r11842改)

これはオブジェクトのインスタンス変数を取り出すための、ruby のイディオムだ。iv_tbl がインスタンス変数を格納するためのテーブルで、st_lookup() は、そのテーブルに id というキーで格納されたデータを取り出して、value に返す。
ここで、オブジェクトのインスタンス変数を取り出すためのイディオム、と言ったが、ここで使われているのはクラスである。なぜクラスにオブジェクトのイディオムが使えるのか?
クラスもオブジェクトの一種だから、という答えはいかにも正しそうに聞こえるし、概念上はまさにそうなのだが、実装的には正確ではない。クラスとオブジェクトを表わす構造体を見てみよう。

struct RObject {
    struct RBasic basic;
    struct st_table *iv_tbl;
};

struct RClass {
    struct RBasic basic;
    struct st_table *iv_tbl;
    struct st_table *m_tbl;
    VALUE super;
};

(ruby.h r11842)

RObject が Object クラスを表わす構造体で、RClass が Class クラスを表わす構造体だ。iv_tbl の宣言までは構造がまったく同じなことが分かる。構造が同じなので、「たまたま」同じイディオムが使えるわけだ。
「たまたま」と言っても、もちろん意図的なのだろうが、Object と Class の継承関係が C の構造体レベルで保証*2されているわけではないことに注意しておこう。


ともかく、値を格納しているインスタンス変数テーブルは klass のもので、これはクラス A を表わすので、クラス変数 @@a はクラス A のインスタンス変数として格納されていることが分かった。
クラス変数は、オブジェクトごとに存在するインスタンス変数と違い、クラスにただ一つだけ存在するので(オブジェクトのではなく)クラスのインスタンス変数テーブルに格納されるのは理にかなっている。

次回は、クラスの外で定義されたクラスメソッドの場合を調べる。この場合は、今回の場合と違い ruby 1.8 でも 1.9 でもエラーが起こりクラス変数 @@a が参照できない。どうしてこういうことになるのだろうか?クラス内部でクラスメソッド定義した場合と何が違うのだろうか?

*1:tool/parse.rb がそれだと、ささださん御本人からご指摘があった。ありがとうございます。

*2:struct RClass が struct RObject を最初のメンバとして持つという形で実現できる。これをやらなかったのはコードが読みにくくなるからか?