Go 語言 通過cgo調(diào)用C代碼

2023-03-14 17:00 更新

原文鏈接:https://gopl-zh.github.io/ch13/ch13-04.html


13.4. 通過cgo調(diào)用C代碼

Go程序可能會遇到要訪問C語言的某些硬件驅(qū)動函數(shù)的場景,或者是從一個C++語言實現(xiàn)的嵌入式數(shù)據(jù)庫查詢記錄的場景,或者是使用Fortran語言實現(xiàn)的一些線性代數(shù)庫的場景。C語言作為一個通用語言,很多庫會選擇提供一個C兼容的API,然后用其他不同的編程語言實現(xiàn)(譯者:Go語言需要也應該擁抱這些巨大的代碼遺產(chǎn))。

在本節(jié)中,我們將構(gòu)建一個簡易的數(shù)據(jù)壓縮程序,使用了一個Go語言自帶的叫cgo的用于支援C語言函數(shù)調(diào)用的工具。這類工具一般被稱為 foreign-function interfaces (簡稱ffi),并且在類似工具中cgo也不是唯一的。SWIG(http://swig.org)是另一個類似的且被廣泛使用的工具,SWIG提供了很多復雜特性以支援C++的特性,但SWIG并不是我們要討論的主題。

在標準庫的compress/...子包有很多流行的壓縮算法的編碼和解碼實現(xiàn),包括流行的LZW壓縮算法(Unix的compress命令用的算法)和DEFLATE壓縮算法(GNU gzip命令用的算法)。這些包的API的細節(jié)雖然有些差異,但是它們都提供了針對 io.Writer類型輸出的壓縮接口和提供了針對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變換算法,運行速度比gzip要慢,但是可以提供更高的壓縮比。標準庫的compress/bzip2包目前還沒有提供bzip2壓縮算法的實現(xiàn)。完全從頭開始實現(xiàn)一個壓縮算法是一件繁瑣的工作,而且 http://bzip.org 已經(jīng)有現(xiàn)成的libbzip2的開源實現(xiàn),不僅文檔齊全而且性能又好。

如果是比較小的C語言庫,我們完全可以用純Go語言重新實現(xiàn)一遍。如果我們對性能也沒有特殊要求的話,我們還可以用os/exec包的方法將C編寫的應用程序作為一個子進程運行。只有當你需要使用復雜而且性能更高的底層C接口時,就是使用cgo的場景了(譯注:用os/exec包調(diào)用子進程的方法會導致程序運行時依賴那個應用程序)。下面我們將通過一個例子講述cgo的具體用法。

譯注:本章采用的代碼都是最新的。因為之前已經(jīng)出版的書中包含的代碼只能在Go1.5之前使用。從Go1.6開始,Go語言已經(jīng)明確規(guī)定了哪些Go語言指針可以直接傳入C語言函數(shù)。新代碼重點是增加了bz2alloc和bz2free的兩個函數(shù),用于bz_stream對象空間的申請和釋放操作。下面是新代碼中增加的注釋,說明這個問題:

// 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)建一個bz_stream結(jié)構(gòu)體,用于保持輸入和輸出緩存。然后有三個函數(shù):BZ2_bzCompressInit用于初始化緩存,BZ2_bzCompress用于將輸入緩存的數(shù)據(jù)壓縮到輸出緩存,BZ2_bzCompressEnd用于釋放不需要的緩存。(目前不要擔心包的具體結(jié)構(gòu),這個例子的目的就是演示各個部分如何組合在一起的。)

我們可以在Go代碼中直接調(diào)用BZ2_bzCompressInit和BZ2_bzCompressEnd,但是對于BZ2_bzCompress,我們將定義一個C語言的包裝函數(shù),用它完成真正的工作。下面是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;
}

現(xiàn)在讓我們轉(zhuǎn)到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>
#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語言的函數(shù)或類型。例如C.bz_stream和C.BZ2_bzCompressInit。cgo工具通過以某種特殊的方式調(diào)用本地的C編譯器來發(fā)現(xiàn)在Go源文件導入聲明前的注釋中包含的C頭文件中的內(nèi)容(譯注:import "C"語句前緊挨著的注釋是對應cgo的特殊語法,對應必要的構(gòu)建參數(shù)選項和C語言代碼)。

在cgo注釋中還可以包含#cgo指令,用于給C語言工具鏈指定特殊的參數(shù)。例如CFLAGS和LDFLAGS分別對應傳給C語言編譯器的編譯參數(shù)和鏈接器參數(shù),使它們可以從特定目錄找到bzlib.h頭文件和libbz2.a庫文件。這個例子假設(shè)你已經(jīng)在/usr目錄成功安裝了bzip2庫。如果bzip2庫是安裝在不同的位置,你需要更新這些參數(shù)(譯注:這里有一個從純C代碼生成的cgo綁定,不依賴bzip2靜態(tài)庫和操作系統(tǒng)的具體環(huán)境,具體請訪問 https://github.com/chai2010/bzip2 )。

NewWriter函數(shù)通過調(diào)用C語言的BZ2_bzCompressInit函數(shù)來初始化stream中的緩存。在writer結(jié)構(gòu)中還包括了另一個buffer,用于輸出緩存。

下面是Write方法的實現(xiàn),返回成功壓縮數(shù)據(jù)的大小,主體是一個循環(huán)中調(diào)用C語言的bz2compress函數(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的地址和容量。這兩個長度信息通過它們的地址傳入而不是值傳入,因為bz2compress函數(shù)可能會根據(jù)已經(jīng)壓縮的數(shù)據(jù)和壓縮后數(shù)據(jù)的大小來更新這兩個值。每個塊壓縮后的數(shù)據(jù)被寫入到底層的io.Writer。

Close方法和Write方法有著類似的結(jié)構(gòu),通過一個循環(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語言運行時資源。此刻w.stream指針將不再有效,我們將它設(shè)置為nil以保證安全,然后在每個方法中增加了nil檢測,以防止用戶在關(guān)閉后依然錯誤使用相關(guān)方法。

上面的實現(xiàn)中,不僅僅寫是非并發(fā)安全的,甚至并發(fā)調(diào)用Close和Write方法也可能導致程序的的崩潰。修復這個問題是練習13.3的內(nèi)容。

下面的bzipper程序,使用我們自己包實現(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)
    }
}

在上面的場景中,我們使用bzipper壓縮了/usr/share/dict/words系統(tǒng)自帶的詞典,從938,848字節(jié)壓縮到335,405字節(jié)。大約是原始數(shù)據(jù)大小的三分之一。然后使用系統(tǒng)自帶的bunzip2命令進行解壓。壓縮前后文件的SHA256哈希碼是相同了,這也說明了我們的壓縮工具是正確的。(如果你的系統(tǒng)沒有sha256sum命令,那么請先按照練習4.2實現(xiàn)一個類似的工具)

$ 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編譯為靜態(tài)庫然后鏈接到C程序,或者將Go程序編譯為動態(tài)庫然后在C程序中動態(tài)加載也都是可行的(譯注:在Go1.5中,Windows系統(tǒng)的Go語言實現(xiàn)并不支持生成C語言動態(tài)庫或靜態(tài)庫的特性。不過好消息是,目前已經(jīng)有人在嘗試解決這個問題,具體請訪問 Issue11058 )。這里我們只展示的cgo很小的一些方面,更多的關(guān)于內(nèi)存管理、指針、回調(diào)函數(shù)、中斷信號處理、字符串、errno處理、終結(jié)器,以及goroutines和系統(tǒng)線程的關(guān)系等,有很多細節(jié)可以討論。特別是如何將Go語言的指針傳入C函數(shù)的規(guī)則也是異常復雜的(譯注:簡單來說,要傳入C函數(shù)的Go指針指向的數(shù)據(jù)本身不能包含指針或其他引用類型;并且C函數(shù)在返回后不能繼續(xù)持有Go指針;并且在C函數(shù)返回之前,Go指針是被鎖定的,不能導致對應指針數(shù)據(jù)被移動或棧的調(diào)整),部分的原因在13.2節(jié)有討論到,但是在Go1.5中還沒有被明確(譯注:Go1.6將會明確cgo中的指針使用規(guī)則)。如果要進一步閱讀,可以從 https://golang.org/cmd/cgo 開始。

練習 13.3: 使用sync.Mutex以保證bzip2.writer在多個goroutines中被并發(fā)調(diào)用是安全的。

練習 13.4: 因為C庫依賴的限制。 使用os/exec包啟動/bin/bzip2命令作為一個子進程,提供一個純Go的bzip.NewWriter的替代實現(xiàn)(譯注:雖然是純Go實現(xiàn),但是運行時將依賴/bin/bzip2命令,其他操作系統(tǒng)可能無法運行)。



以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號