---
title: "Ruby 的 Class Variable 深入解析"
date: 2022-01-21T00:00:00+08:00
publishDate: 2022-01-21T10:54:34+08:00
lastmod: 2022-01-21T11:01:53+08:00
tags: ["Ruby","C","經驗","程式","筆記"]
toc: true
permalink: "https://blog.aotoki.me/posts/2022/01/21/deep-into-ruby-class-variable/"
language: "zh-tw"
---


在[不該使用 Ruby 的 Class Variable 理由](https://blog.aotoki.me/posts/2022/01/16/the-reason-to-avoid-use-class-variable-in-ruby/)這篇文章中有大概提到 Class Variable 的意義在 Ruby 裡面跟我們在其他語言認知到的 `static` 關鍵字是不同的，那麼實際上到底差在哪裡呢？

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

<!--more-->

## 用 ISEQ 尋找線索{#find-by-iseq}

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

```ruby
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 指令{#command-getclassvariable}

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

```c
/* 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_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` 的實作。

```c
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` 這個迴圈中。

```c
    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）同時在滿足三種條件的其中一種時，就會往下一層尋找。

* 目前的 `CREF` 是 `nil` 的狀況
* 目前的 `CREF` 不是 Singleton Class
* 目前的 `CREF` 是由 `_eval` 類型的行為被加入

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

## Class Variable 的保存{#persistent-class-variable}

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

```c
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）所對應的實作如下

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

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

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


