Cgo 支持创建调用 C 代码的 Go 包

通过 go 命令使用 cgo

为了使用 cgo, 你需要在普通的 Go 代码中导入一个 伪包 "C"。这样 Go 代码就可以引用一些 C 的类型 (如 C.size_t)、变量 (如 C.stdout)、或函数 (如 C.putchar)。

如果对 “C” 的导入语句之前紧贴着是一段注释,那么这段注释被称为前言,它被用作编译 C 部分的头文件。如下面例子所示:

1
2
3
// #include <stdio.h>
// #include <errno.h>
import "C"

前言中可以包含任意 C 代码,包括函数和变量的声明和定义。虽然他们是在 “C” 包里定义的,但是在 Go 代码里面依然可以访问它们。所有在前言中声明的名字都可以被 Go 代码使用,即使名字的首字母是小写的。static 变量是个例外:它不能在 Go 代码中被访问。但是 static 函数可以在 Go 代码中访问。

$GOROOT/misc/cgo/stdio 和 $GOROOT/misc/cgo/gmp 中有一些相关例子。”C? Go? Cgo!” 中介绍了如何使用 cgo: https://golang.org/doc/articles/c_go_cgo.html。

CFLAGS, CPFLAGS, CXXFLAGS, FFLAGS 和 LDFLAGS 可以通过在上述注释中使用伪 #cgo 指令来定义,进而调整 C、C++ 或 Fortan 编译器的参数。多个指令定义的值会被串联到一起。这些指令可以包括一系列构建约束,并将其影响限制在满足其中一个约束条件的系统中。(https://golang.org/pkg/go/build/#hdr-Build_Constraints 介绍了约束语法细节)。下面是一个例子:

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

说明:在上面示例中,前言由 4 个注释行组成.

  • 第一个注释行:含义是预定义一个名为PNG_DEBUG的宏并将它的值设置为1
  • 第二个注释行:含义是如果在Linux操作系统下,则预定义一个名为LINUX的宏并将它的值设置为1
  • 第三个注释行:含义是告诉链接器需要用到一个库名为png的代码库文件(与链接器有关的)
  • 第四个注释行:引入了C语言的标准代码库png.h

另外,CPPFLAGS 和 LDFLAGS 也可以通过 pkg-config 工具,使用 #cgo pkg-config 指令,后跟包名称获得。

1
2
3
// #cgo pkg-config: png cairo
// # include <png.h>
import "C"

默认的 pkg-config 工具配置可以通过设置 PKG_CONFIG 环境变量来修改。

出于安全原因,只有一部分标志(flag)允许设置,特别是 特别是 -D,-I,以及 -l. 要允许其他标志,请将 CGO_CFLAGS_ALLOW 设置为与新标志匹配的正则表达式。要禁止原本允许的标志,请将 CGO_CFLAGS_DISALLOW 设置为匹配必须禁止的参数的正则表达式。在这两种情况下,正则表达式必须与完整参数匹配:如果想允许 -mfoo=bar 指令,设置 CGO_CFLAGS_ALLOW=‘-mfoo.*‘,而不能仅仅设置 CGO_CFLAGS_ALLOW=‘-mfoo’。类似名称的变量控制 CPPFLAGS, CXXFLAGS, FFLAGS, 以及 LDFLAGS。

当编译时,CGO_CFLAGS,CGO_CPPFLAGS,CGO_CXXFLAGS,CGO_FFLAGS和CGO_LDFLAGS这些环境变量都会从指令中提取出来,并加入到flags中。包特定的flags需要使用指令来设置,而不是通过环境变量,所以这些构建可以在未更改的环境中也能正常运行。

包中所有 cgo CPPFLAGS 和 CFLAGS 指令都会被连接在一起,用来编译该包中的 C 文件。包中所有的 CPPFLAGS 和 CXXFLAGS 指令都会被连接在一起,用于编译该程序包中的 C++ 文件。包中所有的 CPPFLAGS 和 FFLAGS 指令都会被连接在一起,用于编译该程序包中的 Fortran 文件。程序中的任何 package 中,所有的 LDFALGS 指令都会被连接在一起并在链接时使用。所有的pkg-config指令会被连接起来,并同时发送给pkg-config,以添加到每个适当的命令行标志集中。

当 cgo 指令被解析的时候,任何出现 ${SRCDIR} 字符串的地方,都会被替换为当前源文件所在的绝对路劲。这就允许预编译的静态库包含在包目录中,并能够正确的链接。例如,如果包 foo 在 /go/src/foo 路径下:

1
// #cgo LDFALGS: -L${SRCDIR}/libs -lfoo

会被扩展成:

1
// #cgo LDFALGS: -L/go/src/foo/libs -lfoo

心得:// #cgo LDFLAGS: 可以用来链接静态库。-L 指定静态库所在的目录, -l 指定静态库的文件名,注意静态库文件名必须有 lib 前缀,但是这里不需要写,比如上面的 -lfoo 实际上找的就是 libfoo.a 文件

当 Go tool 发现一个或多个 Go 源文件使用了特殊的 import "C" 时,他就会在当前路劲中寻找非 Go 的文件,并把这些文件编译成 Go 包的一部分。任何 .c.s.S 文件都会被 C 编译器编译。 任何 .cc.cpp.cxx 文件都会被 C++ 编译器编译。任何 .f, .F, .for 或者.f90 文件会被 fortran 编译器编译。任意 .h, .hh, .hpp 或 .hxx 文件都不会被分别编译,但是如果这些头文件被修改了,那么 C 和 C++ 文件会被重新编译。默认的 C 和 C++ 编译器都可以分别通过设置 CC 和 CXX 环境变量来修改。这些环境变量可能包括命令行选项。

cgo tool 对于本地构建默认是启用的。而在交叉编译时默认是禁用的。你可以在运行 go tool 时,通过设置 CGO_ENABLED 环境变量来控制它:设置为 1 表示启用 cgo, 设置为 0 则关闭。如果 cgo 启用,go tool 将会设置构建约束 cgo

在交叉编译时,你必须为 cgo 指定一个交叉编译器。你可以在使用 make.bash 构建工具链时,设置通用的 CC_FOR_TARGET 或更明确的 CCFOR${GOOS}_${{GOARCH}}(比如 CC_FOR_linux_arm) 环境变量来指定,或者在你运行 go tool 的任何时候, 都可以设置 CC 环境变量

CXX_FOR_TARGET, CXXFOR${GOOS}_${GOARCH} 以及 CXX 环境变量使用方式类似。

Go 引用 C (Go 中使用 C 定义的函数)

在 Go 文件中,如果 C 的结构体字段名称是 Go 中的关键字,可以通过用下划线作为前缀来访问:例如,如果 C 结构体 x 有个字段名字叫 “type”,那么在 Go 里面可以通过 “x._type” 来访问它。如果 C 结构体的字段无法在 Go 里表达,比如 bit 字段或未对齐字段,那么这些字段在 Go 结构体中会被忽略,并被合适的填充所取代,以访问下一个字段或结构体的结尾。

标准的 C 数值类型与 Go 中的访问类型对应关系如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
C 类型名称                  Go 类型名称
char	                    C.char
signed char	                C.schar
unsigned char	            C.uchar
short	                    C.short
unsighed short	            C.ushort
int	                        C.int
unsigned int	            C.uint
long	                    C.long
unsigned long	            C.ulong
long long	                C.longlong
unsigned long long	        C.ulonglong
float	                    C.float
double	                    C.double
complex float	            C.complexfloat
complex double	            C.complexdouble
void*	                    unsafe.Pointer
__int128_t   __uint128_t	[16]byte

C 的 void* 类型由 Go 的 unsafe.Pointer 表示。C 的 __int128_t 和 __uint128_t 由 [16]byte 表示。

一些通常在 Go 中被表示为指针类型的特殊 C 类型会被表示成 uintptr。下面的特殊场景会对此进行介绍。

如果想直接访问 C 中的结构体,联合体,或者枚举类型时,在其名字前面加上 struct_、union_、或 enum_、就像 C.struct_stat 这样。

心得:但是如果 C 结构体用了 typedef struct 设置了别名,则就不需要加上前缀来访问,可以直接通过 C.alias 来访问该类型

任何一个 C 类型 T 的大小,都可以通过 C.sizeof_T 获得,如获取结构体 stat 的大小:C.sizeof_struct_stat.

可以在 Go 文件中声明一个带有特殊类型 GoString 类型的 C 函数。可以使用普通的 Go 字符串调用这个函数。可以通过调用这些 C 函数来获取字符串长度,或指向字符串的指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

/*
size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);
*/
import "C"
import "fmt"

func main() {
	var str = "hello, world"
	fmt.Println("len from Go func: ", len(str))
	fmt.Println("ptr from Go func: ", &str)
}

Note: 这些函数只能被写在 Go 文件的前言里,而不能写在其他的 C 文件中。 C 代码一定不能修改 _GoStringPtr 返回的指针的内容。注意字符串内容可能不是 NULL 结尾。

由于Go不支持C的联合类型,所以C的联合类型被表示为具有相同长度的Go字节数组。

Go 的结构体不能嵌入 C 的类型

对于一个非空的 C 结构体,如果它结尾字段大小为 0,那么 Go 代码无法引用这个字段。为了获取到这样字段的地址,你只能先获取结构体的地址,然后将地址加上这个结构体的大小。这也是能获取到这个字段的唯一方式。

cgo会将C类型转换为对应的,非导出的的Go类型。因为转换是非导出的,一个Go包就不应该在它的导出API中暴露C的类型:在一个Go包中使用的C类型,不同于在其它包中使用的同样C类型。

可以在多个赋值语境中,调用任何C函数(甚至是void函数),来获取返回值(如果有的话),以及C errno变量作为Go error(如果方法返回void,则使用 _ 来跳过返回值)。例如:

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

目前还不支持调用 C 函数指针,不过你可以声明存放 C 函数指针的 Go 变量,并在 Go 和 C 之间传递。C 函数可以调用从 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

// 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)))
    // 输出:42
}

在 C 函数中,如果传入参数是一个固定大小的数组,那么它实际需要的是一个指向数据第一个元素的指针。C 编译器知道这个调用约定,并在调用的时候会做相应的调整。然后,Go 不能。在 Go 中,你必须显式地传递一个指向第一个元素的指针:C.f(&C.x[0])

在Go和C类型之间,通过拷贝数据,还有一些特殊的方法转换。用Go伪代码定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Go 字符串转换为 C 字符串
// C 字符串使用 malloc 来分配到 C 堆里。
// 调用方需要把它释放掉,比如调用 C.free (如果需要 C.free,确保程序包含 stdlib.h)
func C.CString(string) *C.char

// Go []byte 切片转换为 C 数组
// C 数组使用 malloc 分配到 C 堆里。
// 调用方需要把它释放掉,比如调用 C.free (如果需要 C.free,确保程序包含 stdlib.h)
func C.CBytes([]byte) unsafe.Pointer

// C 字符串转换为 Go 字符串
func C.GoString(*C.char) string

// 有明确长度的 C 数据转换为 Go 字符串
func C.GoStringN(*C.char, C.int) string

// 有明确长度的 C 数据转换为 Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

有一个特殊的例子,C.malloc 方法不会直接调用 C 库中的 malloc 方法,而是会调用一个 Go 的帮助函数来包装 C 库中的 malloc 方法,并保证永远不会返回空。如果 C malloc 函数显示内存不足,Go 的帮助函数会使程序崩溃,就像 Go 自己内存不足一样。因为 C.malloc 不会失败,所以它不会返回二值结果,不会把 errno 一起返回。

C 引用 Go (C 中使用 Go 定义的函数)

Go 函数可以通过以下方式输出到 C 代码中使用:

1
2
3
4
5
//export MyFunction
func MyFunction(arg1, arg2 int, arg3 string) int64 {...}

//export MyFunction2
func MyFunction2(arg1, arg2 int, arg3 string) (int64, *C.char) {...}

这两个函数可以通过以下方式被 C 代码使用:

1
2
extern int64 MyFunction(int arg1, int arg2, GoString arg3);
extern struct MyFuction2_return MyFunction2(int arg1, int arg2, GoString arg3);

这两个声明会被放在自动生成的 _cgo_export.h 头文件里,位于从 cgo 输入文件拷贝过来的前言之后。多值返回的函数会被映射为一个结构体返回。

并不是所有的 Go 类型都能被映射成可用的 C 类型。Go 结构体就不支持,我们需要使用 C 结构体类型。Go 数组也不支持,我们需要使用 C 指针。

使用字符串类型做入参的 Go 函数可以被 C 类型的 GoString 替代,正如上面的例子所描述的那样。GoString 会自动在前言中定义。注意 C 代码中无法创建一个这种类型的值,它只是用来把 Go 的字符串值传递给 C,然后从 C 传递给 Go。

在文件中使用 //export 时,对前言有个限制:由于它们是被拷贝到两个不同的 C 输出文件中,它必须不能包含任何定义,而只能包含声明。如果一个文件既包含定义,有包含声明,那么两个文件将会产生重复的符号,这样链接会失败。为了避免这种情况发生,前言中的定义必须被放在其他文件的前沿中,或放在 C 文件中。

传递指针

Go 是一种垃圾回收语言,垃圾回收器需要知道每个指针指向的 Go 内存地址。所以在 Go 和 C 之间传递指针时有些限制。

在这一节,术语“Go 指针”意味着由 Go 分配的指向内存的指针 (比如使用 & 操作符或调用事先定义的 new 方法)。“C 指针”意味着由 C 分配的指向内存的指针 (比如调用 C.malloc)。一个指针是 Go 指针还是 C 指针,取决于内存是如何分配的,它和指针类型无关。

请注意,除了类型的零值外,某些 Go 类型总是包括 Go 指针。字符串、切片、接口、channel、map 和函数类型皆是如此。一个指针可能是一个 Go 指针,也可能是一个 C 指针。数组和结构体可能不会包括 Go 指针,这取决于元素类型。下面关于 Go 指针的讨论不止适用于指针类型,也适用于其他包含 Go 指针的类型。

Go 代码可以把 Go 指针传递给 C,前提是指向 Go 内存的指针不包含任何 Go 指针。C 代码必须保留这样的属性:它不能在 Go 内存中存储任何 Go 指针,即使是临时性的。当传递一个指向结构体字段的指针时,所讨论的 Go 内存是被字段占用的内存,而不是整个结构体。当传递一个指向数组或切片元素的指针时,所讨论的 Go 内存是整个数组或切片的整个后备数组。

在调用返回后,C 代码可能不会保存一份 Go 指针的备份。包括 GoString 类型。如上面讨论,这种类型包含了一个 Go 指针。GoString 类型的值可能不会被 C 代码保留。

一个 C 代码调用的 Go 函数可能不会返回一个 Go 指针 (这意味着它可能不会返回一个字符串、切片、channel、等等)。一个 C 代码调用的 Go 函数可以使用 C 指针作为参数,也可以通过这些指针存储非指针或 C 指针数据,但是它不能在内存中存储被 C 指针指向的内存。一个被 C 代码调用的 Go 函数可以拿一个 Go 指针作为参数,但是它必须保存这个特性:指针指向的 Go 内存里不包含任何 Go 指针。

这些规则会在运行时进行动态检查。检查动作由对 GODEBUG 环境变量的 cgocheck 设置来控制。默认的设置是 GODEBUG=cgocheck=1,它实现了相当简单的动态检查。这些检查可以通过设置 GODEBUG=cgocheck=0 来取消。完整的指针处理检查需要设置 GODEBUG=cgocheck=2,这种检查会对运行时造成负担。

使用不安全包会破坏这种强制检查,当然没有什么能阻止 C 代码去做任何它想做的事情。不过破坏这些规则的程序可能会以意想不到且不可预测的方式失败。

特殊场景

直接使用 cgo 指令: go tool cgo

Usage:

1
go tool cgo [cgo options] [-- compiler options] gofiles...

cgo 把指定输入的 Go 源文件转换为多个 Go 和 C 源文件输出

当调用 C 编译器编译包中的 C 部分时,编译器选项会不加解释地直接传递过去

下面是直接运行 cgo 时的可用选项:

 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
-V
    打印 cgo 版本然后退出。
-debug-define
    debug 选项,打印 #defines.
-debug-gcc
    debug 选项,跟踪 C 编译器执行和输出。
-dynimport file
    写入由文件导入的符号列表。写到 -dynout 参数或标准输出。在构建 cgo 包的时候通过 go build 来使用。
-dynlinker
    作为 -dynimport 输出的一部分,写入动态连接器。
-dynout file
    把 -dynimport 输出写入文件。
-dynpackage package
    为 -dynimport 输出设置 Go 包。
-exportheader file
    如果有输出函数,把这些生成的输出声明写入文件中。C 代码可以 #include 这些文件,然后就能看到输出函数了。
-importpath string
    Go 包的导入路径。可选。用于在生成路径中有更好的评论。
-import_runtime_cgo
    是否在生成的输出里设置 import runtime/cgo (默认设置此选项)
-import_syscall
    是否在生成的输出里设置 import syscall (默认设置此选项)
-gccgo
    为 gccgo 编译器设置输出,而不是为 gc 编译器设置输出。
-gccgoprefix prefix
    在 gccgo 中使用的 -fgo-prefix 选项。
-gccgopkgpath path
    在 gccgo 中使用的 -fgo-pkgpath 选项。
-godefs
    在 Go 语法中写入输入文件,使用真实值替换 C 包名称。用来当引导一个新的目标时在生成的 syscall 包中生成文件。
-objdir directory
    把所有生成的文件放在指定路径下。
-srcdir directory
    源文件路径。

See Also

Thanks to the authors 🙂

返回目录