cgo 也是一个 Go 语言自带的特殊工具。一般情况下,我们使用命令 go tool cgo 来运行它。这个工具可以使我们创建能够调用 C 语言代码的 Go 语言源码文件。这使得我们可以使用 Go 语言代码去封装一些 C 语言的代码库,并提供给 Go 语言代码或项目使用。
在执行 go tool cgo 命令的时候,我们需要加入作为目标的 Go 语言源码文件的路径。这个路径可以是绝对路径也可以是相对路径。但是,作者强烈建议在目标源码文件所属的代码包目录下执行 go tool cgo 命令并以目标源码文件的名字作为参数。因为,go tool cgo 命令会在当前目录(也就是我们执行 go tool cgo 命令的目录)中生成一个名为 _obj 的子目录。该目录下会包含一些 Go 源码文件和 C 源码文件。这个子目录及其包含的文件理应被保存在目标代码包目录之下。至于原因,我们稍后再做解释。
我们现在来看可以作为 go tool cgo 命令参数的Go语言源码文件。这个源码文件必须要包含一行只针对于代码包 C 的导入语句。其实,Go 语言标准库中并不存在代码包 C。代码包 C 是一个伪造的代码包。导入这个代码包是为了告诉 cgo 工具在这个源码文件中需要调用 C 代码,同时也是给予 cgo 所产生的代码一个专属的命名空间。除此之外,我们还需要在这个代码包导入语句之前加入一些注释,并且在注释行中写出我们真正需要使用的C语言接口文件的名称。像这样:
// #include <stdlib.h> import "C"
在 Go 语言的规范中,把在代码包 C 导入语句之前的若干注释行叫做序文(preamble)。 在引入了 C 语言的标准代码库 stdlib.h 之后,我们就可以在后面的源码中调用这个库中的接口了。像这样:
func Random() int { return int(C.rand()) } func Seed(i int) { C.srand(C.uint(i)) }
我们把上述的这些 Go 语言代码写入 Go 语言的库源码文件 rand.go 中,并将这个源码文件保存在 goc2 项目的代码包 basic/cgo/lib 的对应目录中。
在 Go 语言源码文件 rand.go 中对代码包 C 有四处引用,分别是三个函数调用语句 C.rand、C.srand 和 C.uint,以及一个导入语句 import "C"。其中,在 Go 语言函数 Random 中调用了 C 语言标准库代码中的函数 rand 并返回了它的结果。但是,C 语言的 rand 函数返回的结果的类型是 C 语言中的 int 类型。在 cgo 工具的环境中,C 语言中的 int 类型与 C.int 相对应。作为一个包装 C 语言接口的函数,我们必须将代码包 C 的使用限制在当前代码包内。也就是说,我们必须对当前代码包之外的 Go 代码隐藏代码包 C。这样做也是为了遵循代码隔离原则。我们在设计接口或者接口适配程序的时候经常会用到这种方法。因此,rand 函数的结果的类型必须是 Go 语言的。所以,我们在这里使用函数 int 对 C.int 类型的 C 语言接口的结果进行了转换。当然,为了更清楚的表达,我们也可以将函数 Random 中的代码return int(C.rand()) 拆分成两行,像这样:
var r C.int = C.rand() return int(r)
而 Go 语言函数 Seed 则恰好相反。C 语言标准代码库中的函数 srand 接收一个参数,且这个参数的类型必须为 C 语言的 uint 类型,即 C.uint。而 Go 语言函数 Seed 的参数为 Go 语言的 int 类型。为此,我们需要使用代码包 C 的函数 unit 对其进行转换。
实际上,标准 C 语言的数字类型都在 cgo 工具中有对应的名称,包括:C.char、C.schar(有符号字符类型)、C.uchar(无符号字符类型)、C.short、C.ushort(无符号短整数类型)、C.int、C.uint(无符号整数类型)、C.long、C.ulong(无符号长整数类型)、C.longlong(对应于 C 语言的类型 long long,它是在 C 语言的C99标准中定义的新整数类型)、C.ulonglong(无符号的long long类型)、C.float和C.double。另外,C 语言类型 void* 对应于 Go 语言的类型 unsafe.Pointer。
如果想直接访问 C 语言中的 struct、union 或 enum 类型的话,就需要在名称前分别加入前缀 struct_、union 或 enum。比如,我们需要在 Go 源码文件中访问C语言代码中的名为 command 的 struct 类型的话,就需要这样写:C.structcommand。那么,如果我们想在Go语言代码中访问C语言类型struct中的字段需要怎样做呢?解决方案是,同样以 C 语言类型 struct 的实例名以及选择符“.”作为前导,但需要在字段的名称前加入下划线“”。例如,如果 command1 是名为 command 的 C 语言 struct 类型的实例名,并且这个类型中有一个名为 name 的字段,那么我们在 Go 语言代码中访问这个字段的方式就是command1._name。需要注意的是,我们不能在 Go 的 struct 类型中嵌入 C 语言类型的字段。这与我们在前面所说的代码隔离原则具有相同的意义。
在上面展示的库源码文件 rand.go 中有多处对 C 语言函数的访问。实际上,任何 C 语言的函数都可以 被 Go 语言代码调用。只要在源码文件中导入了代码包 C。并且,我们还可以同时取回 C 语言函数的结果,以及一个作为错误提示信息的变量。这与我们在 Go 语言中同时获取多个函数结果的方法一样。同样的,我们可以使用下划线“_”直接丢弃取回的值。这在调用无结果的 C 语言函数时非常有用。请看下面的例子:
package cgo /* #cgo LDFLAGS: -lm #include <math.h> */ import "C" func Sqrt(p float32) (float32, error) { n, err := C.sqrt(C.double(p)) return float32(n), err }
上面这段代码被保存在了 Go 语言库源码文件 math.go 中,并与源码文件 rand.go 在同一个代码包目录。在 Go 语言函数 Sqrt 中的 C.sqrt 是一个在 C 语言标准代码库 math.h 中的函数。它会返回参数的平方根。但是在第一行代码中,我们接收由函数 C.sqrt 返回的两个值。其中,第一个值即为 C 语言函数 sqrt 的结果。而第二个值就是我们上面所说的那个作为错误提示信息的变量。实际上,这个变量的类型是 Go 语言的 error 接口类型。它包装了一个 C 语言的全局变量 errno。这个全局变量被定义在了 C 语言代码库 errno.h中。cgo 工具在为我们生成 C 语言源码文件时会默认引入两个 C 语言标准代码库,其中一个就是 errno.h。所以我们并不用在 Go 语言源码文件中使用指令符 #include 显式的引入这个代码库。cgo 工具默认为我们引入的另一个是 C 语言标准代码库 string.h。它包含了很多用于字符串处理和内存处理的函数。
在我们以“C.*”的形式调用 C 语言代码库时,有一点需要特别注意。在 C 语言中,如果一个函数的参数是一个具有固定尺寸的数组,那么实际上这个函数所需要的是指向这个数组的第一个元素的指针。C 编译器能够正确识别和处理这个调用惯例。它可以自行获取到这个指针并传给函数。但是,这在我们使用 cgo 工具调用 C 语言代码库时是行不通的。在 Go 语言中,我们必须显式的将这个指向数组的第一个元素的指针传递给C语言的函数,像这样:C.func1(&x[0])。
另一个需要特别注意的地方是,在 C 语言中没有像 Go 语言中独立的字符串类型。C 语言使用最后一个元素为‘