在不該使用 Ruby 的 Class Variable 理由這篇文章中有大概提到 Class Variable 的意義在 Ruby 裡面跟我們在其他語言認知到的 static
關鍵字是不同的,那麼實際上到底差在哪裡呢?
在閱讀 mruby 原始碼之前我也想不通,然而在使用不同的技巧組合驗證特性之後,終於理解了 mruby 實作中對 RClass
(Class 物件資料)裡面所定義的 iv_tbl
(Instance Varable 對照表)的意義。
用 ISEQ 尋找線索
因為原始的問題是 .class_eval
無法正確存取到 Class Variable 造成的問題,因此用幾種不同的方法試驗之後,決定用 RubyVM::InstructionSequence
來把處理過程中的 ISEQ(Ruby 編譯後的指令碼)顯示出來。
1puts 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/* Get value of class variable id of klass as val. */
2DEFINE_INSN
3getclassvariable
4(ID id)
5()
6(VALUE val)
7/* "class variable access from toplevel" warning can be hooked. */
8// attr bool leaf = false; /* has rb_warning() */
9{
10 val = rb_cvar_get(vm_get_cvar_base(vm_get_cref(GET_EP()), GET_CFP()), id);
11}
在閱讀原始碼的時候我們不需要知道得太過於詳細,根據關鍵字可以看到像是 rb_cvar_get
和 vm_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
的實作。
1static inline VALUE
2vm_get_cvar_base(const rb_cref_t *cref, rb_control_frame_t *cfp)
3{
4 VALUE klass;
5
6 if (!cref) {
7 rb_bug("vm_get_cvar_base: no cref");
8 }
9
10 while (CREF_NEXT(cref) &&
11 (NIL_P(CREF_CLASS(cref)) || FL_TEST(CREF_CLASS(cref), FL_SINGLETON) ||
12 CREF_PUSHED_BY_EVAL(cref))) {
13 cref = CREF_NEXT(cref);
14 }
15 if (!CREF_NEXT(cref)) {
16 rb_warn("class variable access from toplevel");
17 }
18
19 klass = vm_get_iclass(cfp, CREF_CLASS(cref));
20
21 if (NIL_P(klass)) {
22 rb_raise(rb_eTypeError, "no class variables available");
23 }
24 return klass;
25}
快速掃過一遍會知道這是一個用來尋找 Class
的處理,而核心的行為就在 while
這個迴圈中。
1 while (CREF_NEXT(cref) &&
2 (NIL_P(CREF_CLASS(cref)) || FL_TEST(CREF_CLASS(cref), FL_SINGLETON) ||
3 CREF_PUSHED_BY_EVAL(cref))) {
4 cref = CREF_NEXT(cref);
5 }
裡面會先檢查是否存在下一層 CREF
(依照語意推測是 Class Reference)同時在滿足三種條件的其中一種時,就會往下一層尋找。
- 目前的
CREF
是nil
的狀況 - 目前的
CREF
不是 Singleton Class - 目前的
CREF
是由_eval
類型的行為被加入
到這邊為止,我們就可以很快地了解到為什麼 .class_eval
後會是找 Object
物件的 Class Variable 而非呼叫的物件本身,因為在 CRuby 的實作中這本身就是該被跳過的情況。
Class Variable 的保存
既然我們已經能夠從 ISEQ 的資訊得知 Ruby 處理的方法,自然能夠從 rb_cvar_get
方法往下追查,當找到 cvar_lookup_at
方法時,已經可以看出 iv_tbl
的意義。
1static int
2cvar_lookup_at(VALUE klass, ID id, st_data_t *v)
3{
4 if (!RCLASS_IV_TBL(klass)) return 0;
5 return st_lookup(RCLASS_IV_TBL(klass), (st_data_t)id, v);
6}
其中 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 跟其他語言相差非常大的其中一種特性,當我們理解運作的原理之後,就相對的能夠想像可以應用的情境。然而,這些情況大多都可以用其他物件導向的技巧所取代,在大多數情況我們依然還是避免使用會比較恰當。