cgo 的使用总结

SamEichel 7年前
   <h3>背景</h3>    <p>最近正在基于机器学习搭建一个多媒体分析平台,一方面鉴于组内成员多则有近两年的Go使用经验,少则也有半年的Go使用经验,</p>    <p>另一方面由于Go的格式统一、工程系统能力强大,所以选择Go为主要的开发语言。而对于多媒体分析,第一步就是图片视频的编解码,图片好说,而视频就比较难了。普通的编解码可以使用 exec 调用 ffmpeg ,但要获取视频每帧的数据内容,就需要使用 ffmpeg 的API了。通过 github ,我们找到了 go-libav 这个库,相比其他的 go binding of ffmpeg libraries ,这个库有以下几个优点:</p>    <ul>     <li>支持 ffmpeg 3 ,也支持 ffmpeg 2 ,但已废弃</li>     <li>更加面向对象的编程方法</li>     <li>Go-Style,不是对 ffmpeg API 的简单封装,而是以更加go的形式进行封装</li>     <li>更简单的垃圾回收</li>    </ul>    <p>其中第二点和第三点是我最欣赏这个库的主要原因,相比与其他 ffmpeg 库的直接封装, go-libav 库加入了更多的语言易用性的思考。但是,目前这个库还在持续的开发中,还存在下面几个问题:</p>    <ul>     <li>支持的库有限,目前只有 avcodec avfilter avformat avutil 这四个库的一些基础API</li>     <li>缺少样例,若没有使用 ffmpeg API 的经验,上手较难</li>     <li>单元测试覆盖率只有32%,有可能测试不充分</li>    </ul>    <p>我们近期已经为 avutil 扩展了一些功能,正在添加examples和单元测试,后续会提 Merge Request 反馈到这个库。在使用这个库的过程中,我们踩了一些 cgo 的坑,在这里总结一下 cgo 的使用方法和注意问题。</p>    <h3>cgo 的基础知识</h3>    <p>cgo 可以在 go 中调用 C ,也可以在 C 中调用 go 。但因为 go 和 C 垃圾回收以及使用方式的不同,建议尽量避免使用 cgo 。</p>    <p>使用 cgo 的方法比较 <strong>怪异</strong> ,在 go 的源代码中把 C 代码作为注释来写,并标明依赖的库文件和路径,最后使用 import "C" 即可。比如要使用 C 中 stdlib.h 中的 random 函数,可以这么写:</p>    <pre>  <code class="language-go">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>    <p>注意,一般使用 import 会把所有要使用的包放在一起,比如:</p>    <pre>  <code class="language-go">import (      "fmt"      "os"  )</code></pre>    <p>但使用 cgo 是个例外,必须给 import "C" 单独一行,且必须放在注释的 C 代码后面一行。</p>    <p>下面就是 cgo 和 go 对应类型的转换了。进行类型转换的目的很简单,就是为了在 C 中使用C的类型,在go中使用go的类型。</p>    <p>标准类型</p>    <p>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) ,这些类型已经可以满足基本的数值运算了。</p>    <p>例子:要调用一个参数类型为int的C函数,这个函数返回一个int值,在go中需要将返回值做类型转换才可以使用:</p>    <pre>  <code class="language-go">var goInt int  ret := int(C.cfunc(C.int(goInt)))  ...</code></pre>    <p>字符串</p>    <ul>     <li>go 字符串转换为 C 字符串: C.CString(gostr string) ,返回的是 C 中的 *char ,这里返回的 *char 不会被 go 的垃圾回收清理,所以需要自行释放调,可以这么使用 defer C.free(unsafe.Pointer(cstr)) 。</li>     <li>C 字符串转换为 go 字符串: C.GoString(cstr string) ,返回的是 go 的 string 。还有一个类似的函数,通过设置长度,可以取一段子字符串, C.GoString(cstr *C.char, length C.int) 。</li>    </ul>    <p>struct/union/enum</p>    <ul>     <li>struct:因为C的结构体和go的结构体字节数和数据分配上不同,所以无法直接转换,所以在go中都是使用 C.struct_xxx 。比如, C_struct_AVOption</li>     <li>union和enum:和 struct 类似,可以使用 C.union_xx 和 C.enum_xx ,比如, C.enum_AVPictureType 。</li>    </ul>    <p>这样使用起来确实有些别扭,但是封装C的代码时,但遵循一定的方法,也可以让封装库的内部和外部调用都 go-style 。其实方法很简单,想想如果用go写 struct 和 enum 时,是怎么写的?</p>    <p>对于C的 struct ,我们可以新建一个go的 struct ,把 C.struct_xx 作为其中的一个元素,比如:</p>    <pre>  <code class="language-go">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>    <p>这样看起来是不是有了 go-style ?可以使用 NewPixelFormatDescriptorFromC 和 FindPixelFormatDescriptorByPixelFormat 这两个方法创建go的结构体 PixelFormatDescriptor ,后面的调用方法就非常简单明了了。</p>    <p>注意,这里用到了 unsafe.Pointer 这个类型,你可以把它的作用简单的理解为C中的 void* ,从上面的例子也可以看出,主要用来做类型转换的。</p>    <p>在上面的例子中,还有这样一个类型 PixelFormat ,它的定义是</p>    <pre>  <code class="language-go">type PixelFormat C.enum_AVPixelFormat</code></pre>    <p>这样,在后续的传参和调用时,使用 PixelFormat 会更加简单些。</p>    <p>同时,我们也可以看到,调用C的结构体中的元素时,也很简单:</p>    <pre>  <code class="language-go">CAVPixFmtDescriptor.nb_components</code></pre>    <p>直接加点就可以访问其成员。</p>    <p>封装自定义函数</p>    <p>有了上面的知识,做一些简单的封装应该没有问题,要注意的地方就是类型转换,尤其是涉及到指针时,更要小心谨慎。如果觉得难以处理,就可以使用自定义函数的方法,把复杂的类型转换拆解为简单的函数调用,这时只要注意C代码的编写规范就可以了。</p>    <h3>总结</h3>    <p>以上是自己这段时间使用cgo和阅读源码的一些总结,网上有人会说使用cgo很难,其实只是cgo的用法与go有差异,一旦涉及到C,可能就会让人望而却步。其实不然,用好cgo有以下几个方面:</p>    <ul>     <li>注意类型转换</li>     <li>注意C string的释放</li>     <li>注意使用 unsafe.Pointer</li>     <li>如果需要,添加自定义函数,避免过多或复杂的转换</li>     <li>最后一条,也是最重要的,要对C API熟悉</li>    </ul>    <h3>参考资料</h3>    <ul>     <li><a href="/misc/goto?guid=4959730284368793583" rel="nofollow,noindex">Command cgo</a></li>     <li><a href="/misc/goto?guid=4959730284455884938" rel="nofollow,noindex">C? Go? Cgo!</a></li>     <li><a href="/misc/goto?guid=4959730284536450207" rel="nofollow,noindex">cgo</a></li>     <li><a href="/misc/goto?guid=4959730284617482187" rel="nofollow,noindex">go-libav源码</a></li>    </ul>    <p> </p>    <p>来自:http://www.hackcv.com/index.php/archives/105/</p>    <p> </p>