Hacks on Computer Vision

cgo 的使用总结

2016.12.19

背景

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

参考资料

__EOF__

本文作者HackCV
版权声明本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
本文链接https://hackcv.com/posts/cgo-%E7%9A%84%E4%BD%BF%E7%94%A8%E6%80%BB%E7%BB%93/

发表评论