ch13 review

This commit is contained in:
chai2010
2015-12-24 14:37:53 +08:00
parent 983524dbc8
commit 427ac25df9
7 changed files with 138 additions and 92 deletions

View File

@@ -1,10 +1,10 @@
## 13.4. 通過cgo調用C代碼
Go程序可能會遇到要訪問C語言的某些硬件驅動的場景, 或者是從一個C++實現的嵌入式數據庫査詢記録的場景, 或者是使用Fortran實現的一些線性代數庫的場景. C作爲一個通用語言, 很多庫會選擇提供一個C兼容的API, 然後用其他語言實現.
Go程序可能會遇到要訪問C語言的某些硬件驅動函數的場景或者是從一個C++語言實現的嵌入式數據庫査詢記録的場景或者是使用Fortran語言實現的一些線性代數庫的場景。C語言作爲一個通用語言很多庫會選擇提供一個C兼容的API然後用其他不同的編程語言實現譯者Go語言需要也應該擁抱這些鉅大的代碼遺産
在本節中, 我們將構建一個簡易的數據壓縮程序, 通過使用一個Go語言自帶的叫cgo的用於支援C語言函數調用的工具. 這類工具被稱爲外圍函數接口(ffi), 併且cgo也不是Go中唯一的類似工具. SWIG(swig.org) 是類似的另一個被廣泛使用的工具, 它提供了很多複雜特性以支援C++的集成, 但 SWIG 不是這里要討論的主題.
在本節中我們將構建一個簡易的數據壓縮程序使用一個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` 的解壓縮接口. 例如:
在標準庫的`compress/...`子包有很多流行的壓縮算法的編碼和解碼實現,包括流行的LZW壓縮算法Unix的compress命令用的算法和DEFLATE壓縮算法GNU gzip命令用的算法)。這些包的API的細節雖然有些差異但是它們都提供了針對 io.Writer類型輸齣的壓縮接口和提供了針對io.Reader類型輸入的解壓縮接口例如
```Go
package gzip // compress/gzip
@@ -12,15 +12,34 @@ 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 開源實現, 文檔齊全而且性能較好,
bzip2壓縮算法是基於優雅的Burrows-Wheeler變換算法,運行速度比gzip要慢但是可以提供更高的壓縮比標準庫的compress/bzip2包目前還沒有提供bzip2壓縮算法的實現。完全從頭開始實現是一個壓縮算法是一件繁瑣的工作而且 http://bzip.org 已經有現成的libbzip2開源實現,不僅文檔齊全而且性能又好。
如果C庫比較小, 我們可以用純Go重新實現一遍. 如果我們對性能沒有特殊要求, 我們可以用 `os/exec` 包的方法將C編寫的應用程序作爲一個子進運行. 隻有當你需要使用複雜但是性能更高的底層C接口時, 就是使用cgo的場景了. 下面我們將通過一個例子講述cgo的用法.
如果比較小的C語言庫我們完全可以用純Go語言重新實現一遍如果我們對性能沒有特殊要求的話,我們可以用os/exec包的方法將C編寫的應用程序作爲一個子進運行隻有當你需要使用複雜而且性能更高的底層C接口時就是使用cgo的場景了譯註用os/exec包調用子進程的方法會導致程序運行時依賴那個應用程序下面我們將通過一個例子講述cgo的具體用法
要使用 libbzip2, 我們需要一個 `bz_stream` 結構體, 用於保持輸入和輸齣緩存.
然後有三個函數: BZ2_bzCompressInit 用於初始化緩存, BZ2_bzCompress 用於將輸入緩存的數據壓縮到輸齣緩存, BZ2_bzCompressEnd 用於釋放不需要的緩存.
(目前不要擔心包的具體結構, 這個例子的目的就是演示各個部分如何組合在一起的)
譯註本章采用的代碼都是最新的。因爲之前已經齣版的書中包含的代碼隻能在Go1.5之前使用。從Go1.6開始Go語言已經明確規定了哪些Go語言指針可以之間傳入C語言函數。新代碼重點是增加了bz2alloc和bz2free的兩個函數用於bz_stream對象空間的申請和釋放操作。下面是新代碼中增加的註釋説明這個問題
我們可以在Go代碼中直接調用 BZ2_bzCompressInit 和 BZ2_bzCompressEnd, 但是對於 BZ2_bzCompress, 我們將定義一個C語言的包裝函數, 爲了顯示他是如何完成的. 下面是C代碼, 對應一個獨立的文件.
```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
@@ -38,12 +57,12 @@ int bz2compress(bz_stream *s, int action,
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語言部分,第一部分如下所示其中`import "C"`的語句是比較特别的其實併沒有一個叫C的包但是這行語句會讓Go編譯程序在編譯之前先運行cgo工具
```Go
// Package bzip provides a writer that uses bzip2 compression (bzip.org).
@@ -53,8 +72,11 @@ 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"
@@ -71,20 +93,48 @@ type writer struct {
// 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)}
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
}
```
循環的每次迭代中, 向bz2compress傳入數據的地址和剩餘部分的長度, 還有輸齣緩存 w.outbuf 的地址和容量. 這兩個長度信息通過它們的地址傳入而不是值傳入, 因爲bz2compress函數可能會根據已經壓縮的數據和壓縮後數據的大小來更新這兩個值(譯註: 這里的用法有問題, 勘誤已經提到. 具體脩複的方法稍後再補充). 每個塊壓縮後的數據被寫入到底層的 io.Writer.
預處理過程中cgo工具爲生成一個臨時包用於包含所有在Go語言中訪問的C語言的函數或類型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通過以某種特殊的方式調用本地的C編譯器來發現在Go源文件導入聲明前的註釋中包含的C頭文件中的內容譯註`import "C"語句前僅捱着的註釋是對應cgo的特殊語法對應必要的構建參數選項和C語言代碼`)。
Close 方法和 Write 方法有着類似的結構, 通過一個循環將剩餘的壓縮數據刷新到輸齣緩存.
在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.
@@ -95,6 +145,7 @@ func (w *writer) Close() error {
}
defer func() {
C.BZ2_bzCompressEnd(w.stream)
C.bz2free(w.stream)
w.stream = nil
}()
for {
@@ -111,11 +162,11 @@ func (w *writer) Close() error {
}
```
壓縮完成後, Close 用了 defer 確保函數退齣前調用 C.BZ2_bzCompressEnd 釋放輸入和輸齣流的緩存. 此刻 `w.stream` 指針將不在有效, 我們將它設置爲 nil 以保證安全, 然後在每個方法中增加 nil 檢測, 以防止用戶在關閉後依然錯誤使用相關方法.
壓縮完成後Close方法用了defer函數確保函數退齣前調用C.BZ2_bzCompressEnd和C.bz2free釋放相關的C資源。此刻w.stream指針將不在有效我們將它設置爲nil以保證安全然後在每個方法中增加nil檢測以防止用戶在關閉後依然錯誤使用相關方法
不僅僅寫是非併發安全的, 甚至併發調用 CloseWrite 也可能導致C代碼的崩潰. 脩複這個問題是 練習13.3 的內容.
上面的實現中,不僅僅寫是非併發安全的甚至併發調用CloseWrite方法也可能導致程序的的崩潰脩複這個問題是練習13.3的內容
下面的bzipper程序使用我們自己包實現的bzip2壓縮命令. 它的行爲和許多Unix繫統的 bzip2 命令類似.
下面的bzipper程序使用我們自己包實現的bzip2壓縮命令它的行爲和許多Unix繫統的bzip2命令類似
```Go
gopl.io/ch13/bzipper
@@ -141,7 +192,7 @@ func main() {
}
```
在上面的場景中, 我們使用 bzipper 壓縮了 /usr/share/dict/words 繫統自帶的詞典, 從 938,848 字節壓縮到 335,405 字節, 大於是原始大小的三分之一. 然後使用繫統自帶的bunzip2命令進行解壓. 壓縮前後文件的SHA256哈希碼是相同了, 這也説明了我們的壓縮工具是可用的. (如果你的繫統沒有sha256sum命令, 那麽請先按照 練習4.2 實現一個類似的工具)
在上面的場景中我們使用bzipper壓縮了/usr/share/dict/words繫統自帶的詞典,從938,848字節壓縮到335,405字節。大約是原始數據大小的三分之一然後使用繫統自帶的bunzip2命令進行解壓壓縮前後文件的SHA256哈希碼是相同了這也説明了我們的壓縮工具是正確的。(如果你的繫統沒有sha256sum命令那麽請先按照練習4.2實現一個類似的工具
```
$ go build gopl.io/ch13/bzipper
@@ -155,8 +206,9 @@ $ ./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 開始.
我們演示了如何將一個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.3** 使用sync.Mutex以保證bzip2.writer在多個goroutines中被併發調用是安全的
**練習 13.4** 因爲C庫依賴的限製。 使用os/exec包啟動/bin/bzip2命令作爲一個子進程提供一個純Go的bzip.NewWriter的替代實現譯註雖然是純Go實現但是運行時將依賴/bin/bzip2目録其他操作繫統可能無法運行
**練習13.4:** 因爲C庫依賴的限製. 使用 `os/exec` 包啟動 `/bin/bzip2` 命令作爲一個子進程, 提供一個純Go的 bzip.NewWriter 的替代實現.