Cgo 让 C 和 Go 手牵手

go 里面的 cgo 包可以直接调用 c 代码,只需要遵守一定的使用规则即可,对于那些熟悉 c/c++ 的开发者来说,不失为一个福音。

三个代表

cgo 会从 go 代码中去寻找 c/c++ 代码,约定:

紧挨在 import "C" 上面的所有 c 风格的注释语句都可以被 go 编译。

1.所有代码都写在一个 go 文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//foo.go
package main

/*
#include <stdio.h>

int count = 6;
void foo() {
printf("I am foo!\n");
}
*/
import "C"
import "fmt"

func main() {
fmt.Println(C.count)
C.foo()
}

这是最简便的方式,适用于 c 代码逻辑比较简单的情况。

1
2
3
4
5
6
7
8
hxzdeMac-mini:~ $ tree
.
├── foo.go
0 directories, 1 files
hxzdeMac-mini:~ $ go build -o foo
hxzdeMac-mini:~ $ ./foo
6
I am foo!

2.c 代码和 go 代码写在独立文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//foo.h
extern int count;
void foo();

//foo.c
#include <stdio.h>
#include "foo.h"

int count = 6;
void foo() {
printf("I am foo!\n");
}

//foo.go
package main

/*
#include "foo.h"
*/
import "C"
import "fmt"

func main() {
fmt.Println(C.count)
C.foo()
}

像这种直接编译 c 源码和 go 源码的方式,只需执行 go build,cgo 会在当前目录或 c 语言 -I 编译参数指定的目录寻找需要的
.h,.c文件。

1
2
3
4
5
6
7
8
9
10
hxzdeMac-mini:~ $ tree
.
├── foo.c
├── foo.go
└── foo.h
0 directories, 3 files
hxzdeMac-mini:~ $ go build -o foo
hxzdeMac-mini:~ $ ./foo
6
I am foo!

3.go 代码调用动态链接库

如果只提供了 c 的动态链接库和头文件,那么需要在 go 文件中指定 cgo 的 CFLAGS, CPPFLAGS, CXXFLAGS, LDFLAGS 等编译选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//foo.h
extern int count;
void foo();

//foo.go
package main

/*
#cgo LDFLAGS: -L./ -lfoo
#include "foo.h"
*/
import "C"
import "fmt“

func main() {
fmt.Println(C.count)
C.foo()
}
1
2
3
4
5
6
7
8
9
10
11
hxzdeMac-mini:~ $ tree
.
├── foo.go
├── foo.h
└── libfoo.dylib

0 directories, 3 files
hxzdeMac-mini:~ $ go build -o foo
hxzdeMac-mini:~ $ ./foo
6
I am foo!

go 引用 c

C 里面的标准类型可以在 go 里面直接引用:

1
2
3
4
5
6
7
8
9
10
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).

void* == unsafe.Pointer
__int128_t and __uint128_t == [16]byte

引用 struct,union,enum:

1
2
3
C.struct_Foo == struct Foo
C.union_Foo == union Foo
C.enum_Foo == enum Foo

Go 里面的 struct 不能包含 C 类型。

sizeof:

1
C.sizeof_struct_Foo == sizeof(struct Foo)

c 中结构体字段如果是 go 里面的关键字,那么可以在该结构体字段名前面加个 _ 来引用,比如:

1
2
3
4
5
6
7
typedef struct Foo {
int val;
int type;
}Foo;

foo := C.Foo{2,4}
fmt.Println(foo.val, foo._type) // 2 4

Cgo 将 C 类型转译成对应的 go 类型是不可导出的,因此,go 包导出的 API 里面不能含有 C 类型。

C 里面的函数执行结果可以赋给 go 里面的多返回值(执行结果和可能产生的错误值),即使是没有返回值的 void 类型函数。

1
2
n, err = C.sqrt(-1)
_, err := C.voidFunc()

cgo 目前还不支持直接调用 C 里面的函数指针,你可以声明一个包含 C 函数指针的 go 变量(如下的 f),这样就可以在 go 和 C 之间传递了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

/*
typedef int (*intFunc) ();

int bridge_int_func(intFunc f) {
return f();
}

int fortytwo()
{
return 42;
}
*/
import "C"
import "fmt"

func main() {
f := C.intFunc(C.fortytwo)
fmt.Println(int(C.bridge_int_func(f))) // Output: 42
fmt.Println(int(C.bridge_int_func(C.fortytwo))) // error
// cannot use _Cgo_ptr(_Cfpvar_fp_fortytwo) (type unsafe.Pointer) as type *[0]byte in argument to _Cfunc_bridge_int_func
}

在 C 里面, 如果一个函数接受一个数组参数,只需要传递数组名即可,但在 go 里面不行,你必须显示指定数组首元素的地址:

1
2
3
4
5
void f(int[] arr) {}

f(arr); // C

C.f(&C.arr[0]) // go

下面是几组 c 和 go 之间拷贝数据的语法:

1
2
3
4
5
6
7
8
9
10
11
// Go string to C string; result must be freed with C.free
func C.CString(string) *C.char

// C string to Go string
func C.GoString(*C.char) string

// C string, length to Go string
func C.GoStringN(*C.char, C.int) string

// C pointer, length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

c 引用 go

  1. 使用 //export <function-name> 导出 Go 代码
  2. 在 C 代码里面使用 extern 关键字引用 go 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

/*
extern void myprint(int i);

void dofoo(void) {
int i;
for (i=0;i<10;i++) {
myprint(i);
}
}
*/
import "C"

//export myprint
func myprint(i C.int) {
fmt.Printf("i = %v\n", uint32(i))
}

func main() {
C.dofoo()
}

传递指针

go 是一门垃圾回收语言,垃圾回收器需要知道每个指针所指向的内存位置,正因为如此,在 go 和 c 之间传递指针有些许限制。

  • go 指针:使用 & 操作符或 new 函数返回的指针
  • c 指针: 使用 C.malloc 返回的指针

首先,我们需要明白:

将指针传递给 struct 某个字段时,所涉及的 Go 内存是该字段占用的内存,而不是整个结构体占有的内存;将指针传递给数组或切片中的元素时,所涉及的 Go 内存是整个数组或整个切片的底层数组。

go 代码可以传递 go 指针给 c, 前提是这个指针指向的内存不包含任何 go 指针。c 代码必须保留这个属性:

它不能在 go 内存中存储任何 go 指针(即使是临时变量也不行)。

C 代码调用的 Go 函数可能会将 C 指针作为参数,并且可能通过这些指针存储非指针或 C 指针数据,但是它可能不会将 Go 指针存储在由C 指针指向的内存中;C 代码调用的 Go 函数可能会将 Go 指针作为参数,但它必须保留“指向的 Go 内存不包含任何 Go 指针”的属性。

Go 代码可能不会在 C 内存中存储 Go 指针,C 代码可能会在 C 内存中存储 Go 指针,这取决于上面的规则:当 C 函数返回时,它必须停止存储 Go 指针。

References

彦祖老师 wechat