Golang | cmd - go cgo
Contents
Cgo 支持创建调用 C 代码的 Go 包
通过 go 命令使用 cgo
为了使用 cgo, 你需要在普通的 Go 代码中导入一个 伪包 "C"
。这样 Go 代码就可以引用一些 C 的类型 (如 C.size_t)、变量 (如 C.stdout)、或函数 (如 C.putchar)。
如果对 “C” 的导入语句之前紧贴着是一段注释
,那么这段注释被称为前言
,它被用作编译 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 介绍了约束语法细节)。下面是一个例子:
|
|
说明:在上面示例中,前言由 4 个注释行组成.
- 第一个注释行:含义是预定义一个名为PNG_DEBUG的宏并将它的值设置为1
- 第二个注释行:含义是如果在Linux操作系统下,则预定义一个名为LINUX的宏并将它的值设置为1
- 第三个注释行:含义是告诉链接器需要用到一个库名为png的代码库文件(与链接器有关的)
- 第四个注释行:引入了C语言的标准代码库png.h
另外,CPPFLAGS 和 LDFLAGS 也可以通过 pkg-config 工具,使用 #cgo pkg-config
指令,后跟包名称获得。
|
|
默认的 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 路径下:
|
|
会被扩展成:
|
|
心得:// #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 中的访问类型对应关系如下:
|
|
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 函数来获取字符串长度,或指向字符串的指针。
|
|
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,则使用 _ 来跳过返回值)。例如:
|
|
目前还不支持调用 C 函数指针,不过你可以声明存放 C 函数指针的 Go 变量,并在 Go 和 C 之间传递。C 函数可以调用从 Go 中获取到的函数指针, 例如:
|
|
在 C 函数中,如果传入参数是一个固定大小的数组,那么它实际需要的是一个指向数据第一个元素的指针。C 编译器知道这个调用约定,并在调用的时候会做相应的调整。然后,Go 不能。在 Go 中,你必须显式地传递一个指向第一个元素的指针:C.f(&C.x[0])
在Go和C类型之间,通过拷贝数据,还有一些特殊的方法转换。用Go伪代码定义如下:
|
|
有一个特殊的例子,C.malloc 方法不会直接调用 C 库中的 malloc 方法,而是会调用一个 Go 的帮助函数来包装 C 库中的 malloc 方法,并保证永远不会返回空。如果 C malloc 函数显示内存不足,Go 的帮助函数会使程序崩溃,就像 Go 自己内存不足一样。因为 C.malloc 不会失败,所以它不会返回二值结果,不会把 errno 一起返回。
C 引用 Go (C 中使用 Go 定义的函数)
Go 函数可以通过以下方式输出到 C 代码中使用:
|
|
这两个函数可以通过以下方式被 C 代码使用:
|
|
这两个声明会被放在自动生成的 _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 时的可用选项:
|
|
See Also
Thanks to the authors 🙂