iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
佛心分享-SideProject30

Mongory:打造跨語言、高效能的萬用查詢引擎系列 第 26

Day 25:Go ↔ C:unsafe.Pointer/cgo.Handle 的正確姿勢

  • 分享至 

  • xImage
  •  

Go 與 C 溝通時,指標與生命週期是第一原則。這篇以 Mongory 的 Go Bridge 為例,整理出在實作中踩過、避過的雷,並給出可直接對照的實作片段


unsafe.Pointer:只是無型別指標的「容器」

  • 只能作為暫時中介,嚴禁把帶有 Go 物件生命週期的指標長期停放在 C 端
  • 需要從 Go 帶資料到 C、或從 C 帶 token 回 Go 時,搭配 cgo.Handle 轉成/還原 Go 值

對應工具函式(節錄):

void *go_handle_to_ptr(uintptr_t h) { return (void *)h; }
uintptr_t go_ptr_to_handle(void *p) { return (uintptr_t)p; }
...
func handleToPtr(h rcgo.Handle) unsafe.Pointer {
    return C.go_handle_to_ptr(C.uintptr_t(uintptr(h)))
}

func ptrToHandle(ptr unsafe.Pointer) rcgo.Handle {
    return rcgo.Handle(C.go_ptr_to_handle(ptr))
}

cgo.Handle:跨界保存/取回 Go 值的唯一正道

  • 用途:把任意 Go 值封裝為 Handle,轉成 unsafe.Pointer 傳進 C,日後 C 回傳再還原為 Handle 取回 Go 值
  • 關鍵點:Handle 必須由擁有者生命週期統一釋放(Delete),避免洩漏與誤用

在 Mongory 中,所有 Handle 都掛在記憶體池(MemoryPool)上,跟著 Reset/Free 一起回收:

type MemoryPool struct {
    CPoint  *C.mongory_memory_pool
    handles []rcgo.Handle
}
...
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]
}

正確姿勢一:將 Go 值交給 C 端暫存(token 化)

以建立 Matcher 時傳入「context」為例:

h := rcgo.NewHandle(context)
pool.trackHandle(h)
cpoint := C.mongory_matcher_new(pool.CPoint, conditionValue.CPoint, handleToPtr(h))
  • 只傳 handleToPtr(h)(token),不直接傳 Go 指標
  • MemoryPool 管理 Handle 的回收時機,避免遺漏

正確姿勢二:C 端回呼時,還原 Go 值並做 shallow 取值

Shallow Array 的 get

pool := MemoryPool{CPoint: a.base.pool}
h := ptrToHandle(a.go_array)
target := h.Value()
rv := reflect.ValueOf(target)
var iv any
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) {
    iv = rv.Index(int(index)).Interface()
}
return pool.ValueConvert(iv).CPoint
  • 透過 ptrToHandle 還原 Go 值,短生命週期使用
  • 取到元素後立刻轉回 mongory_value,避免把 Go 值持久化在 C 裡

Shallow Table 的 get 同理:

h := ptrToHandle(a.go_table)
target := h.Value()
rv := reflect.ValueOf(target)
var iv any
if rv.IsValid() && rv.Kind() == reflect.Map {
    v := rv.MapIndex(reflect.ValueOf(C.GoString(key)))
    if v.IsValid() { iv = v.Interface() }
}
return pool.ValueConvert(iv).CPoint

常見陷阱與對策

  • 不要從 C 端長期保存 Go 指標:只能保存 Handle token,生命週期由 Go 端管理
  • 別忘了 Delete Handle:跟著 pool.Reset/Free 一起清
  • 別把 Go 物件直接掛到 C 的結構裡:以 void * 保存 token 已足夠,取用時再還原
  • 反射帶來的成本:在 shallow 取值路徑才做反射,避開熱路徑的巨量複製

讀者如何安全使用(簡)

  • 建立 matcher 後可重複使用,多筆資料呼叫 Match,避免重複建構
  • 若需要自訂 context,直接傳入任意 Go 值即可,安全性由內部 Handle 管控
  • 不需要接觸 unsafe.Pointercgo.Handle——這些細節已封裝在 API 內

小結

在 Go 與 C 的邊界上,正確姿勢是:以 Handle 作為唯一的跨界「代幣」,以 pool 管理生命週期,所有指標都以 unsafe.Pointer 作短暫通道。這能兼顧安全、可維運與效能,讓讀者專注在條件與資料本身


上一篇
Day 24:Go 型別反射與 data converter 困境:設計邊界
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言