iT邦幫忙

2025 iThome 鐵人賽

DAY 30
0

本篇聚焦「讀者如何把 C 核心以 Ruby C 擴充橋接進專案」。筆者以 Mongory 的 ext 實作為範本:extconf.rb 編譯旗標、子模組整合、Ruby 值轉 C 值的 shallow/deep/recover、記憶體池與 GC 協作、例外傳遞語意,以及「不可以讓他 segfault!」的防線。文內所有做法均可抽離 Mongory,遷移到讀者的任意 C×Ruby 專案。

——

讀者能學到什麼

  • 如何寫出最小可用的 extconf.rb,安全地編譯並連結 C 原始碼。
  • 如何將 C 核心以 git submodule 方式整合,或直接納入原始碼樹。
  • Ruby ↔ C 的型別轉換邊界設計:shallow/deep/recover 三段式與零拷貝原則。
  • 記憶體池(pool)與 Ruby GC 的協作:mark_list、origin 指標、reset/free。
  • 例外與錯誤語意傳遞:避免 segfault,統一由 Ruby 例外承接。

——

一、專案骨架與 extconf.rb:最小可用版

目標:用 Ruby 的 mkmf 直接把 C 核心的 .c 檔納入擴充模組,產生 mongory_ext(讀者可替換名稱)。

關鍵做法(抽象化自 Mongory):

  • Bundler/mkmf 產生 Makefile。
  • 將子模組 core/include 加入 $INCFLAGS,並蒐集 core/src/**.c 作為 $srcs
  • 設定通用 -std=c99 -Wall -Wextra,針對舊工具鏈額外關閉噪音警告。
  • macOS 連結使用 -Wl,-undefined,dynamic_lookup,避免強鏈接 libruby(由載入器解符號)。
  • 產生完成後追加自訂規則,讓 Make 能正確編譯子模組來源檔。

這些要點在 ext/mongory_ext/extconf.rb 皆可見,讀者可 1:1 改名複製。

——

二、是否採用 submodule:判斷準則

何時用 submodule:

  • C 核心獨立維護、跨語言共享(Ruby/Go/…)。
  • 需要定期同步 upstream,保持一致測試集合。

何時直接內嵌原始碼:

  • 僅此專案使用,程式量小或穩定。
  • 想避開 submodule 操作複雜度。

Mongory 的做法:Ruby 端直接編譯 mongory-core 的 C 檔案,不需要獨立建立靜態庫,也不引入 core 的 CMake 測試依賴,簡化安裝步驟。

——

三、Ruby ↔ C 型別轉換:shallow / deep / recover

設計目標:

  • shallow:即時包裝 Ruby Array/Hash,getter 委派回 Ruby(O(1)),避免首層 O(n) 展開。
  • deep:條件(condition)需完整物化,保證 matcher 結構穩定且可重複使用。
  • recover:從 C 回傳 Ruby 值,以 origin 回指實體,避免不必要的重新分配。

抽象化自 mongory_ext.c 的關鍵要點:

  • rb_to_mongory_value_primitive:直譯數值/字串/布林/正則/符號。
  • rb_to_mongory_value_shallow:Hash/Array 以 wrapper 結構包住,get 時再呼叫 Ruby API。
  • rb_to_mongory_value_deep:條件遞迴轉換為 C 結構,並在 key 路徑上建立 string_map/symbol_map 快取以減少 Ruby 物件 churn。
  • mongory_value_to_ruby:以 value->origin 回傳原生 Ruby 值。

——

四、記憶體池 × Ruby GC:不漏、不重複、不驚擾

Mongory 的 C 側以 mongory_memory_pool 管理生命週期,Ruby 側透過 mark_list 配合 GC:

  • origin:所有 C 包裝值指回對應的 Ruby VALUE。
  • mark_list:在 Ruby 的 mark 階段逐一 rb_gc_mark,確保外部物件壽命足夠。
  • scratch_pool->reset:每次 match? 後重置,避免重複配置。
  • pool->free:在 wrapper 釋放時統一回收(含 trace_pool)。

這使 C 程式碼能像高階語言一樣「一次配置、多次使用、集中釋放」。

——

五、例外與錯誤語意:守住「不能 segfault」的紅線

原則:所有錯誤都收斂到 Ruby 例外,避免讓 C 發生未定義行為。

做法:

  • pool->error 作為錯誤匯流,統一在邊界檢查並 rb_raise(見 rb_mongory_error_handling)。
  • Regex/Custom adapter 透過 Ruby 方法呼叫(Regexp#match?Matchers.lookup/new/match?),錯誤自然以 Ruby 例外呈現。
  • 如需嚴格保護外部回呼,可考慮 rb_protect 或等效保護策略;Mongory 當前實作以簡潔性為先,由呼叫端確保輸入正確。

——

六、建置與本地驗證:一步到位

最小流程:

bundle install
bundle exec rake compile

或使用專案腳本(若有):

scripts/build_with_core.sh

驗證是否載入 C 擴充:

require 'mongory'
puts defined?(Mongory::CMatcher) ? 'C extension available' : 'pure Ruby path'

——

七、讀者專案可直接複製的骨架與檢查清單

骨架:

  • ext/your_ext/your_ext.c:Ruby 綁定與轉換實作。
  • ext/your_ext/extconf.rb:收集 core 原始碼、設定旗標、處理 macOS dynamic_lookup。
  • ext/your_ext/core/**:C 核心(submodule 或直接複製)。

檢查清單:

  1. extconf.rb 會:
    • 將 core includesrc 加入編譯。
    • 在 macOS 設定 -Wl,-undefined,dynamic_lookup
    • 產出 $srcs/$objs,並為外部來源附加明確規則。
  2. C 邊界:
    • shallow/deep/recover 分工清楚。
    • 所有 mongory_value 設定 origin(必要時)。
    • mark_list 確保 GC 可達,reset/free 節點明確。
  3. 例外:
    • 所有錯誤透過 Ruby 例外終止;不得讓 segfault 漏出。

——

八、延伸:如何決定 submodule 與同步策略

若核心將跨多語言重用,建議:

  • 在各語言橋接層「直接編譯核心原始碼」,避免再製/同步複雜度。
  • 以「測試資料(JSON)共用」維持語意一致;各語言層各自補齊 API 與效能最佳化。

——

結語:C → Ruby bridge 的本質是「把語意邊界穩穩立起來」。以 Mongory 的實作為範本,讀者能在自家專案中搭出可維護、可觀測、可擴充、且不會炸掉整個進程的 C 擴充橋接。


上一篇
Bonus 1:Rails generator 全攻略
下一篇
Bonus 3:CI/CD 與預編譯
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎33
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言