• 背景

    最近正在基于机器学习搭建一个多媒体分析平台,一方面鉴于组内成员多则有近两年的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熟悉

    参考资料