gopl-zh.github.com/ch13/ch13-04.md
2015-12-26 20:05:30 +08:00

215 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 13.4. 通過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類型輸入的解壓縮接口。例如
```Go
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對象空間的申請和釋放操作。下面是新代碼中增加的註釋説明這個問題
```Go
// 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代碼對應一個獨立的文件。
```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;
s->next_in = s->next_out = NULL;
return r;
}
```
現在讓我們轉到Go語言部分第一部分如下所示。其中`import "C"`的語句是比較特别的。其實併沒有一個叫C的包但是這行語句會讓Go編譯程序在編譯之前先運行cgo工具。
```Go
// 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
}
```
在預處理過程中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庫是安裝在不同的位置你需要更新這些參數。
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類型併不相同卽使它們具有相同的大小也是不同的類型。
```Go
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
}
```
在循環的每次迭代中向bz2compress傳入數據的地址和剩餘部分的長度還有輸出緩存w.outbuf的地址和容量。這兩個長度信息通過它們的地址傳入而不是值傳入因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值。每個塊壓縮後的數據被寫入到底層的io.Writer。
Close方法和Write方法有着類似的結構通過一個循環將剩餘的壓縮數據刷新到輸出緩存。
```Go
// 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函數確保函數退出前調用C.BZ2_bzCompressEnd和C.bz2free釋放相關的C語言運行時資源。此刻w.stream指針將不再有效我們將它設置爲nil以保證安全然後在每個方法中增加了nil檢測以防止用戶在關閉後依然錯誤使用相關方法。
上面的實現中不僅僅寫是非併發安全的甚至併發調用Close和Write方法也可能導致程序的的崩潰。脩複這個問題是練習13.3的內容。
下面的bzipper程序使用我們自己包實現的bzip2壓縮命令。它的行爲和許多Unix繫統的bzip2命令類似。
```Go
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程序中動態加載也都是可行的譯註在Go1.5中Windows繫統的Go語言實現併不支持生成C語言動態庫或靜態庫的特性。不過好消息是目前已經有人在嚐試解決這個問題具體請訪問 [Issue11058](https://github.com/golang/go/issues/11058) 。這里我們隻展示的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命令其他操作繫統可能無法運行