iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0

今天會接著講 Ruby <-> C 的橋接
在 Clang 不知道經歷了多少次 segmentation fault
經過了無數次的通靈,mongory-core 終於有所小成之後
終於來到了令人興奮的 bridge 環節
也就是 C core 真的可以邁入實際使用的里程碑
Ruby 具有原生的 C extension 機制與完善的 Ruby C api
串接起來非常自然
在我不知道撞牆了多少次之後
歸納出這一篇「能跑、好維護、可發佈」的做法

POV:

  • 為什麼用 git submodule 帶入 C core、extconf.rb 的編譯流程與關鍵旗標
  • macOS/Clang 的雷點
  • Rake 任務如何串起本地開發與發佈
  • 最後補一份排錯清單

為什麼用 submodule 來橋接 C core

  • 可控的來源:submodule 指向特定 commit,讓 Ruby gem 能在任何時間點重現同一套 C 來源
  • 單一發佈源:Ruby gem 與 C core 同倉管理(以 submodule 形式),簡化開發者安裝,不要求讀者自備 toolchain 或另外安裝 C SDK
  • 一致的編譯體驗:在 extconf.rb 直接把 C source 編進 extension,避免複雜的系統相依與連結

目標是「不複製檔案、不額外安裝外部庫」,就能在 gem install 的時候把 core 編譯好、載入好

extconf.rb:編譯流程與關鍵設計

ext/mongory_ext/extconf.rb 的職責有三個:

  1. 找到 submodule 來源與 include 路徑
  2. 收集 foundations/matchers.c 檔做成 $srcs/$objs
  3. 調整編譯與連結旗標,對 macOS 使用動態符號解析,避免硬連 libruby

關鍵片段(摘錄重點):

require 'mkmf'
require 'rbconfig'

core_dir = File.expand_path('mongory-core', __dir__)
core_src_dir = File.join(core_dir, 'src')
core_include_dir = File.join(core_dir, 'include')

abort('Please run: git submodule update --init --recursive') unless Dir.exist?(core_dir)

$INCFLAGS << " -I#{core_include_dir}"
foundations_src = Dir.glob(File.join(core_src_dir, 'foundations', '*.c'))
matchers_src    = Dir.glob(File.join(core_src_dir, 'matchers', '*.c'))

$CFLAGS << ' -std=c99 -Wall -Wextra -Wno-incompatible-pointer-types -Wno-int-conversion'
$CFLAGS << ' -Wno-declaration-after-statement -Wno-discarded-qualifiers'
$CFLAGS << (ENV['DEBUG'] ? ' -g -O0 -DDEBUG' : ' -O2')

if RbConfig::CONFIG['host_os'] =~ /darwin/
  $LDFLAGS  << ' -Wl,-undefined,dynamic_lookup'
  $DLDFLAGS << ' -Wl,-undefined,dynamic_lookup'
end

$INCFLAGS << ' -I.'
$INCFLAGS << " -I#{File.join(core_src_dir, 'foundations')}"
$INCFLAGS << " -I#{File.join(core_src_dir, 'matchers')}"
all_sources = ['mongory_ext.c'] + foundations_src + matchers_src
$srcs = all_sources
$objs = all_sources.map { |src| "#{File.basename(src, '.c')}.o" }

create_makefile('mongory_ext') # 這會生成一個針對使用者平台客製化的 Makefile

實務重點:

  • 收斂警告:有些 CI 或舊工具鏈會注入不相容旗標,筆者會在 extconf 裡過濾特定 warn flags,讓 GCC/Clang 都能編譯過
  • macOS 連結:用 -Wl,-undefined,dynamic_lookup,讓 Ruby 符號在載入時由宿主 resolver 解決,而不是硬連 libruby(可避免 framework 路徑地獄)
  • 物件目標:把 submodule 的 .c 直接加入 $srcs/$objs,由 Ruby 的 mkmf 幫忙編,無需事先在子模組內 cmake .. && make

Makefile 的客製化規則:

# 針對 submodule sources 顯式定義編譯規則,避免 mkmf 漏掉路徑
foo.o: /path/to/mongory-core/src/foundations/foo.c
	$(ECHO) compiling $<
	$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<

筆者偏好在 extconf 結尾讀取剛生成的 Makefile,追加上述規則,這招在處理「跨目錄來源」時很穩

Rake 工作流:submodule、compile、clean

mongory-rb/Rakefile 會提供幾個對開發者友善的任務:

namespace :submodule do
  task :init   # git submodule update --init --recursive
  task :update # git submodule update --remote
  task :build  # 若要在子模組內自行 cmake/make,可選擇性提供
end

desc 'Build the project (without standalone mongory-core build)'
task build_all: ['submodule:init', :compile]

task :compile do
  Dir.chdir('ext/mongory_ext') { sh 'ruby extconf.rb && make' }
end

task :clean do
  Dir.chdir('ext/mongory_ext') { sh 'make clean'; sh 'rm -f Makefile *.o foundations/*.o matchers/*.o mongory_ext.so' }
end

如果系統裝有 rake-compiler,還能產出 ABI 隔離的預編目錄(例如 lib/core/3.2),讓 gem 依 Ruby 版號載入對應的 .so/.bundle

macOS/Clang 要點

  • 動態符號解析:-Wl,-undefined,dynamic_lookup 是關鍵,不要嘗試硬連 librubyRuby.framework
  • Xcode CLI 工具:確保 xcode-select --install 完整裝好,否則 clang 或系統 headers 會缺失
  • 架構與平台:Ruby 平台字串常見如 arm64-darwin24x86_64-darwin23,不要誤用「universal」輸出導致載入錯誤
    • 事實上筆者自己就有踩到 universal 的雷,詳見 issue
  • 旗標清理:在 extconf 內移除不相容 warn flags,降低「CI 用 GCC、dev 機用 Clang」時的分歧

本地開發:從 0 到載入成功

步驟清單:

git clone https://github.com/mongoryhq/mongory-rb.git
cd mongory-rb
bundle install

# 1) 抓子模組(C core)
rake submodule:init

# 2) 編譯 extension(可加 DEBUG 強化診斷)
cd ext/mongory_ext
DEBUG=1 ruby extconf.rb && make

# 3) 回到專案根目錄,用 Rake 走一次完整流程
cd -
rake build_all

快速驗證:

require 'mongory'
data = [ { 'name' => 'John', 'age' => 20 }, { 'name' => 'Alice', 'age' => 17 } ]
result = data.mongory.c.where(:age.gte => 18)
puts result.map { |r| r['name'] }.inspect

如果使用 trace/explain,應該看得到 matcher 樹與比對過程(Day 7 有示例)

發佈流程:版本、建置、上傳

Mongory 的 Ruby gem 採「原始碼編譯」策略:

  1. 調整版本號(lib/mongory/version.rb
  2. 確保 rake build_allbundle exec rake(含測試與 RuboCop)皆通過
  3. gem build mongory.gemspec 產出 .gem
  4. gem push 上傳

若要支援「跨平台預編譯二進位包」,可以在 CI 跑各平台 native build 把 .so/bundle 打進 gem(搭配 rake-compilerlib/core/<abi> 佈署),細節將於 Day 21 展開

常見錯誤排查

  1. submodule not found
  • 訊息:mongory-core submodule not found ... Please run: git submodule update --init --recursive
  • 對策:執行 git submodule update --init --recursive,CI 需設 actions/checkoutsubmodules: recursive
  1. macOS 未定義符號(undefined symbols for architecture ...)
  • 症狀:link 階段找不到 rb_* 符號
  • 對策:確認 extconf.rb 加了 -Wl,-undefined,dynamic_lookup,同時避免硬連 -lruby
  1. 不相容的警告旗標導致編譯失敗
  • 症狀:例如 GCC 報 Clang 專屬的 warn flags
  • 對策:在 extconf 內清理 warnflags/cflags/optflags,或以 CC=gcc/CC=clang 固定工具鏈
  1. Makefile 未編譯 submodule 檔案
  • 症狀:只編到 mongory_ext.c,沒把 foundations/matchers 編進去
  • 對策:檢查 $srcs/$objs 是否包含 submodule 檔案,必要時追加顯式規則(見上文)
  1. gem 安裝時卡 toolchain
  • 症狀:使用者機器沒有 compiler,gem install 失敗
  • 對策:文件中加註「需安裝開發工具」,或提供預編譯版本(Day 21)
  1. 版本對齊與 ABI 目錄
  • 症狀:不同 Ruby 版本載入了錯誤 ABI 的 .so/bundle
  • 對策:以 rake-compilerlib/core/<major.minor> 隔離,發佈前在多 Ruby 版本驗證

為什麼這套流程可維護?

  • 來源單一:所有 C 原始碼從 submodule 進來,沒有「複製貼上」同步負擔
  • 明確邊界:Ruby extension 只做「編譯與載入」,C core 專注資料結構與匹配邏輯,跨語言 adapter 規則清楚(Day 14)
  • 工具鏈相容:透過旗標清理與動態連結策略,讓 macOS 與 Linux CI 的結果穩定一致

小結與預告

  • 今天把 Ruby C 擴充的建構路徑走完:submodule 帶 core、extconf.rb 編譯、macOS/Clang 的雷與解、Rake 連成一條龍、排錯清單收尾
  • 明天 Day 16 進入「VALUE ↔ C value 轉換策略」,把 converter 與 adapter 的責任邊界講清楚,鋪陳 shallow/deep 與 recover 的細節

專案首頁(Ruby 版)


上一篇
Day 14:Matchers 架構總覽與 Adapter 邊界
下一篇
Day 16:VALUE↔C value 轉換策略
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言