想要快速參觀 Ruby 內部結構嗎?
那你來對地方了。
因為……
我們將一起探索 Ruby 物件在記憶體中的佈局,以及如何操作內部資料結構做一些酷的事。
繫好安全帶,準備踏上 Ruby 解譯器的深度之旅!
陣列的記憶體佈局
當你建立陣列,Ruby 必須用一些系統記憶體和一點點中繼資料來支撐它。
中繼資料包含:
- 陣列大小(項目計數)
- 陣列容量
- 類別
- 物件狀態(frozen 或否)
- 指向資料儲存記憶體位置的指標
因為主要 Ruby 解譯器(MRI)是用 C 寫的,所以沒有物件。
但有別的東西:structs。
C 語言的 struct 幫你把相關資料存一起,這在 MRI 原始碼中大量用來表示 Array, String 等物件。
看其中一個 struct 我們可以推斷物件的記憶體佈局。
來看 Array 的 struct,叫 RArray:
struct RArray { struct RBasic basic; union { struct { long len; union { long capa; VALUE shared; } aux; const VALUE *ptr; } heap; const VALUE ary[RARRAY_EMBED_LEN_MAX]; } as; }; 如果你不熟 C 這可能看起來有點嚇人,別擔心!我會幫你拆解成容易消化的小塊 🙂
第一個是 RBasic,也是個 struct:
struct RBasic { VALUE flags; VALUE klass; } 這是大多數 Ruby 物件都有的,包含這個物件的類別和一些二進位旗標,說這物件是否 frozen(還有像 'tainted' 這類屬性)。
換句話說:
RBasic 包含物件的通用中繼資料。
接著是另一個 struct,包含陣列長度(len)。
union 表達式說 aux 可以是 capa(容量)或 shared。這主要是優化,細節在 Pat Shaughnessy 的這篇優秀文章解釋。記憶體分配方面,編譯器會用 union 裡最大的型別。
然後是 ptr,包含實際 Array 資料儲存的記憶體位址。
這是長什麼樣子的圖(每個白/灰格子在 32-bit 系統是 4 bytes):

你可以用 ObjectSpace 模組看物件的記憶體大小:
require 'objspace' ObjectSpace.memsize_of([]) # 20
現在我們準備好來點好玩的了!
Fiddle:一個有趣的實驗
RBasic 在 32-bit 系統正好 8 bytes,64-bit 系統 16 bytes。知道這點我們可以用 Fiddle 模組存取物件的原始記憶體位元組並改變它們做有趣實驗。
例如:
我們可以透過切換單一位元來改變 frozen 狀態。
這本質上就是 freeze 方法做的事,但注意沒有 unfreeze 方法。
我們為了好玩實作它!
先 require Fiddle 模組(Ruby 標準函式庫一部分)並建立 frozen 字串。
require 'fiddle' str = 'water'.freeze str.frozen? # true
接著:
我們需要字串的記憶體位址,可以這樣獲得。
memory_address = str.object_id * 2
最後:
我們翻轉 Ruby 檢查物件是否 frozen 的确切那個位元。我們也用 frozen? 方法檢查是否生效。
Fiddle::Pointer.new(memory_address)[1] ^= 8 str.frozen? # false
注意 index [1] 指的是 flags 值的第 2 個位元組(總共 4 個位元組)。
然後用 ^= 這是「XOR」(獨佔 OR)運算子翻轉那個位元。
我們這樣做是因為 flags 裡不同位元有不同含義,我們不想改無關的東西。
如果讀過我的 ruby tricks 文章可能看過這個,但現在你知道它怎麼運作了 🙂
另一件能試的是改陣列長度並列印陣列。
你會看到陣列變短!
你甚至可以改類別讓 Array 以為它是 String……
結論
你學到了一點 Ruby 背後如何運作。Ruby 物件記憶體如何佈局 以及如何用 Fiddle 模組玩弄它。
你大概不該在真實應用這樣用 Fiddle,但實驗起來很有趣。
別忘了 分享這篇文章 讓更多人看到 🙂