弦而時習之

Ruby 的 Class Variable 深入解析

不該使用 Ruby 的 Class Variable 理由這篇文章中有大概提到 Class Variable 的意義在 Ruby 裡面跟我們在其他語言認知到的 static 關鍵字是不同的,那麼實際上到底差在哪裡呢?

在閱讀 mruby 原始碼之前我也想不通,然而在使用不同的技巧組合驗證特性之後,終於理解了 mruby 實作中對 RClass(Class 物件資料)裡面所定義的 iv_tbl(Instance Varable 對照表)的意義。

用 ISEQ 尋找線索

因為原始的問題是 .class_eval 無法正確存取到 Class Variable 造成的問題,因此用幾種不同的方法試驗之後,決定用 RubyVM::InstructionSequence 來把處理過程中的 ISEQ(Ruby 編譯後的指令碼)顯示出來。

1
puts RubyVM::InstructionSequence.compile('Counter.class_eval { @@count }').disasm

執行之後會拿到生成的指令碼,這些指令在 Ruby 中實際上會是一些編號,為了方便理解所以會轉換為一些有意義的關鍵字,同時也能在原始碼搜尋到。

== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(1,30)> (catch: FALSE)
== catch table
| catch type: break  st: 0000 ed: 0012 sp: 0000 cont: 0012
| == disasm: #<ISeq:block in <compiled>@<compiled>:1 (1,19)-(1,30)> (catch: FALSE)
| == catch table
| | catch type: redo   st: 0001 ed: 0003 sp: 0000 cont: 0001
| | catch type: next   st: 0001 ed: 0003 sp: 0000 cont: 0003
| |------------------------------------------------------------------------
| 0000 nop                                                              (   1)[Bc]
| 0001 getclassvariable                       :@@count[Li]
| 0003 nop
| 0004 leave                                                            (   1)[Br]
|------------------------------------------------------------------------
0000 opt_getinlinecache                     9, <is:0>                 (   1)[Li]
0003 putobject                              true
0005 getconstant                            :Counter
0007 opt_setinlinecache                     <is:0>
0009 send                                   <calldata!mid:class_eval, argc:0>, block in <compiled>
0012 nop
0013 leave                                                            (   1)

我們先不討論 class 關鍵字和 .class_eval 方法呼叫時的差異,兩者都會在這個階段呼叫 getclassvariable 這個指令,並且嘗試取出 :@@count 這個 Class Variable 數值。

getclassvariable 指令

當我們知道這個指令後,就可以沿著線索開始朝 Ruby 的原始碼尋找,會發現 getclassvariable 指令在 insns.def 檔案中,定義了對應的 C 語言呼叫是這樣的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* Get value of class variable id of klass as val. */
DEFINE_INSN
getclassvariable
(ID id)
()
(VALUE val)
/* "class variable access from toplevel" warning can be hooked. */
// attr bool leaf = false; /* has rb_warning() */
{
    val = rb_cvar_get(vm_get_cvar_base(vm_get_cref(GET_EP()), GET_CFP()), id);
}

在閱讀原始碼的時候我們不需要知道得太過於詳細,根據關鍵字可以看到像是 rb_cvar_getvm_get_cvar_base 等等資訊,經過檢查之後 vm_get_cvar_base 是影響 .class_eval 方法運作的關鍵。

在 CRuby 或 mruby 中遇到這類呼叫,通常會是 類型_get(對象, 屬性) 的狀況,因此 vm_get_cvar_base 極有可能跟決定 Class Variable 從哪裡抓取的判定有關。

vm_get_cvar_base

繼續跟著線索,我們會從 vm_insnhelper.c 這個檔案裡面找到 vm_get_cvar_base 的實作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static inline VALUE
vm_get_cvar_base(const rb_cref_t *cref, rb_control_frame_t *cfp)
{
    VALUE klass;

    if (!cref) {
	rb_bug("vm_get_cvar_base: no cref");
    }

    while (CREF_NEXT(cref) &&
	   (NIL_P(CREF_CLASS(cref)) || FL_TEST(CREF_CLASS(cref), FL_SINGLETON) ||
	    CREF_PUSHED_BY_EVAL(cref))) {
	cref = CREF_NEXT(cref);
    }
    if (!CREF_NEXT(cref)) {
	rb_warn("class variable access from toplevel");
    }

    klass = vm_get_iclass(cfp, CREF_CLASS(cref));

    if (NIL_P(klass)) {
	rb_raise(rb_eTypeError, "no class variables available");
    }
    return klass;
}

快速掃過一遍會知道這是一個用來尋找 Class 的處理,而核心的行為就在 while 這個迴圈中。

1
2
3
4
5
    while (CREF_NEXT(cref) &&
	   (NIL_P(CREF_CLASS(cref)) || FL_TEST(CREF_CLASS(cref), FL_SINGLETON) ||
	    CREF_PUSHED_BY_EVAL(cref))) {
	cref = CREF_NEXT(cref);
    }

裡面會先檢查是否存在下一層 CREF(依照語意推測是 Class Reference)同時在滿足三種條件的其中一種時,就會往下一層尋找。

  • 目前的 CREFnil 的狀況
  • 目前的 CREF 不是 Singleton Class
  • 目前的 CREF 是由 _eval 類型的行為被加入

到這邊為止,我們就可以很快地了解到為什麼 .class_eval 後會是找 Object 物件的 Class Variable 而非呼叫的物件本身,因為在 CRuby 的實作中這本身就是該被跳過的情況。

Class Variable 的保存

既然我們已經能夠從 ISEQ 的資訊得知 Ruby 處理的方法,自然能夠從 rb_cvar_get 方法往下追查,當找到 cvar_lookup_at 方法時,已經可以看出 iv_tbl 的意義。

1
2
3
4
5
6
static int
cvar_lookup_at(VALUE klass, ID id, st_data_t *v)
{
    if (!RCLASS_IV_TBL(klass)) return 0;
    return st_lookup(RCLASS_IV_TBL(klass), (st_data_t)id, v);
}

其中 RCLASS_IV_TBL 巨集(Macro)所對應的實作如下

1
#define RCLASS_IV_TBL(c) (RCLASS_EXT(c)->iv_tbl)

到這裡就很明顯了,在 Ruby 裡面想實現 static 的特性只需要借助 Class 物件的 Instance Variable 即可,然而在 Ruby 中 RClass 物件所具備的 iv_tbl 資訊則是用來保存 Class Variable 的,也因為這樣的特性,我們只需要沿著物件的繼承關係網上找到符合條件的 Class Variable 即可。

這個特性應該是 Ruby 跟其他語言相差非常大的其中一種特性,當我們理解運作的原理之後,就相對的能夠想像可以應用的情境。然而,這些情況大多都可以用其他物件導向的技巧所取代,在大多數情況我們依然還是避免使用會比較恰當。

電子報

留言