CGO 入门

学下 CGO

CGO

用法

Go 调用 C

Go 可以通过如下方式引入 “C” 假包

1
2
3
4
5
// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -lpng
// #include <png.h>
import "C"

引入注释中的 C 语言语句, 这里的注释被称为前导码(preamble); 通过 #cgo 引入编译和链接选项

注意, 即便引入的 C 语言函数/变量为小写开头, 依然对 Go 是可见的, 包括静态函数; 但静态变量可能(原文如此)不可见

C 调用 Go

1
2
3
4
5
6
import "C"

//export add
func add(a, b C.int) C.int {
return a+b
}

export 指令将 Go 中的函数作为全局函数引入到 C 中, 因此若有多个 Go 包引入了同名函数, 则会报链接错误

1
2
3
4
5
#include "_cgo_export.h"

void foo() {
add(1, 1);
}

C 与 Go 之间的数据转换

大部分 C 语言的类型 A 的 CGO 类型是 C.A, 但是 C 语言类型中可能含有空格, 此类转换可以参照下表中的对应选项. 也可以使用 typedef 将其转换为单个单词的类型

C 语言类型 CGO 类型 Go 语言类型
char C.char byte
singed char C.schar int8
unsigned char C.uchar uint8
short C.short int16
unsigned short C.ushort uint16
int C.int int32
unsigned int C.uint uint32
long C.long int32
unsigned long C.ulong uint32
long long int C.longlong int64
unsigned long long int C.ulonglong uint64
float C.float float32
double C.double float64
size_t C.size_t uint
int8_t C.int8_t int8
uint8_t C.uint8_t uint8
int16_t C.int16_t int16
uint16_t C.uint16_t uint16
int32_t C.int32_t int32
uint32_t C.uint32_t uint32
int64_t C.int64_t int64
uint64_t C.uint64_t uint64

复杂类型

struct

C 语言中的结构体 struct A 的 CGO 类型是 C.struct_A, 在普通情况下 其内存布局和对齐规则与 C 语言中的一致(32 位/64 位), 但无法访问设置了特殊对齐规则的 C 结构体

若结构体中的成员名称为 Go 中的关键字, 可以通过添加下划线前缀对齐进行访问; 但若结构体中存在与其增加前缀后相同名称的成员, 则无法访问无前缀的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
struct A {
int type; // 1: type 是 Go 语言的关键字
float _type; // 2: 将屏蔽 CGO 对 type 成员的访问
};
*/
import "C"
import "fmt"

func main() {
var a C.struct_A
fmt.Println(a._type) // _type 对应成员变量 2
}

Go 亦不支持访问位域和 0 长数组

union

可以通过 C.union_xxx 访问 C 中的 union xxx, 但会被直接转换为相应大小的字节数组; 可以通过强制类型转换或在 C 中编写辅助函数获得原始数据

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
/*
#include <stdint.h>

union B1 {
int i;
float f;
};

union B2 {
int8_t i8;
int64_t i64;
};
*/
import "C"
import "fmt"

func main() {
var b1 C.union_B1;
fmt.Printf("%T\n", b1) // [4]uint8
fmt.Println("b1.i:", *(*C.int)(unsafe.Pointer(&b1)))
fmt.Println("b1.f:", *(*C.float)(unsafe.Pointer(&b1)))

var b2 C.union_B2;
fmt.Printf("%T\n", b2) // [8]uint8
}

enum

可以通过 C.enum_xxx 访问 C 中的 enum xxx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
enum C {
ONE,
TWO,
};
*/
import "C"
import "fmt"

func main() {
var c C.enum_C = C.TWO
fmt.Println(c)
fmt.Println(C.ONE)
fmt.Println(C.TWO)
}

数组

引入的 “C” 伪包提供了一系列 C 与 Go 之间数组相互转换的函数; 需要注意的是前两个函数使用 malloc 在 C 的堆中分配内存, 需要使用 C.free 手动释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char

// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer

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

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

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

以及 C 中数组作为函数参数会退化为指针, 直接传入数组名和数组首地址的指针是等价的; 但是 Go 则不同, Go 的数组名就代表数组本身, 可以通过 C.foo(unsafe.Pointer(&array[0])), C.foo(&C.array[0]) 传入 C 函数中

内存安全

Go 的内存会随着函数栈的动态伸缩发生移动, 若 C 语言持有移动前的指针在移动后进行访问会导致访问野指针

因此 CGO 要求传入 C 语言中的指针应该是被固定(pinned)的, Go 会定时检查这条规则

  • 作为参数传递给 C 函数的指向 Go 语言分配的内存的指针指向的内存在函数调用期间会被隐式固定;
  • 其他 Go 内存也可以使用 runtime.Pinner 进行手动固定和解除固定; 可以多次固定/解除固定, 但最终解除固定的次数应与固定的次数相同(类似 std::recursive_mutex)

这样保证了 C 不会访问失效指针, 但可能导致 Goroutine 因不能动态伸缩内存导致的阻塞.

同时需要注意应在获得指向 Go 语言分配的内存的指针后因直接传入 C 函数中, 若作为中间变量存储下来则可能导致在获取与调用 C 函数之间该指针就失效了

runtime/cgo 包的 Handle 提供了一种更高级的指针传递方法.

底层原理

编写简单的 CGO 程序如下

1
2
3
4
5
6
7
8
package main

//int sum(int a, int b) { return a+b; }
import "C"

func main() {
println(C.sum(1, 1))
}

使用

1
go tool cgo main.go

可以看到在 _obj 目录下生成了如下文件

1
2
3
4
5
6
7
_cgo_.o
_cgo_export.c
_cgo_export.h
_cgo_gotypes.go
_cgo_main.c
main.cgo1.go
main.cgo2.c

其中 main.cgo1.go 为对 C 函数和变量进行替换后的 Go 代码

1
2
3
4
5
6
7
8
9
10
// Code generated by cmd/cgo; DO NOT EDIT.

package main

//int sum(int a, int b) { return a+b; }
import _ "unsafe"

func main() {
println(( /*line :7:10*/_Cfunc_sum /*line :7:14*/)(1, 1))
}

其定义在 _cgo_gotypes.go 中, 可以看到最终调用 _cgo_runtime_cgocall 传入 C 函数地址和保存有调用函数的参数的结构体

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Code generated by cmd/cgo; DO NOT EDIT.

package main

import "unsafe"

import "syscall"

import _cgopackage "runtime/cgo"

type _ _cgopackage.Incomplete
var _ syscall.Errno
func _Cgo_ptr(ptr unsafe.Pointer) unsafe.Pointer { return ptr }

//go:linkname _Cgo_always_false runtime.cgoAlwaysFalse
var _Cgo_always_false bool
//go:linkname _Cgo_use runtime.cgoUse
func _Cgo_use(interface{})
//go:linkname _Cgo_no_callback runtime.cgoNoCallback
func _Cgo_no_callback(bool)
type _Ctype_int int32

type _Ctype_void [0]byte

//go:linkname _cgo_runtime_cgocall runtime.cgocall
func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32

//go:linkname _cgoCheckPointer runtime.cgoCheckPointer
//go:noescape
func _cgoCheckPointer(interface{}, interface{})

//go:linkname _cgoCheckResult runtime.cgoCheckResult
//go:noescape
func _cgoCheckResult(interface{})

//go:cgo_import_static _cgo_8ded9d269593_Cfunc_sum
//go:linkname __cgofn__cgo_8ded9d269593_Cfunc_sum _cgo_8ded9d269593_Cfunc_sum
var __cgofn__cgo_8ded9d269593_Cfunc_sum byte
var _cgo_8ded9d269593_Cfunc_sum = unsafe.Pointer(&__cgofn__cgo_8ded9d269593_Cfunc_sum)

//go:cgo_unsafe_args
func _Cfunc_sum(p0 _Ctype_int, p1 _Ctype_int) (r1 _Ctype_int) {
_cgo_runtime_cgocall(_cgo_8ded9d269593_Cfunc_sum, uintptr(unsafe.Pointer(&p0)))
if _Cgo_always_false {
_Cgo_use(p0)
_Cgo_use(p1)
}
return
}

其中的 C 函数是一个中间函数, 调用原始的 C 函数, 这两者都可以在 main.cgo2.c 看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int sum(int a, int b) { return a+b; }


void
_cgo_8ded9d269593_Cfunc_sum(void *v)
{
struct {
int p0;
int p1;
int r;
char __pad12[4];
} __attribute__((__packed__)) *_cgo_a = v;
char *_cgo_stktop = _cgo_topofstack();
__typeof__(_cgo_a->r) _cgo_r;
_cgo_tsan_acquire();
_cgo_r = sum(_cgo_a->p0, _cgo_a->p1);
_cgo_tsan_release();
_cgo_a = (void*)((char*)_cgo_a + (_cgo_topofstack() - _cgo_stktop));
_cgo_a->r = _cgo_r; // <-- 返回值会被存储在传入的结构体中
_cgo_msan_write(&_cgo_a->r, sizeof(_cgo_a->r));
}

小细节

  1. C.malloc 不是直接调用 C 中的 malloc 函数, 而是调用了一个 Go 包装的 malloc; 其永远不会返回 nil, 在内存不足时直接使程序崩溃, 与 Go 程序遇到内存不足时的行为保持一致
  2. CGO 不支持调用可变参数的 C 函数
  3. Go 不支持直接调用 C 函数指针, 但是可以保存指针并在 C 与 Go 之间传递
  4. CGO 导入的 C 语言类型为 unexported, 因此不应将其作为接口的参数. 且不同包使用的相同 C 类型是不同的
  5. 可以使用 C.sizeof_T 等效查看 C 中的 sizeof(T)
  6. 可以通过 errno.h 来实现类似 Go 中返回 err 的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
#include <errno.h>

static int div(int a, int b) {
if(b == 0) {
errno = EINVAL;
return 0;
}
return a/b;
}
*/
import "C"
import "fmt"

func main() {
v0, err0 := C.div(2, 1)
fmt.Println(v0, err0)

v1, err1 := C.div(1, 0)
fmt.Println(v1, err1)
}
1
2
3
4
5
6
7
8
//static void noreturn() {}
import "C"
import "fmt"

func main() {
_, err := C.noreturn()
fmt.Println(err)
}
  1. Go 中的 string 不一定以 \0 结尾, 但 C.CString 会贴心的在结尾加上一个

extern “C”

CGO 是 C 与 Go 的桥梁. 但由于名称修饰等原因, C++ 的动态库暴露出的符号不会只是函数名本身, 而是类似于 _ZN12MyNamespace10myFunctionEi 以实现函数重载, namespace 区分等功能

因此需要在头文件中形如

1
2
3
4
5
6
7
8
9
#ifdef __cplusplus 
extern "C" {
#endif

void foo();

#ifdef __cplusplus
}
#endif

告知编译器不要对其中的函数声明对应的实现做名称修饰. 使用 ifdef 可以将该头文件直接复制到 Go 项目中让 CGO 编译(C 编译器不会接受 extern "C")

由于没有修饰, 不能使用函数重载, 也不能使用 namespace(如有同名函数或变量依然会导致冲突); 但函数的实现依然可以使用任何 C++ 的特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int x;
namespace A {
extern "C" int f();
extern "C" int g() { return 1; }
extern "C" int h();
extern "C" int x(); // ill-formed: same name as global-space object x
}

namespace B {
extern "C" int f(); // A::f and B::f refer to the same function
extern "C" int g() { return 1; } // ill-formed, the function g with C language linkage has two definitions
}

int A::f() { return 98; } // definition for the function f with C language linkage
extern "C" int h() { return 97; } // definition for the function h with C language linkage
// A::h and ::h refer to the same function

此外还需要注意 extern "C"extern "C" {} 的区别

1
2
3
4
extern "C" int i;                   // declaration
extern "C" {
int i; // definition
}

extern 甚至可以将其中的内容当做其他语言来做名称修饰, 只不过标准只要求编译器必须支持 “C”

语言链接也是函数类型的一部分

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
27
28
29
extern "C"                      // the name f1 and its function type have C language linkage;
void f1(void(*pf)(int)); // pf is a pointer to a C function

extern "C" typedef void FUNC();
FUNC f2; // the name f2 has C++ language linkage and the
// function's type has C language linkage

extern "C" FUNC f3; // the name of function f3 and the function's type have C language linkage

void (*pf2)(FUNC*); // the name of the variable pf2 has C++ linkage and the type
// of pf2 is “pointer to C++ function that takes one parameter of type
// pointer to C function”
extern "C" {
static void f4(); // the name of the function f4 has internal linkage (not C language linkage)
// and the function's type has C language linkage.
}

extern "C" void f5() {
extern void f4(); // OK: Name linkage (internal) and function type linkage (C language linkage)
// obtained from previous declaration.
}

extern void f4(); // OK: Name linkage (internal) and function type linkage (C language linkage)
// obtained from previous declaration.

void f6() {
extern void f4(); // OK: Name linkage (internal) and function type linkage (C language linkage)
// obtained from previous declaration.
}