cgo 的使用總結
背景
最近正在基于機器學習搭建一個多媒體分析平臺,一方面鑒于組內成員多則有近兩年的Go使用經驗,少則也有半年的Go使用經驗,
另一方面由于Go的格式統一、工程系統能力強大,所以選擇Go為主要的開發語言。而對于多媒體分析,第一步就是圖片視頻的編解碼,圖片好說,而視頻就比較難了。普通的編解碼可以使用 exec 調用 ffmpeg ,但要獲取視頻每幀的數據內容,就需要使用 ffmpeg 的API了。通過 github ,我們找到了 go-libav 這個庫,相比其他的 go binding of ffmpeg libraries ,這個庫有以下幾個優點:
- 支持 ffmpeg 3 ,也支持 ffmpeg 2 ,但已廢棄
- 更加面向對象的編程方法
- Go-Style,不是對 ffmpeg API 的簡單封裝,而是以更加go的形式進行封裝
- 更簡單的垃圾回收
其中第二點和第三點是我最欣賞這個庫的主要原因,相比與其他 ffmpeg 庫的直接封裝, go-libav 庫加入了更多的語言易用性的思考。但是,目前這個庫還在持續的開發中,還存在下面幾個問題:
- 支持的庫有限,目前只有 avcodec avfilter avformat avutil 這四個庫的一些基礎API
- 缺少樣例,若沒有使用 ffmpeg API 的經驗,上手較難
- 單元測試覆蓋率只有32%,有可能測試不充分
我們近期已經為 avutil 擴展了一些功能,正在添加examples和單元測試,后續會提 Merge Request 反饋到這個庫。在使用這個庫的過程中,我們踩了一些 cgo 的坑,在這里總結一下 cgo 的使用方法和注意問題。
cgo 的基礎知識
cgo 可以在 go 中調用 C ,也可以在 C 中調用 go 。但因為 go 和 C 垃圾回收以及使用方式的不同,建議盡量避免使用 cgo 。
使用 cgo 的方法比較 怪異 ,在 go 的源代碼中把 C 代碼作為注釋來寫,并標明依賴的庫文件和路徑,最后使用 import "C" 即可。比如要使用 C 中 stdlib.h 中的 random 函數,可以這么寫:
package main
/*
include <stdlib.h>
*/
import "C"
import "fmt"
func main() {
rand := int(C.random())
fmt.Println("get random value from C", rand)
}</code></pre>
注意,一般使用 import 會把所有要使用的包放在一起,比如:
import (
"fmt"
"os"
)
但使用 cgo 是個例外,必須給 import "C" 單獨一行,且必須放在注釋的 C 代碼后面一行。
下面就是 cgo 和 go 對應類型的轉換了。進行類型轉換的目的很簡單,就是為了在 C 中使用C的類型,在go中使用go的類型。
標準類型
go的標準類型轉換為C的標準類型比較簡單,直接使用 C.char , C.schar (signed char) , C.uchar (unsigned char) , C.short , C.ushort (unsigned short) , C.int , C.uint (unsigned int) , C.long , C.ulong (unsigned long) , C.longlong (long long) , C.ulonglong (unsigned long long) , C.float , C.double , C.complexfloat (complex float) 以及 C.complexdouble (complex double) ,這些類型已經可以滿足基本的數值運算了。
例子:要調用一個參數類型為int的C函數,這個函數返回一個int值,在go中需要將返回值做類型轉換才可以使用:
var goInt int
ret := int(C.cfunc(C.int(goInt)))
...
字符串
- go 字符串轉換為 C 字符串: C.CString(gostr string) ,返回的是 C 中的 *char ,這里返回的 *char 不會被 go 的垃圾回收清理,所以需要自行釋放調,可以這么使用 defer C.free(unsafe.Pointer(cstr)) 。
- C 字符串轉換為 go 字符串: C.GoString(cstr string) ,返回的是 go 的 string 。還有一個類似的函數,通過設置長度,可以取一段子字符串, C.GoString(cstr *C.char, length C.int) 。
struct/union/enum
- struct:因為C的結構體和go的結構體字節數和數據分配上不同,所以無法直接轉換,所以在go中都是使用 C.struct_xxx 。比如, C_struct_AVOption
- union和enum:和 struct 類似,可以使用 C.union_xx 和 C.enum_xx ,比如, C.enum_AVPictureType 。
這樣使用起來確實有些別扭,但是封裝C的代碼時,但遵循一定的方法,也可以讓封裝庫的內部和外部調用都 go-style 。其實方法很簡單,想想如果用go寫 struct 和 enum 時,是怎么寫的?
對于C的 struct ,我們可以新建一個go的 struct ,把 C.struct_xx 作為其中的一個元素,比如:
type PixelFormatDescriptor struct {
CAVPixFmtDescriptor *C.AVPixFmtDescriptor
}
func NewPixelFormatDescriptorFromC(cCtx unsafe.Pointer) PixelFormatDescriptor {
return &PixelFormatDescriptor{CAVPixFmtDescriptor: (C.AVPixFmtDescriptor)(cCtx)}
}
func FindPixelFormatDescriptorByPixelFormat(pixelFormat PixelFormat) *PixelFormatDescriptor {
cDescriptor := C.av_pix_fmt_desc_get(C.enum_AVPixelFormat(pixelFormat))
if cDescriptor == nil {
return nil
}
return NewPixelFormatDescriptorFromC(unsafe.Pointer(cDescriptor))
}
func (d *PixelFormatDescriptor) Name() string {
return C.GoString(d.CAVPixFmtDescriptor.name)
}
func (d PixelFormatDescriptor) ComponentCount() int {
return int(d.CAVPixFmtDescriptor.nb_components)
}</code></pre>
這樣看起來是不是有了 go-style ?可以使用 NewPixelFormatDescriptorFromC 和 FindPixelFormatDescriptorByPixelFormat 這兩個方法創建go的結構體 PixelFormatDescriptor ,后面的調用方法就非常簡單明了了。
注意,這里用到了 unsafe.Pointer 這個類型,你可以把它的作用簡單的理解為C中的 void
,從上面的例子也可以看出,主要用來做類型轉換的。</p>
在上面的例子中,還有這樣一個類型 PixelFormat ,它的定義是
type PixelFormat C.enum_AVPixelFormat
這樣,在后續的傳參和調用時,使用 PixelFormat 會更加簡單些。
同時,我們也可以看到,調用C的結構體中的元素時,也很簡單:
CAVPixFmtDescriptor.nb_components
直接加點就可以訪問其成員。
封裝自定義函數
有了上面的知識,做一些簡單的封裝應該沒有問題,要注意的地方就是類型轉換,尤其是涉及到指針時,更要小心謹慎。如果覺得難以處理,就可以使用自定義函數的方法,把復雜的類型轉換拆解為簡單的函數調用,這時只要注意C代碼的編寫規范就可以了。
總結
以上是自己這段時間使用cgo和閱讀源碼的一些總結,網上有人會說使用cgo很難,其實只是cgo的用法與go有差異,一旦涉及到C,可能就會讓人望而卻步。其實不然,用好cgo有以下幾個方面:
- 注意類型轉換
- 注意C string的釋放
- 注意使用 unsafe.Pointer
- 如果需要,添加自定義函數,避免過多或復雜的轉換
- 最后一條,也是最重要的,要對C API熟悉
參考資料
來自:http://www.hackcv.com/index.php/archives/105/