
本文将深入探讨如何从 C 代码中调用 Go 函数,这在构建混合系统时非常有用。通过代码示例,详细解释了如何在 Go 中定义可供 C 调用的函数,以及如何在 C 代码中调用这些 Go 函数。此外,文章还提供了一个更高级的示例,展示如何使用回调函数处理 C 代码中的异步事件,并最终提供一个完整的、可运行的例子,帮助开发者理解并实践这一技术。
从 C 调用 Go 函数的原理
从 C 代码中调用 Go 函数涉及到使用 cgo 工具,它是 Go 的一部分,允许 Go 代码与 C 代码进行互操作。cgo 能够生成必要的桥接代码,使得 C 代码可以调用 Go 函数,反之亦然。其核心在于 //export 注释和 extern 声明,它们分别用于在 Go 代码中标记可导出的函数,以及在 C 代码中声明这些函数。
简单的加法示例
以下是一个简单的示例,演示了如何从 C 代码中调用 Go 函数执行加法运算:
package main // #include// #include // // extern int goCallbackHandler(int, int); // // static int doAdd(int a, int b) { // return goCallbackHandler(a, b); // } import "C" import "fmt" //export goCallbackHandler func goCallbackHandler(a, b C.int) C.int { fmt.Printf("Go: Received %d and %d from C\n", a, b) return a + b } // MyAdd is the public function, callable from outside this package. // It forwards the parameters to C.doAdd(), which in turn forwards // them back to goCallbackHandler(). This one performs the addition // and yields the result. func MyAdd(a, b int) int { return int( C.doAdd( C.int(a), C.int(b)) ) } func main() { result := MyAdd(5, 3) fmt.Printf("Go: Result from C call: %d\n", result) }
在这个例子中:
- //export goCallbackHandler 告诉 cgo 工具导出 goCallbackHandler 函数,使其可以从 C 代码中调用。
- extern int goCallbackHandler(int, int); 在 C 代码中声明了 goCallbackHandler 函数。
- doAdd 是一个 C 函数,它调用了 Go 的 goCallbackHandler 函数。
- MyAdd 是一个 Go 函数,它调用 C 函数 doAdd,从而触发对 Go 函数 goCallbackHandler 的调用。
要编译和运行此示例,你需要创建一个 C 文件(例如 main.c),并在其中调用 MyAdd 函数。然后,使用 go build 命令编译 Go 代码,并使用 C 编译器编译 C 代码,并将它们链接在一起。
使用回调函数处理异步事件
一个更高级的用例是在 C 代码中使用回调函数来处理异步事件。例如,假设你有一个 C 库,它从 USB 设备读取文件列表,并且你想在 Go 代码中显示读取进度。你可以通过传递一个回调函数指针给 C 库来实现这一点。
package main // #include// #include // #include // // typedef void (*ProgressHandler)(uint64_t current, uint64_t total, void* userdata); // // extern int goProgressCB(uint64_t current, uint64_t total, void* userdata); // // static int simulate_get_files(ProgressHandler progress_cb, void* userdata) { // uint64_t total_files = 100; // for (uint64_t i = 0; i < total_files; i++) { // if (progress_cb != NULL) { // progress_cb(i, total_files, userdata); // } // // Simulate some work // for (int j = 0; j < 100000; j++); // } // return 0; // } // // static int goGetFiles(void* userdata) { // return simulate_get_files((ProgressHandler)goProgressCB, userdata); // } import "C" import ( "fmt" "unsafe" ) // ProgressHandler defines the signature of our user's progress handler. type ProgressHandler func(current, total uint64, userdata interface{}) int // progressRequest is an internal type which will pack the user's callback function and userdata. type progressRequest struct { f ProgressHandler // The user's function pointer d interface{} // The user's userdata. } //export goProgressCB func goProgressCB(current, total C.uint64_t, userdata unsafe.Pointer) C.int { req := (*progressRequest)(userdata) return C.int(req.f(uint64(current), uint64(total), req.d)) } // GetFiles is our public function, which is called by the user. func GetFiles(pf ProgressHandler, userdata interface{}) int { req := unsafe.Pointer(&progressRequest{pf, userdata}) return int(C.goGetFiles(req)) } func main() { // We call GetFiles. Pass it our progress handler and some arbitrary userdata. ret := GetFiles(myProgress, "Callbacks rock!") fmt.Printf("Go: GetFiles returned: %d\n", ret) } // myProgress is our progress handler. func myProgress(current, total uint64, userdata interface{}) int { fc := float64(current) ft := float64(total) * 0.01 fmt.Printf("%s: %d / %d (%3.2f%%)\n", userdata.(string), current, total, fc/ft) return 0 }
在这个例子中:
- ProgressHandler 定义了 Go 中回调函数的类型。
- progressRequest 结构体用于将 Go 回调函数和用户数据传递给 C 代码。
- goProgressCB 是一个 Go 函数,它被导出并作为回调函数传递给 C 代码。
- GetFiles 是一个 Go 函数,它调用 C 代码,并将 goProgressCB 函数的指针和用户数据传递给它。
- simulate_get_files 是一个模拟的 C 函数,它模拟从 USB 设备读取文件的过程,并定期调用回调函数。
注意事项
- 类型转换: 在 C 和 Go 之间传递数据时,需要进行类型转换。例如,C.int(a) 将 Go 的 int 类型转换为 C 的 int 类型。
- 内存管理: C 和 Go 有不同的内存管理方式。需要小心处理内存分配和释放,以避免内存泄漏或崩溃。通常,最好在 Go 代码中分配内存,并将指针传递给 C 代码。
- 线程安全: 如果 C 代码在不同的线程中调用 Go 函数,需要确保 Go 代码是线程安全的。可以使用 Go 的并发原语(例如互斥锁)来保护共享资源。
- 错误处理: C 和 Go 的错误处理方式不同。需要在 C 代码中检查错误,并将错误信息传递给 Go 代码。
- 编译选项: 使用 cgo 时,需要设置正确的编译选项,以确保 C 代码和 Go 代码能够正确链接在一起。
总结
从 C 代码中调用 Go 函数是一种强大的技术,可以让你利用 Go 的优势来扩展现有的 C 代码库。但是,它也需要仔细的规划和实施,以避免潜在的问题。通过理解 cgo 的工作原理,并遵循上述注意事项,你可以成功地构建混合系统,并将 C 和 Go 的优势结合起来。










