使用Cgo的一点总结

今天想给一个C库写一个Golang binding,就查了一下cgo的使用,也遇到了一些坑。

cgo的基本使用

想在Go代码中使用C语言必须在代码开头注释中写,然后再紧接着的下一行写import "C",这样就算是导入完成了。这个”C”不是一个真正的包,而是一个类似于命名空间的东西,所有能调用的C的变量、函数都包含在里面。
举个最简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
// #include <stdio.h>
// #include <stdlib.h>
/*
void print(char *str) {
printf("%s\n", str);
}
*/

import "C"
import "unsafe"

func main() {
s := "hello"
cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
C.print(cs)
}

这个例子展示了cgo的基本使用方法。开头的注释中写了要调用的函数和相关的头文件,头文件被include之后里面的所有元素都会被加入到”C”这个命名空间中。需要注意的是,import "C"必须单独一行,不能与其他包一同import
向C函数传递参数也很简单,就直接转化成对应类型传递就可以。如上例中C.CString强制类型转换将一个字符串转化为了C字符串然后传递给了print函数。需要注意的是,Go是强类型语言,所以cgo中传递的参数类型必须与声明的类型完全一致,而且传递前必须用”C”中的转化函数转换成对应的C类型,不能直接传入Go中类型的变量。

  • 数值类型
    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.char*C.int等。比较特殊的是void *,它的对应类型是unsafe.Pointer。Go中禁止对指针进行算术操作,unsafe.Pointer类型可以直接转换成uintptr作为数字进行操作,来绕开Go对指针运算的限制。
    另外一点就是在判断指针是否为空时可以直接拿指针与nil比较,不必与unsafe.Pointer(uintptr(0))或其他类型指针零值比较。
  • 数组和字符串类型。
    由于字符串比较特殊,所以专门有C.CString类型对应。注意这个强制类型转换会分配一块空间然后将原字符串拷贝过去,而且这块空间不会被Go的垃圾回收扫描到,所以记得用C.free来回收。
    其他的数组类型比较麻烦一点,因为C中的数组与Go中的数组完全不同。我们只能自己编写函数通过指针操作遍历数组,然后逐个append到一个slice中。比如可以这么写:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // int cArray[] = {1, 2, 3, 4, 5, 6, 7};
    import "C"
    func CArrayToGoArray(cArray unsafe.Pointer, size int) (goArray []int) {
    p := uintptr(cArray)
    for i :=0; i < size; i++ {
    j := *(*int)(unsafe.Pointer(p))
    goArray = append(goArray, j)
    p += unsafe.Sizeof(j)
    }
    return
    }

    func main() {
    … …
    goArray := CArrayToGoArray(unsafe.Pointer(&C.cArray[0]), 7)
    fmt.Println(goArray)
    }

以上代码摘自这篇文章,在此表示感谢。
Go不能将C数组自动转化成地址,所以要将数组的第一个元素取地址传给函数。
而将Go数组传给C就相对容易了一些。只需要取数组的第一个元素地址,然后转化成相应的指针类型即可。例如:

1
2
3
var a []int
a = [1, 2, 3]
ca := (*C.int)(&a[0])

  • 结构体
    结构体对应到”C”中的名字是struct_xxx,就是在struct和名字之间加了个下划线。访问域也可以直接用点操作符访问。由于Go没有->操作符,所以结构体指针必须先解引用再用点操作符访问相关域。
  • 类型别名
    类型别名的访问有两种情况。
    首先是原生类型的别名。比如:
    1
    2
    3
    // typedef int int_alias
    import "C"
    var a C.int_alias

可以直接访问。
还有就是复合类型的别名:

1
2
3
//typedef struct example example_alias
import "C"
var a C.struct_example_alias

必须在别名前加上原类型名,比如上例中example是个struct,所以必须在example_alias前加上struct。

静态库的链接

静态库的连接还是很简单的。我们要理解cgo的原理。其实cgo就是先由编译器识别出import "C"的位置,然后在其上的注释中提取C代码,最后调用C编译器进行分开编译。所以在cgo中可以指定编译参数。
连接静态库时必须添加参数。具体方法如下:

1
2
// #cgo LDFLAGS: -L. -lhello
// #include "hello.h"

这条宏就为代码加入了libhello静态库的连接。在编译时,会先从-L后面的目录查找libhello.a,如果没有找到就到系统默认的目录下查找。
我们只需写好hello.c和hello.h,然后执行

1
2
gcc -c hello.c
ar r libhello.a hello.o

就创建好了静态库,然后Go程序就能运行了。
除了LDFLAGS之外,#cgo宏还能修改CFLAGS, CPPFLAGS, CXXFLAGS这几个编译参数,但是我目前还没有用到过。另外,gccgo编译器支持C++库的链接,GC则不支持。

还有一点,C函数中对errno的设置在Go中会作为一个error返回值返回,这个error返回值的打印结果就是errno的对应错误信息,这点很贴心。

动态库的链接

动态库没法在编译时进行链接,只能在代码中调用。在*nix下我们通常使用dlfcn.h中的一系列函数,举个例子:

1
2
3
4
5
6
7
8
9
10
// #cgo LDFLAGS: -ldl
// #include <dlfcn.h>
// #include <stdlib.h>
import "C"
import "unsafe"

libname := C.CString("example.so")
defer C.free(unsafe.Pointer(libname))
lib := C.dlopen(libname, C.RTLD_NOW)
defer C.dlclose(lib)

这样就算是打开了动态库,之后的调用请参考man手册,其实用起来是很麻烦的。在获得了函数指针之后,使用syscall中的一系列函数进行调用:

1
2
3
4
5
syscall.Syscall
syscall.Syscall6
syscall.Syscall9
syscall.Syscall12
syscall.Syscall15

这套函数的参数都类似于:syscall.Syscall(trap, nargs, a1, a2, a3),nargs是函数参数数量,这五个函数调用适用于参数数量不同的函数调用,比如Syscall6支持最多四至六(闭区间)个参数的函数调用。多余的参数用0补全。
是不是感觉很麻烦呢?好在标准库的编写者也很清楚这一点,所以还有个syscall.Call函数,它的参数是个不定参数,可以直接syscall.Call(function, args...)这样来调用。事实上它的内部实现还是使用了上面的那一套函数,只不过加了个封装。
windows下好像有’syscall.Loadlibrary()’函数,用起来比dlfcn.h那一套简单,不过我不在windows下写代码,也就没仔细看。

总结

这篇文章到这里就结束了,我写这篇文章一方面是当做个笔记,另一方面希望能让读者看完后对cgo的用法有一点了解。目前版本的cgo本身还是挺完善的,至少Go中调用C函数的基本使用时没有问题的。读者可能会发现我没有提从C中调用Go函数的功能,一方面是因为我觉得它的应用场景很窄,而且这个功能还非常有限,毕竟Go中有GC,有多返回值,另一方面cgo的文档实在是不完善,啥都得google前人的实验资料或者自己写代码去试。

总之,我觉得相比于lua/python那样的机制,cgo方便一点,但是一般情况下还是不要使用cgo,除非要调用现成的C库,或者有些场景对性能有极致需求,再或者希望能降低GC时间(cgo中分配的内存可以躲过GC的扫描)。