cgo 命令详解

原文地址:Command cgo

Cgo 允许创建能调用 C 代码的 Go 包。

通过 go 命令使用 cgo

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

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

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

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

CPPFLAGS 和 LDFLAGS 也可以通过 #cgo pkg-config 命令来通过 pkg-config 来获取。随后的指令指定获取的包名。比如:

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

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

处于安全原因,只有一部分标志 (flag) 允许设置,特别是 -D,-I,以及 -l。如果想允许设置额外的 flag,可以设置 CGO_CFLAGS_ALLOW,按正则条件匹配新的 flag。如果想禁止当前允许的 flag,设置 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 环境变量会被赋值为由上述指令派生的值。跟包有关的 flag 应该使用指令来设置,而不是通过环境变量来设置,这样构建工作可以处于未修改的环境中。从环境变量中获取的值不属于上述描述的安全限制。

在一个 Go 包中的所有 CPPFLAGS 和 CFLAGS 指令都会被串联到一起,然后被用来编译这个包中的 C 文件。包中所有的 CPPFLAGS 和 CXXFLAGS 指令都会被串联到一起,用于编译包中的 C++ 文件。包中所有的 CPPFLAGS 和 FFLAGS 指令都会被串联到一起,用来编译包中的 Fortran 文件。一个程序中所有包中的 LDFLAGS 指令都会被串联到一起,并在连接 (link) 的时候使用。所有的 pkg-config 指令都会被串联到一起,并同时发给 pkg-config,来添加每个合适的 flag 命令行的集合。

在解析 cgo 指令时,所有字符串中的 ${SRCDIR} 都会被替换成当前源文件所在的绝对路径。这样允许提前编译的静态库被包含在包路径中且被正确地链接。比如,如果包 foo 在 /go/src/foo 路径下:

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

会被扩展成:

// #cgo LDFLAGS: -L/go/src/foo/libs -lfoo

当 Go tools 发现一个或多个 Go 文件使用特殊的引用 "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 环境变量来修改。这些环境变量可能包括命令行选项。

当在被期望的系统上构建 Go 时,cgo tool 默认是开启的。在交叉编译时,它默认是关闭的。你可以通过设置 CGO_ENABLED 环境变量来控制开启和关闭:设置为 1 表示启用 cgo,设置为 0 表示不启用 cgo。如果 cgo 被启用,则 go tool 会设置构建约束“cgo”。

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

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

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

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

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

C 类型名称

Go 类型名称
charC.char
signed charC.schar
unsigned charC.uchar
shortC.short
unsighed shortC.ushort
intC.int
unsigned intC.uint
longC.long
unsigned longC.ulong
long longC.longlong
unsigned long longC.ulonglong
floatC.float
doubleC.double
complex floatC.complexfloat
complex doubleC.complexdouble
void*unsafe.Pointer
__int128_t   __uint128_t[16]byte

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

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

C 的任意类型 T 的大小,在 Go 中用 C.sizeof_T 表示,比如 C.sizeof_struct_stat。

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

size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);

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

由于通常情况下 Go 不支持 C 的联合类型,C 的联合类型在 Go 中被表示为相同长度的比特数组。

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

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

Cgo 把 C 类型转换成等价的不可输出的 Go 类型。因此 Go 包不应该在它的输出接口中暴露 C 类型:同一个 C 类型,在不同包里是不一样的。

任意 C 函数 (即使是 void 函数) 可能会被在多赋值场景下被调用,以获取返回值 (如果有的话),和 C errno 值 (作为 error 值)。如果 C 函数返回类型是 void,那么相应的值可以用 _ 代替。

请看下面的例子:

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

目前还不支持调用 C 函数指针,不过你可以声明存放 C 函数指针的 Go 变量,并在 Go 和 C 之间传递。C 函数可以调用从 Go 中获取到的函数指针,例如:

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 定义中:

// 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 代码中使用:

//export MyFunction
func MyFunction(arg1, arg2 int, arg3 string) int64 {...}

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

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

extern int64 MyFunction(int arg1, int arg2, GoString arg3);
extern struct MyFunction2_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 代码去做任何它想做的事情。不过破坏这些规则的程序可能会以意想不到且不可预测的方式失败。

特殊场景

一些一般会被表示为 Go 指针的 C 类型会被表示成 uintptr。这些 C 类型包括:

  1. Darwin 上的 *Ref 类型,源于 CoreFoundation 的 CFTypeRef 类型。
  2. Java JNI 接口中的对象类型:
jbject
jclass
jthrowable
jstring
jarray
jbooleanArray
jbyteArray
jcharArray
jshortArray
jintArray
jlongArray
jfloatArray
jdoubleArray
jobjectArray
jweak

这些类型在 Go 里被表示成 uintptr,否则他们会把 Go 垃圾回收器弄混淆;它们有时候不是真正的指针,而是编码在指针类型中的数据结构。所有对这些类型的操作都必须在 C 里面进行。初始化一个空的这样的引用的适当常量是 0,而不是 nil。

这些特殊场景是在 Go 1.10 中被引入。如果想从 Go 1.9 或更早版本自定升级代码,需要在 Go fix 工具中使用 cftype 或 jni 重写:

go tool fix -r cftype <pkg>
go tool fix -r jni <pkg>

它会在合适的地方把 nil 替换成 0.

直接使用 cgo

使用方法:

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

Cgo 把指定的输入 Go 源文件转换成若干输出的 Go 和 C 源文件。

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

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

-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
    源文件路径。

 

转载于:https://my.oschina.net/dokia/blog/1845536

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值