在 Go 1.17 发行版中,我们贡献了一个新的 cgo 设施runtime/cgo.Handle,以帮助未来的 cgo 应用在 Go 和 C 之间传递指针的同时,更好、更容易地构建并发安全的应用。本文将通过询问该功能为我们提供了什么,为什么我们需要这样一个设施,以及我们最终究竟如何贡献具体实现来引导我们了解这个功能。
从 Cgo 和 X Window 剪贴板开始
Cgo 是在 Go 中与 C 语言设施进行交互的事实标准。然而,我们有多少次需要在 Go 中与 C 语言进行交互呢?这个问题的答案取决于我们在系统层面上的工作程度,或者我们有多少次需要利用传统的 C 库,比如用于图像处理。每当 Go 程序需要使用一个来自C
的遗留物时,它需要导入一种 C
的专用包,如下所示,然后在 Go 方面,人们可以通过导入的C
符号简单地调用myprint
函数。
/*
#include <stdio.h>
void myprint() {
printf("Hello %s", "World");
}
*/
import "C"
func main() {
C.myprint()
// Output:
// Hello World
}
几个月前,当我们在建立一个提供跨平台剪贴板访问的新的包golang.design/x/clipboard
时, 我们发现,Go 中缺乏这样的设施,尽管有很多野路子,但仍然存在健全性和性能问题。
在golang.design/x/clipboard
软件包中,我们不得不与 cgo 合作来访问系统级的 API(从技术上讲,它是一个来自传统的、广泛使用的 C 系统的 API),但缺乏在 C 代码侧了解执行进度的设施。例如,在 Go 代码这边,我们必须在一个 goroutine 中调用 C 代码,然后并行地做其他事情。
go func() {
C.doWork() // Cgo: 调用一个C函数,并在C代码侧做一些事情
}()
// .. 在Go代码侧做点事 ..
然而,在某些情况下,我们需要一种机制来了解 C 代码的执行进度,这就带来了 Go 和 C 之间通信和同步的需要。例如,如果我们需要我们的 Go 代码等待 C 代码完成一些初始化工作,直到某个执行点执行,我们将需要这种类型的通信来精确的了解 C 函数的执行进度。
我们遇到的一个真实的例子是需要与剪贴板设施互动。在 Linux 的X Window 环境中,剪贴板是分散的,只能由每个应用程序拥有。需要访问剪贴板信息的人需要创建他们的剪贴板实例。假设一个应用程序A
想把某些东西粘贴到剪贴板上,它必须向 X Window 服务器提出请求,然后成为剪贴板的所有者,在其他应用程序发出复制请求时把信息送回去。
这种设计被认为是自然的,经常需要应用程序进行合作。如果另一个应用程序B
试图提出请求,成为剪贴板的下一个所有者,那么A
将失去其所有权。之后,来自应用程序C
、D
等的复制请求将被转发给应用程序B
而不是A
。类似于一个共享的内存区域被别人覆盖了,而原来的所有者失去了访问权。
根据以上的上下文信息,我们可以理解,在一个应用程序开始 "粘贴"(服务)剪贴板信息之前,它首先要获得剪贴板的所有权。在我们获得所有权之前,剪贴板信息将不能用于访问目的。换句话说,如果一个剪贴板 API 被设计成如下方式:
clipboard.Write("某些信息")
我们必须从内部保证,当函数返回时,信息应该可以被其他应用程序访问。
当时,我们处理这个问题的第一个想法是,从 Go 到 C 传递一个channel
,然后通过channel
从 C 到 Go 发送一个值。经过快速的研究,我们意识到这是不可能的,因为由于Cgo 中传递指针的规则,channel
不能作为一个值在 C 和 Go 之间传递(见之前的提案文件)。即使有办法将整个channel的值传递给 C,在 C 代码侧也没有设施可以通过该channel
发送值,因为 C 没有<-
操作符的语言支持。
下一个想法是传递一个函数回调,然后让它在 C 代码侧被调用。该函数的执行将使用所需的channel
向等待的 goroutine 发送一个通知。
经过几次尝试,我们发现唯一可能的方法是附加一个全局函数指针,并通过一个函数包装器使其被调用:
/*
int myfunc(void* go_value);
*/
import "C"
// 这个funcCallback试图避免在运行时直接出现恐慌错误。
// 传递给Cgo,因为它违反了指针传递规则:
//
// panic: runtime error: cgo argument has Go pointer to Go pointer
var (
funcCallback func()
funcCallbackMu sync.Mutex
)
type gocallback struct{ f func() }
func main() {
go func() {
ret := C.myfunc(unsafe.Pointer(&gocallback{func() {
funcCallbackMu.Lock()
f := funcCallback // 必须使用一个全局函数变量。
funcCallbackMu.Unlock()
f()
}}))
// ... 搞事 ...
}()
// ... 搞事 ...
}
在上面的例子中,Go 一方的gocallback
指针是通过 C 函数myfunc
传递的。在 C 代码侧,将有一个使用go_func_callback
的调用,通过传递结构gocallback
作为参数,在 C 代码侧被调用:
// myfunc将在需要时触发一个回调,c_func,并通过void*参数传递
// gocallback的数据通过void*参数。
void c_func(void *data) {
void *gocallbac