13.4. 通过cgo调用C代码

Go程序可能会遇到要访问C语言的某些硬件驱动的场景, 或者是从一个C++实现的嵌入式数据库查询记录的场景, 或者是使用Fortran实现的一些线性代数库的场景. C作为一个通用语言, 很多库会选择提供一个C兼容的API, 然后用其他语言实现.

在本节中, 我们将构建一个简易的数据压缩程序, 通过使用一个Go语言自带的叫cgo的用于支援C语言函数调用的工具. 这类工具被称为外围函数接口(ffi), 并且cgo也不是Go中唯一的类似工具. SWIG(swig.org) 是类似的另一个被广泛使用的工具, 它提供了很多复杂特性以支援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 算法的压缩实现. 完全从头实现是一个繁琐的工作, 而且 bzip.org 有现成的 libbzip2 开源实现, 文档齐全而且性能较好,

如果C库比较小, 我们可以用纯Go重新实现一遍. 如果我们对性能没有特殊要求, 我们可以用 os/exec 包的方法将C编写的应用程序作为一个子进行运行. 只有当你需要使用复杂但是性能更高的底层C接口时, 就是使用cgo的场景了. 下面我们将通过一个例子讲述cgo的用法.

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

我们可以在Go代码中直接调用 BZ2_bzCompressInit 和 BZ2_bzCompressEnd, 但是对于 BZ2_bzCompress, 我们将定义一个C语言的包装函数, 为了显示他是如何完成的. 下面是C代码, 对应一个独立的文件.

gopl.io/ch13/bzip

/* This file is gopl.io/ch13/bzip/bzip2.c,         */
/* a simple wrapper for libbzip2 suitable for cgo. */
#include <bzlib.h>

int bz2compress(bz_stream *s, int action,
                char *in, unsigned *inlen, char *out, unsigned *outlen) {
    s->next_in = in;
    s->avail_in = *inlen;
    s->next_out = out;
    s->avail_out = *outlen;
    int r = BZ2_bzCompress(s, action);
    *inlen -= s->avail_in;
    *outlen -= s->avail_out;
    return r;
}

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

// Package bzip provides a writer that uses bzip2 compression (bzip.org).
package bzip

/*
#cgo CFLAGS: -I/usr/include
#cgo LDFLAGS: -L/usr/lib -lbz2
#include <bzlib.h>
int bz2compress(bz_stream *s, int action,
                char *in, unsigned *inlen, char *out, unsigned *outlen);
*/
import "C"

import (
    "io"
    "unsafe"
)

type writer struct {
    w      io.Writer // underlying output stream
    stream *C.bz_stream
    outbuf [64 * 1024]byte
}

// NewWriter returns a writer for bzip2-compressed streams.
func NewWriter(out io.Writer) io.WriteCloser {
    const (
        blockSize = 9
        verbosity = 0
        workFactor = 30
    )
    w := &writer{w: out, stream: new(C.bz_stream)}
    C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
    return w
}

在循环的每次迭代中, 向bz2compress传入数据的地址和剩余部分的长度, 还有输出缓存 w.outbuf 的地址和容量. 这两个长度信息通过它们的地址传入而不是值传入, 因为bz2compress函数可能会根据已经压缩的数据和压缩后数据的大小来更新这两个值(译注: 这里的用法有问题, 勘误已经提到. 具体修复的方法稍后再补充). 每个块压缩后的数据被写入到底层的 io.Writer.

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

// Close flushes the compressed data and closes the stream.
// It does not close the underlying io.Writer.
func (w *writer) Close() error {
    if w.stream == nil {
        panic("closed")
    }
    defer func() {
        C.BZ2_bzCompressEnd(w.stream)
        w.stream = nil
    }()
    for {
        inlen, outlen := C.uint(0), C.uint(cap(w.outbuf))
        r := C.bz2compress(w.stream, C.BZ_FINISH, nil, &inlen,
            (*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
        if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
            return err
        }
        if r == C.BZ_STREAM_END {
            return nil
        }
    }
}

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

不仅仅写是非并发安全的, 甚至并发调用 Close 和 Write 也可能导致C代码的崩溃. 修复这个问题是 练习13.3 的内容.

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

gopl.io/ch13/bzipper

// Bzipper reads input, bzip2-compresses it, and writes it out.
package main

import (
    "io"
    "log"
    "os"
    "gopl.io/ch13/bzip"
)

func main() {
    w := bzip.NewWriter(os.Stdout)
    if _, err := io.Copy(w, os.Stdin); err != nil {
        log.Fatalf("bzipper: %v\n", err)
    }
    if err := w.Close(); err != nil {
        log.Fatalf("bzipper: close: %v\n", err)
    }
}

在上面的场景中, 我们使用 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程序中动态加载也都是可行的. 这里我们只展示的cgo很小的一些方面, 更多的关于内存管理, 指针, 回调函数, 信号处理, 字符串, errno处理, 终结器, 以及 goroutines 和系统线程的关系等, 有很多细节可以讨论. 特别是如何将Go的指针传入C函数的规则也是异常复杂的, 部分的原因在 13.2节 有讨论到, 但是在Go1.5中还没有被明确. 如果要进一步阅读, 可以从 https://golang.org/cmd/cgo 开始.

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

练习13.4: 因为C库依赖的限制. 使用 os/exec 包启动 /bin/bzip2 命令作为一个子进程, 提供一个纯Go的 bzip.NewWriter 的替代实现.