Go 端的第一個現實問題,並不是 API 長什麼樣,而是「讀者 go get
下來能不能直接編、可不可用」。筆者在設計 Mongory 的 Go Bridge 時,先把「子模組/cgo/同步流程」釐清,確保讀者零設定就能起步
mongory-go
只是引用 mongory-core
當 submodule,讀者 go get
往往會失手筆者的目標很單純:讓讀者只用 Go 的標準流程就能編譯,把複雜度收斂在維運腳本與 CI 端
維運時同步核心的標頭與來源至 cgo/binding/
,讀者端不需要 submodule。關鍵腳本如下:
SYNC_SRC := mongory-core
SYNC_DST := cgo/binding
.PHONY: sync-core clean-core
sync-core:
@git submodule update --init --recursive
@mkdir -p $(SYNC_DST)/include $(SYNC_DST)/src
@rsync -av --delete $(SYNC_SRC)/include/ $(SYNC_DST)/include/
@rsync -av --delete --prune-empty-dirs \
--exclude 'test_helper/**' \
--include '*/' \
--include '*.c' \
--include '*.h' \
--exclude '*' \
$(SYNC_SRC)/src/ $(SYNC_DST)/src/
@echo "Synced headers and C sources to $(SYNC_DST)"
clean-core:
@rm -rf $(SYNC_DST)
@echo "Cleaned $(SYNC_DST)"
sync-core
:
include/
與 src/
的 .h/.c
到 cgo/binding/
test_helper
等非必要內容,縮小體積將所有核心 .c
以單檔匯入,交給 cgo 直接編譯,避免額外的 build system 與參數協調:
// This file aggregates all mongory-core C sources (synced into cgo/binding)
// into a single compilation unit so that cgo can compile automatically.
#include "binding/src/foundations/array.c"
#include "binding/src/foundations/config.c"
#include "binding/src/foundations/error.c"
#include "binding/src/foundations/memory_pool.c"
#include "binding/src/foundations/string_buffer.c"
#include "binding/src/foundations/table.c"
#include "binding/src/foundations/utils.c"
#include "binding/src/foundations/value.c"
#include "binding/src/matchers/array_record_matcher.c"
#include "binding/src/matchers/base_matcher.c"
#include "binding/src/matchers/compare_matcher.c"
#include "binding/src/matchers/composite_matcher.c"
#include "binding/src/matchers/existance_matcher.c"
#include "binding/src/matchers/external_matcher.c"
#include "binding/src/matchers/inclusion_matcher.c"
#include "binding/src/matchers/literal_matcher.c"
#include "binding/src/matchers/matcher_explainable.c"
#include "binding/src/matchers/matcher_traversable.c"
#include "binding/src/matchers/matcher.c"
.c
檔Matcher 封裝(節錄):
func NewMatcher(condition map[string]any, context *any) (*Matcher, error) {
pool := NewMemoryPool()
conditionValue := pool.ConditionConvert(condition)
if conditionValue == nil {
return nil, errors.New(pool.GetError())
}
h := rcgo.NewHandle(context)
pool.trackHandle(h)
cpoint := C.mongory_matcher_new(pool.CPoint, conditionValue.CPoint, handleToPtr(h))
if cpoint == nil {
return nil, errors.New(pool.GetError())
}
return &Matcher{
CPoint: cpoint,
condition: &condition,
context: context,
pool: pool,
scratchPool: NewMemoryPool(),
tracePool: nil,
traceEnabled: false,
}, nil
}
Memory pool 的重置同時釋放 cgo.Handle
,避免洩漏或懸空:
func NewMemoryPool() *MemoryPool {
pool := C.mongory_memory_pool_new()
return &MemoryPool{CPoint: pool, handles: make([]rcgo.Handle, 0)}
}
func (m *MemoryPool) trackHandle(h rcgo.Handle) {
m.handles = append(m.handles, h)
}
func (m *MemoryPool) Reset() {
C.go_mongory_memory_pool_reset(m.CPoint)
for _, h := range m.handles {
h.Delete()
}
m.handles = m.handles[:0]
}
Shallow to_string 的橋接,維持觀測友善:
// Forward declarations for shallow to_string
extern char *go_shallow_array_to_string(void *go_array);
extern char *go_shallow_table_to_string(void *go_table);
static char *cgo_shallow_array_to_string(mongory_value *v, mongory_memory_pool *pool) {
return go_shallow_array_to_string(v->data.a);
}
static char *cgo_shallow_table_to_string(mongory_value *v, mongory_memory_pool *pool) {
return go_shallow_table_to_string(v->data.t);
}
static void mongory_value_set_array_to_string(mongory_value *v) {
v->to_str = cgo_shallow_array_to_string;
}
static void mongory_value_set_table_to_string(mongory_value *v) {
v->to_str = cgo_shallow_table_to_string;
}
make sync-core
,把 mongory-core
的標頭與來源拷貝到 cgo/binding/
go build ./...
,cgo 會自動以 core_all.c
編譯核心程式碼sync-core
,必要時 make clean-core
清乾淨後再同步go test
與 explain/trace
驗證可觀測性是否正常runtime/cgo.Handle
與 pool 生命週期管理筆者把「子模組與 cgo 的複雜度」收斂在維運腳本與單檔編譯單元,讓讀者在 Go 端只需 go build
就能使用 Mongory 的匹配能力。下一篇,筆者會面對 Go 型別反射與 converter 的設計邊界,談如何延續 shallow/deep/recover 讓效能與易用性同時成立