如何通过cgo在Go中向C函数传递回调与上下文(DNSService场景)
问题解决方案
1. 链接器重复符号问题
直接在C代码中将queryDNS声明为static即可解决。原因是cgo编译时,全局C函数会被多个编译单元共享,导致链接时出现重复定义;static关键字会将函数限定在当前编译单元内,避免符号冲突。这是正确且必要的做法,无需修改为非static。
2. cgo指针panic问题
错误runtime error: cgo argument has Go pointer to unpinned Go pointer是因为违反了cgo指针规则:不能将包含未固定Go指针的Go值传递给C。以下是修正步骤:
核心思路
- 避免直接传递包含Go指针的结构体/函数给C,改用
uintptr包装需要被C持有的Go值(如通知用的channel)。 - 使用
runtime.Pinner固定该Go值,确保GC不会在C使用期间回收它。 - 在C回调中,将
uintptr转换回Go值,完成通知逻辑。
修正后的代码示例
package main import ( "fmt" "runtime" "unsafe" ) /* #include <dns_sd.h> #include <stdint.h> // 声明为static,解决链接重复符号问题 static void goCallback(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *fullName, uint16_t rrtype, uint16_t rrclass, uint16_t rdlen, const void *rdata, uint32_t ttl, void *context) { // 将C上下文转换为Go的channel指针 chan struct{} *doneChan = (chan struct{} *)(uintptr)context; *doneChan <- (struct{}){}; } // 声明为static,解决链接重复符号问题 static DNSServiceErrorType queryDNS(DNSServiceRef *sdRef, uint32_t interfaceIndex, DNSServiceFlags flags, const char *fullName, uint16_t rrtype, uint16_t rrclass, void *context) { return DNSServiceQueryRecord(sdRef, interfaceIndex, flags, fullName, rrtype, rrclass, goCallback, context); } */ import "C" func QueryDNS(fullName string) error { // 创建用于通知查询完成的channel done := make(chan struct{}, 1) // 固定channel,防止GC在C使用期间回收它 pinner := runtime.Pinner{} pinner.Pin(done) defer pinner.Unpin() // 回调完成后释放固定 // 转换Go字符串为C字符串,使用后释放内存 cFullName := C.CString(fullName) defer C.free(unsafe.Pointer(cFullName)) var sdRef C.DNSServiceRef // 将固定后的channel指针转成uintptr再传递给C,规避cgo指针检测 errCode := C.queryDNS(&sdRef, 0, 0, cFullName, C.kDNSServiceType_TXT, C.kDNSServiceClass_IN, unsafe.Pointer(uintptr(unsafe.Pointer(done)))) if errCode != 0 { return fmt.Errorf("DNS query failed with code: %d", errCode) } defer C.DNSServiceRefDeallocate(sdRef) // 启动DNS事件循环,驱动回调触发 for { select { case <-done: return nil default: // 处理DNS服务的待处理结果 C.DNSServiceProcessResult(sdRef) // 让出CPU给其他goroutine,避免阻塞 runtime.Gosched() } } } func main() { err := QueryDNS("example.com") if err != nil { fmt.Println(err) } }
关键注意点
- Pinner生命周期:必须在C不再使用Go值之前保持固定状态,所以
defer pinner.Unpin()要放在函数最后,确保回调执行完毕后才释放。 - 指针转换逻辑:将Go值的指针转成
uintptr再传给C,避免cgo检测到嵌套的Go指针结构。 - 事件循环必要性:必须调用
DNSServiceProcessResult来驱动DNS服务的回调触发,否则回调永远不会执行。
内容的提问来源于stack exchange,提问作者Vinicius Fortuna
相关产品推荐
相关产品推荐

