原文鏈接:https://gopl-zh.github.io/ch13/ch13-04.html
Go程序可能會(huì)遇到要訪問C語言的某些硬件驅(qū)動(dòng)函數(shù)的場(chǎng)景,或者是從一個(gè)C++語言實(shí)現(xiàn)的嵌入式數(shù)據(jù)庫查詢記錄的場(chǎng)景,或者是使用Fortran語言實(shí)現(xiàn)的一些線性代數(shù)庫的場(chǎng)景。C語言作為一個(gè)通用語言,很多庫會(huì)選擇提供一個(gè)C兼容的API,然后用其他不同的編程語言實(shí)現(xiàn)(譯者:Go語言需要也應(yīng)該擁抱這些巨大的代碼遺產(chǎn))。
在本節(jié)中,我們將構(gòu)建一個(gè)簡易的數(shù)據(jù)壓縮程序,使用了一個(gè)Go語言自帶的叫cgo的用于支援C語言函數(shù)調(diào)用的工具。這類工具一般被稱為 foreign-function interfaces (簡稱ffi),并且在類似工具中cgo也不是唯一的。SWIG(http://swig.org)是另一個(gè)類似的且被廣泛使用的工具,SWIG提供了很多復(fù)雜特性以支援C++的特性,但SWIG并不是我們要討論的主題。
在標(biāo)準(zhǔn)庫的compress/...
子包有很多流行的壓縮算法的編碼和解碼實(shí)現(xiàn),包括流行的LZW壓縮算法(Unix的compress命令用的算法)和DEFLATE壓縮算法(GNU gzip命令用的算法)。這些包的API的細(xì)節(jié)雖然有些差異,但是它們都提供了針對(duì) io.Writer類型輸出的壓縮接口和提供了針對(duì)io.Reader類型輸入的解壓縮接口。例如:
package gzip // compress/gzip
func NewWriter(w io.Writer) io.WriteCloser
func NewReader(r io.Reader) (io.ReadCloser, error)
bzip2壓縮算法,是基于優(yōu)雅的Burrows-Wheeler變換算法,運(yùn)行速度比gzip要慢,但是可以提供更高的壓縮比。標(biāo)準(zhǔn)庫的compress/bzip2包目前還沒有提供bzip2壓縮算法的實(shí)現(xiàn)。完全從頭開始實(shí)現(xiàn)一個(gè)壓縮算法是一件繁瑣的工作,而且 http://bzip.org 已經(jīng)有現(xiàn)成的libbzip2的開源實(shí)現(xiàn),不僅文檔齊全而且性能又好。
如果是比較小的C語言庫,我們完全可以用純Go語言重新實(shí)現(xiàn)一遍。如果我們對(duì)性能也沒有特殊要求的話,我們還可以用os/exec包的方法將C編寫的應(yīng)用程序作為一個(gè)子進(jìn)程運(yùn)行。只有當(dāng)你需要使用復(fù)雜而且性能更高的底層C接口時(shí),就是使用cgo的場(chǎng)景了(譯注:用os/exec包調(diào)用子進(jìn)程的方法會(huì)導(dǎo)致程序運(yùn)行時(shí)依賴那個(gè)應(yīng)用程序)。下面我們將通過一個(gè)例子講述cgo的具體用法。
譯注:本章采用的代碼都是最新的。因?yàn)橹耙呀?jīng)出版的書中包含的代碼只能在Go1.5之前使用。從Go1.6開始,Go語言已經(jīng)明確規(guī)定了哪些Go語言指針可以直接傳入C語言函數(shù)。新代碼重點(diǎn)是增加了bz2alloc和bz2free的兩個(gè)函數(shù),用于bz_stream對(duì)象空間的申請(qǐng)和釋放操作。下面是新代碼中增加的注釋,說明這個(gè)問題:
// 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,我們需要先構(gòu)建一個(gè)bz_stream結(jié)構(gòu)體,用于保持輸入和輸出緩存。然后有三個(gè)函數(shù):BZ2_bzCompressInit用于初始化緩存,BZ2_bzCompress用于將輸入緩存的數(shù)據(jù)壓縮到輸出緩存,BZ2_bzCompressEnd用于釋放不需要的緩存。(目前不要擔(dān)心包的具體結(jié)構(gòu),這個(gè)例子的目的就是演示各個(gè)部分如何組合在一起的。)
我們可以在Go代碼中直接調(diào)用BZ2_bzCompressInit和BZ2_bzCompressEnd,但是對(duì)于BZ2_bzCompress,我們將定義一個(gè)C語言的包裝函數(shù),用它完成真正的工作。下面是C代碼,對(duì)應(yīng)一個(gè)獨(dú)立的文件。
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;
s->next_in = s->next_out = NULL;
return r;
}
現(xiàn)在讓我們轉(zhuǎn)到Go語言部分,第一部分如下所示。其中import "C"
的語句是比較特別的。其實(shí)并沒有一個(gè)叫C的包,但是這行語句會(huì)讓Go編譯程序在編譯之前先運(yùn)行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>
#include <stdlib.h>
bz_stream* bz2alloc() { return calloc(1, sizeof(bz_stream)); }
int bz2compress(bz_stream *s, int action,
char *in, unsigned *inlen, char *out, unsigned *outlen);
void bz2free(bz_stream* s) { free(s); }
*/
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
const verbosity = 0
const workFactor = 30
w := &writer{w: out, stream: C.bz2alloc()}
C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
return w
}
在預(yù)處理過程中,cgo工具生成一個(gè)臨時(shí)包用于包含所有在Go語言中訪問的C語言的函數(shù)或類型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通過以某種特殊的方式調(diào)用本地的C編譯器來發(fā)現(xiàn)在Go源文件導(dǎo)入聲明前的注釋中包含的C頭文件中的內(nèi)容(譯注:import "C"
語句前緊挨著的注釋是對(duì)應(yīng)cgo的特殊語法,對(duì)應(yīng)必要的構(gòu)建參數(shù)選項(xiàng)和C語言代碼)。
在cgo注釋中還可以包含#cgo指令,用于給C語言工具鏈指定特殊的參數(shù)。例如CFLAGS和LDFLAGS分別對(duì)應(yīng)傳給C語言編譯器的編譯參數(shù)和鏈接器參數(shù),使它們可以從特定目錄找到bzlib.h頭文件和libbz2.a庫文件。這個(gè)例子假設(shè)你已經(jīng)在/usr目錄成功安裝了bzip2庫。如果bzip2庫是安裝在不同的位置,你需要更新這些參數(shù)(譯注:這里有一個(gè)從純C代碼生成的cgo綁定,不依賴bzip2靜態(tài)庫和操作系統(tǒng)的具體環(huán)境,具體請(qǐng)?jiān)L問 https://github.com/chai2010/bzip2 )。
NewWriter函數(shù)通過調(diào)用C語言的BZ2_bzCompressInit函數(shù)來初始化stream中的緩存。在writer結(jié)構(gòu)中還包括了另一個(gè)buffer,用于輸出緩存。
下面是Write方法的實(shí)現(xiàn),返回成功壓縮數(shù)據(jù)的大小,主體是一個(gè)循環(huán)中調(diào)用C語言的bz2compress函數(shù)實(shí)現(xiàn)的。從代碼可以看到,Go程序可以訪問C語言的bz_stream、char和uint類型,還可以訪問bz2compress等函數(shù),甚至可以訪問C語言中像BZ_RUN那樣的宏定義,全部都是以C.x語法訪問。其中C.uint類型和Go語言的uint類型并不相同,即使它們具有相同的大小也是不同的類型。
func (w *writer) Write(data []byte) (int, error) {
if w.stream == nil {
panic("closed")
}
var total int // uncompressed bytes written
for len(data) > 0 {
inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf))
C.bz2compress(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
}
在循環(huán)的每次迭代中,向bz2compress傳入數(shù)據(jù)的地址和剩余部分的長度,還有輸出緩存w.outbuf的地址和容量。這兩個(gè)長度信息通過它們的地址傳入而不是值傳入,因?yàn)閎z2compress函數(shù)可能會(huì)根據(jù)已經(jīng)壓縮的數(shù)據(jù)和壓縮后數(shù)據(jù)的大小來更新這兩個(gè)值。每個(gè)塊壓縮后的數(shù)據(jù)被寫入到底層的io.Writer。
Close方法和Write方法有著類似的結(jié)構(gòu),通過一個(gè)循環(huán)將剩余的壓縮數(shù)據(jù)刷新到輸出緩存。
// 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)
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
}
}
}
壓縮完成后,Close方法用了defer函數(shù)確保函數(shù)退出前調(diào)用C.BZ2_bzCompressEnd和C.bz2free釋放相關(guān)的C語言運(yùn)行時(shí)資源。此刻w.stream指針將不再有效,我們將它設(shè)置為nil以保證安全,然后在每個(gè)方法中增加了nil檢測(cè),以防止用戶在關(guān)閉后依然錯(cuò)誤使用相關(guān)方法。
上面的實(shí)現(xiàn)中,不僅僅寫是非并發(fā)安全的,甚至并發(fā)調(diào)用Close和Write方法也可能導(dǎo)致程序的的崩潰。修復(fù)這個(gè)問題是練習(xí)13.3的內(nèi)容。
下面的bzipper程序,使用我們自己包實(shí)現(xiàn)的bzip2壓縮命令。它的行為和許多Unix系統(tǒng)的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)
}
}
在上面的場(chǎng)景中,我們使用bzipper壓縮了/usr/share/dict/words系統(tǒng)自帶的詞典,從938,848字節(jié)壓縮到335,405字節(jié)。大約是原始數(shù)據(jù)大小的三分之一。然后使用系統(tǒng)自帶的bunzip2命令進(jìn)行解壓。壓縮前后文件的SHA256哈希碼是相同了,這也說明了我們的壓縮工具是正確的。(如果你的系統(tǒng)沒有sha256sum命令,那么請(qǐng)先按照練習(xí)4.2實(shí)現(xiàn)一個(gè)類似的工具)
$ 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 -
我們演示了如何將一個(gè)C語言庫鏈接到Go語言程序。相反,將Go編譯為靜態(tài)庫然后鏈接到C程序,或者將Go程序編譯為動(dòng)態(tài)庫然后在C程序中動(dòng)態(tài)加載也都是可行的(譯注:在Go1.5中,Windows系統(tǒng)的Go語言實(shí)現(xiàn)并不支持生成C語言動(dòng)態(tài)庫或靜態(tài)庫的特性。不過好消息是,目前已經(jīng)有人在嘗試解決這個(gè)問題,具體請(qǐng)?jiān)L問 Issue11058 )。這里我們只展示的cgo很小的一些方面,更多的關(guān)于內(nèi)存管理、指針、回調(diào)函數(shù)、中斷信號(hào)處理、字符串、errno處理、終結(jié)器,以及goroutines和系統(tǒng)線程的關(guān)系等,有很多細(xì)節(jié)可以討論。特別是如何將Go語言的指針傳入C函數(shù)的規(guī)則也是異常復(fù)雜的(譯注:簡單來說,要傳入C函數(shù)的Go指針指向的數(shù)據(jù)本身不能包含指針或其他引用類型;并且C函數(shù)在返回后不能繼續(xù)持有Go指針;并且在C函數(shù)返回之前,Go指針是被鎖定的,不能導(dǎo)致對(duì)應(yīng)指針數(shù)據(jù)被移動(dòng)或棧的調(diào)整),部分的原因在13.2節(jié)有討論到,但是在Go1.5中還沒有被明確(譯注:Go1.6將會(huì)明確cgo中的指針使用規(guī)則)。如果要進(jìn)一步閱讀,可以從
https://golang.org/cmd/cgo 開始。
練習(xí) 13.3: 使用sync.Mutex以保證bzip2.writer在多個(gè)goroutines中被并發(fā)調(diào)用是安全的。
練習(xí) 13.4: 因?yàn)镃庫依賴的限制。 使用os/exec包啟動(dòng)/bin/bzip2命令作為一個(gè)子進(jìn)程,提供一個(gè)純Go的bzip.NewWriter的替代實(shí)現(xiàn)(譯注:雖然是純Go實(shí)現(xiàn),但是運(yùn)行時(shí)將依賴/bin/bzip2命令,其他操作系統(tǒng)可能無法運(yùn)行)。
![]() | ![]() |
更多建議: