通过cgo调用C代码

Go 程序可能会遇到要访问 C 语言的某些硬件驱动函数的场景,或者是从一个 C++ 语言实现的嵌入式数据库查询记录的场景,或者是使用 Fortran 语言实现的一些线性代数库的场景。C 语言作为一个通用语言,很多库会选择提供一个 C 兼容的 API ,然后用其他不同的编程语言实现(译者:Go 语言需要也应该拥抱这些巨大的代码遗产)。

在本节中,我们将构建一个简易的数据压缩程序,使用了一个 Go 语言自带的叫 cgo 的用于支援 C 语言函数调用的工具。这类工具一般被称为 foreign-function interfaces (简称ffi),并且在类似工具中 cgo 也不是唯一的。SWIG(http://swig.org)是另一个类似的且被广泛使用的工具,SWIG 提供了很多复杂特性以支援 C++ 的特性,但 SWIG 并不是我们要讨论的主题。

在标准库的 compress/…​ 子包有很多流行的压缩算法的编码和解码实现,包括流行的 LZW 压缩算法(Unix 的 compress 命令用的算法)和 DEFLATE 压缩算法(GNU gzip 命令用的算法)。这些包的 API 的细节虽然有些差异,但是它们都提供了针对 io.Writer 类型输出的压缩接口和提供了针对 io.Reader 类型输入的解压缩接口。例如:

package gzip // compress/gzip
func NewWriter(w io.Writer) io.WriteCloser
func NewReader(r io.Reader) (io.ReadCloser, error)

bzip2 压缩算法,是基于优雅的 Burrows-Wheeler 变换算法,运行速度比 gzip 要慢,但是可以提供更高的压缩比。标准库的 compress/bzip2 包目前还没有提供 bzip2 压缩算法的实现。完全从头开始实现一个压缩算法是一件繁琐的工作,而且 http://bzip.org 已经有现成的 libbzip2 的开源实现,不仅文档齐全而且性能又好。

如果是比较小的 C 语言库,我们完全可以用纯 Go 语言重新实现一遍。如果我们对性能也没有特殊要求的话,我们还可以用 os/exec 包的方法将 C 编写的应用程序作为一个子进程运行。只有当你需要使用复杂而且性能更高的底层 C 接口时,就是使用 cgo 的场景了(译注:用 os/exec 包调用子进程的方法会导致程序运行时依赖那个应用程序)。下面我们将通过一个例子讲述 cgo 的具体用法。

译注:本章采用的代码都是最新的。因为之前已经出版的书中包含的代码只能在 Go1.5 之前使用。从 Go1.6 开始,Go 语言已经明确规定了哪些 Go 语言指针可以直接传入 C 语言函数。新代码重点是增加了 bz2alloc 和 bz2free 的两个函数,用于 bz_stream 对象空间的申请和释放操作。下面是新代码中增加的注释,说明这个问题:

// The version of this program that appeared in the first and second
// printings did not comply with the proposed rules for passing
// pointers between Go and C, described here:
// https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md
//
// The rules forbid a C function like bz2compress from storing 'in'
// and 'out' (pointers to variables allocated by Go) into the Go
// variable 's', even temporarily.
//
// The version below, which appears in the third printing, has been
// corrected.  To comply with the rules, the bz_stream variable must
// be allocated by C code.  We have introduced two C functions,
// bz2alloc and bz2free, to allocate and free instances of the
// bz_stream type.  Also, we have changed bz2compress so that before
// it returns, it clears the fields of the bz_stream that contain
// pointers to Go variables.

要使用 libbzip2,我们需要先构建一个 bz_stream 结构体,用于保持输入和输出缓存。然后有三个函数: BZ2_bzCompressInit 用于初始化缓存,BZ2_bzCompress 用于将输入缓存的数据压缩到输出缓存, BZ2_bzCompressEnd 用于释放不需要的缓存。(目前不要担心包的具体结构,这个例子的目的就是演示各个部分如何组合在一起的。)

我们可以在 Go 代码中直接调用 BZ2_bzCompressInit 和 BZ2_bzCompressEnd,但是对于 BZ2_bzCompress,我们将定义一个 C 语言的包装函数,用它完成真正的工作。下面是 C 代码,对应一个独立的文件。

ch13/bzip
Unresolved include directive in modules/ROOT/pages/ch13/ch13-04.adoc - include::example$/ch13/bzip/bzip2.c[]

现在让我们转到 Go 语言部分,第一部分如下所示。其中 import "C" 的语句是比较特别的。其实并没有一个叫 C 的包,但是这行语句会让 Go 编译程序在编译之前先运行 cgo 工具。

ch13/bzip
Unresolved include directive in modules/ROOT/pages/ch13/ch13-04.adoc - include::example$/ch13/bzip/bzip2.go[]

在预处理过程中,cgo 工具生成一个临时包用于包含所有在 Go 语言中访问的 C 语言的函数或类型。例如 C.bz_stream 和 C.BZ2_bzCompressInit。cgo 工具通过以某种特殊的方式调用本地的 C 编译器来发现在 Go 源文件导入声明前的注释中包含的 C 头文件中的内容(译注:import "C"语句前紧挨着的注释是对应 cgo 的特殊语法,对应必要的构建参数选项和 C 语言代码)。

在 cgo 注释中还可以包含 #cgo 指令,用于给 C 语言工具链指定特殊的参数。例如 CFLAGS 和 LDFLAGS 分别对应传给 C 语言编译器的编译参数和链接器参数,使它们可以从特定目录找到 bzlib.h 头文件和 libbz2.a 库文件。这个例子假设你已经在 /usr 目录成功安装了 bzip2 库。如果 bzip2 库是安装在不同的位置,你需要更新这些参数(译注:这里有一个从纯 C 代码生成的 cgo 绑定,不依赖 bzip2 静态库和操作系统的具体环境,具体请访问 https://github.com/chai2010/bzip2 )。

NewWriter 函数通过调用 C 语言的 BZ2_bzCompressInit 函数来初始化 stream 中的缓存。在 writer 结构中还包括了另一个 buffer,用于输出缓存。

下面是 Write 方法的实现,返回成功压缩数据的大小,主体是一个循环中调用 C 语言的 bz2compress 函数实现的。从代码可以看到,Go 程序可以访问 C 语言的 bz_stream、char 和 uint 类型,还可以访问 bz2compress 等函数,甚至可以访问 C 语言中像 BZ_RUN 那样的宏定义,全部都是以 C.x 语法访问。其中 C.uint 类型和 Go 语言的 uint 类型并不相同,即使它们具有相同的大小也是不同的类型。

ch13/bzip
Unresolved include directive in modules/ROOT/pages/ch13/ch13-04.adoc - include::example$/ch13/bzip/bzip2.go[]

在循环的每次迭代中,向 bz2compress 传入数据的地址和剩余部分的长度,还有输出缓存 w.outbuf 的地址和容量。这两个长度信息通过它们的地址传入而不是值传入,因为 bz2compress 函数可能会根据已经压缩的数据和压缩后数据的大小来更新这两个值。每个块压缩后的数据被写入到底层的 io.Writer。

Close 方法和 Write 方法有着类似的结构,通过一个循环将剩余的压缩数据刷新到输出缓存。

ch13/bzip
Unresolved include directive in modules/ROOT/pages/ch13/ch13-04.adoc - include::example$/ch13/bzip/bzip2.go[]

压缩完成后,Close 方法用了 defer 函数确保函数退出前调用 C.BZ2_bzCompressEnd 和 C.bz2free 释放相关的 C 语言运行时资源。此刻 w.stream 指针将不再有效,我们将它设置为 nil 以保证安全,然后在每个方法中增加了 nil 检测,以防止用户在关闭后依然错误使用相关方法。

上面的实现中,不仅仅写是非并发安全的,甚至并发调用 Close 和 Write 方法也可能导致程序的的崩溃。修复这个问题是练习 13.3 的内容。

下面的 bzipper 程序,使用我们自己包实现的 bzip2 压缩命令。它的行为和许多 Unix 系统的 bzip2 命令类似。

ch13/bzipper
Unresolved include directive in modules/ROOT/pages/ch13/ch13-04.adoc - include::example$/ch13/bzipper/main.go[]

在上面的场景中,我们使用 bzipper 压缩了 /usr/share/dict/words 系统自带的词典,从 938,848 字节压缩到 335,405 字节。大约是原始数据大小的三分之一。然后使用系统自带的 bunzip2 命令进行解压。压缩前后文件的 SHA256 哈希码是相同了,这也说明了我们的压缩工具是正确的。(如果你的系统没有 sha256sum 命令,那么请先按照练习 4.2 实现一个类似的工具)

$ go build gopl.io/ch13/bzipper
$ wc -c < /usr/share/dict/words
938848
$ sha256sum < /usr/share/dict/words
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -
$ ./bzipper < /usr/share/dict/words | wc -c
335405
$ ./bzipper < /usr/share/dict/words | bunzip2 | sha256sum
126a4ef38493313edc50b86f90dfdaf7c59ec6c948451eac228f2f3a8ab1a6ed -

我们演示了如何将一个 C 语言库链接到 Go 语言程序。相反,将 Go 编译为静态库然后链接到 C 程序,或者将 Go 程序编译为动态库然后在 C 程序中动态加载也都是可行的(译注:在Go1.5中,Windows 系统的 Go 语言实现并不支持生成 C 语言动态库或静态库的特性。不过好消息是,目前已经有人在尝试解决这个问题,具体请访问 Issue11058 )。这里我们只展示的 cgo 很小的一些方面,更多的关于内存管理、指针、回调函数、中断信号处理、字符串、errno处理、终结器,以及 goroutines 和系统线程的关系等,有很多细节可以讨论。特别是如何将 Go 语言的指针传入 C 函数的规则也是异常复杂的(译注:简单来说,要传入 C 函数的 Go 指针指向的数据本身不能包含指针或其他引用类型;并且 C 函数在返回后不能继续持有 Go 指针;并且在 C 函数返回之前,Go 指针是被锁定的,不能导致对应指针数据被移动或栈的调整),部分的原因在13.2节有讨论到,但是在 Go1.5 中还没有被明确(译注:Go1.6 将会明确 cgo 中的指针使用规则)。如果要进一步阅读,可以从 https://golang.org/cmd/cgo 开始。


练习 13.3: 使用 sync.Mutex 以保证 bzip2.writer 在多个 goroutines 中被并发调用是安全的。

练习 13.4: 因为 C 库依赖的限制。 使用 os/exec 包启动 /bin/bzip2 命令作为一个子进程,提供一个纯 Go 的 bzip.NewWriter 的替代实现(译注:虽然是纯 Go 实现,但是运行时将依赖 /bin/bzip2 命令,其他操作系统可能无法运行)。