转载

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 。但因为 goC 垃圾回收以及使用方式的不同,建议尽量避免使用 cgo

使用 cgo 的方法比较 怪异 ,在 go 的源代码中把 C 代码作为注释来写,并标明依赖的库文件和路径,最后使用 import "C" 即可。比如要使用 Cstdlib.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)
}

注意,一般使用 import 会把所有要使用的包放在一起,比如:

import (
    "fmt"
    "os"
)

但使用 cgo 是个例外,必须给 import "C" 单独一行,且必须放在注释的 C 代码后面一行。

下面就是 cgogo 对应类型的转换了。进行类型转换的目的很简单,就是为了在 C 中使用C的类型,在go中使用go的类型。

标准类型

go的标准类型转换为C的标准类型比较简单,直接使用 C.char , C.schar (signed char)C.uchar (unsigned char)C.shortC.ushort (unsigned short)C.intC.uint (unsigned int)C.longC.ulong (unsigned long)C.longlong (long long)C.ulonglong (unsigned long long)C.floatC.doubleC.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) ,返回的是 gostring 。还有一个类似的函数,通过设置长度,可以取一段子字符串, 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_xxC.enum_xx ,比如, C.enum_AVPictureType

这样使用起来确实有些别扭,但是封装C的代码时,但遵循一定的方法,也可以让封装库的内部和外部调用都 go-style 。其实方法很简单,想想如果用go写 structenum 时,是怎么写的?

对于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)
}

这样看起来是不是有了 go-style ?可以使用 NewPixelFormatDescriptorFromCFindPixelFormatDescriptorByPixelFormat 这两个方法创建go的结构体 PixelFormatDescriptor ,后面的调用方法就非常简单明了了。

注意,这里用到了 unsafe.Pointer 这个类型,你可以把它的作用简单的理解为C中的 void* ,从上面的例子也可以看出,主要用来做类型转换的。

在上面的例子中,还有这样一个类型 PixelFormat ,它的定义是

type PixelFormat C.enum_AVPixelFormat

这样,在后续的传参和调用时,使用 PixelFormat 会更加简单些。

同时,我们也可以看到,调用C的结构体中的元素时,也很简单:

CAVPixFmtDescriptor.nb_components

直接加点就可以访问其成员。

封装自定义函数

有了上面的知识,做一些简单的封装应该没有问题,要注意的地方就是类型转换,尤其是涉及到指针时,更要小心谨慎。如果觉得难以处理,就可以使用自定义函数的方法,把复杂的类型转换拆解为简单的函数调用,这时只要注意C代码的编写规范就可以了。

总结

以上是自己这段时间使用cgo和阅读源码的一些总结,网上有人会说使用cgo很难,其实只是cgo的用法与go有差异,一旦涉及到C,可能就会让人望而却步。其实不然,用好cgo有以下几个方面:

  • 注意类型转换
  • 注意C string的释放
  • 注意使用 unsafe.Pointer
  • 如果需要,添加自定义函数,避免过多或复杂的转换
  • 最后一条,也是最重要的,要对C API熟悉

参考资料

  • Command cgo
  • C? Go? Cgo!
  • cgo
  • go-libav源码
原文  http://www.hackcv.com/index.php/archives/105/
正文到此结束
Loading...