今天會接著講 Ruby <-> C 的橋接
在 Clang 不知道經歷了多少次 segmentation fault
經過了無數次的通靈,mongory-core 終於有所小成之後
終於來到了令人興奮的 bridge 環節
也就是 C core 真的可以邁入實際使用的里程碑
Ruby 具有原生的 C extension 機制與完善的 Ruby C api
串接起來非常自然
在我不知道撞牆了多少次之後
歸納出這一篇「能跑、好維護、可發佈」的做法
POV:
extconf.rb
的編譯流程與關鍵旗標extconf.rb
直接把 C source 編進 extension,避免複雜的系統相依與連結目標是「不複製檔案、不額外安裝外部庫」,就能在 gem install
的時候把 core 編譯好、載入好
ext/mongory_ext/extconf.rb
的職責有三個:
foundations/matchers
的 .c
檔做成 $srcs/$objs
關鍵片段(摘錄重點):
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
實務重點:
-Wl,-undefined,dynamic_lookup
,讓 Ruby 符號在載入時由宿主 resolver 解決,而不是硬連 libruby(可避免 framework 路徑地獄).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,追加上述規則,這招在處理「跨目錄來源」時很穩
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
-Wl,-undefined,dynamic_lookup
是關鍵,不要嘗試硬連 libruby
或 Ruby.framework
xcode-select --install
完整裝好,否則 clang
或系統 headers 會缺失arm64-darwin24
或 x86_64-darwin23
,不要誤用「universal
」輸出導致載入錯誤
universal
的雷,詳見 issue
步驟清單:
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 採「原始碼編譯」策略:
lib/mongory/version.rb
)rake build_all
與 bundle exec rake
(含測試與 RuboCop)皆通過gem build mongory.gemspec
產出 .gem
gem push
上傳若要支援「跨平台預編譯二進位包」,可以在 CI 跑各平台 native build 把 .so/bundle
打進 gem(搭配 rake-compiler
的 lib/core/<abi>
佈署),細節將於 Day 21 展開
mongory-core submodule not found ... Please run: git submodule update --init --recursive
git submodule update --init --recursive
,CI 需設 actions/checkout
的 submodules: recursive
rb_*
符號extconf.rb
加了 -Wl,-undefined,dynamic_lookup
,同時避免硬連 -lruby
warnflags/cflags/optflags
,或以 CC=gcc
/CC=clang
固定工具鏈mongory_ext.c
,沒把 foundations/matchers 編進去$srcs/$objs
是否包含 submodule 檔案,必要時追加顯式規則(見上文)gem install
失敗.so/bundle
rake-compiler
的 lib/core/<major.minor>
隔離,發佈前在多 Ruby 版本驗證