使用go与c进行混合编程。

cgo是外部函数接口(FFI),用来为C函数创建Go绑定的工具,除了cgo之外还有SWIG。

这里使用cgo构建一个简单的数据压缩程序。

有一个C语言实现的bzip2算法的压缩库,来自bzip.org的libbzip2包。C库如果小的话,可以使用纯Go进行移植,性能不关注的话可以使用os/exec以辅助子进程的方式来调用C程序。

但是当C库的API优先并且性能关键,实现复杂的时候使用cgo非常有意义。

在C库libbzip2中,需要结构体类型bz_stream,这个结构体包含输入和输出缓冲区以及三个C函数:BZ2_bzCompressInit分配流的缓冲区;BZ2_bzCompress压缩输入缓冲区的数据并写到输出缓冲区;BZ2_bzCompressEnd释放缓冲区。

我们要做的就是从Go语言中直接调用C函数BZ2_bzCompressInit和BZ2_bzCompress,而BZ2_bzCompress使用C语言先包装一下,如下:

#include <bzlib.h>
 
int bz2compress(bz_stream *s, int action, char *int, 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文件里有关C的代码写在注释里,然后import “C”,这个导入会在Go编译器看到它之前促使go build利用cgo工作预处理文件。

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

预处理过程中,cgo产生一个临时包,这个包里面包含了所有C函数和类型对应的Go语言声明,例如C.bz_stream和c.BZ2_bzCompressInit。cgo工具通过特殊的方式调用C编译器import “C”声明之前的注释来发现这些类型。 还可以通过#cgo指令来指定C工具链中其它的选项。

package bzip
/*
#cgo CFLAGS: -I/usr/include
#go LDFLAGS: -L/usr/lib -lbz2
#include <bzlib.h>
int bz2compress(bz_stream *s, int action, char *int, unsigned *inlen, char *out, unsigned *outlen);
*/
import "C"
import (
	"io"
	"unsafe"
)
type writer struct {
	w io.Writer
	stream *C.bz_stream
	outbuf [64*1024]byte
}
// NewWriter 对于bzip2压缩的流返回一个writer
fun NewWriter(out io.Writer) io.WriterCloser {
	const (
		blockSize = 9
		verbosity = 0
		workFactor = 30
	)
	w := &writer{w:out, stream: C.bz2alloc()}
	C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
	return w
}

NewWriter嗲用C函数BZ2_bzCompressInit来初始化流的缓冲区。下面的writer掉哟个bz2compress函数机型数据压缩

func (w *writer) Write(data []byte) (int, error) {
	if w.stream == nil {
		panic("closed")
	}
	var total int
	for len(data) > 0 {
		inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf))
		C.zb2compress(w.stream, C.BZ_RUN, (*C.char)(unsafe.Pointer(&data[0])), &inlen, (*C.char)(unsafe.Pointer(&w.outbuf)), &outlen)
		total += int(inlen)
		data = data[inlen:]
		if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
			return total, err
		}
	}
	return total, nil
}

Close方法与Writer类似,将剩余数据清空并关闭数据流:

func (w *writer) Close() error {
	if w.stream == nil {
		panic("closed")
	}
	defer func() {
		C.B2_bzCompressEnd(w.stream)
		C.bz2free(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
		}
	}
}

一个调用C语言的Go程序就完成了:

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)
	}
}