cgo 命令详解

原文地址:Command cgo。

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

通过 go 命令使用 cgo

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

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

// #include 
// #include 
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 
import "C"

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

// #cgo: pkg-config: png cairo
// #include 
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 类型名称
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

一些通常在 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 
go tool fix -r jni 

它会在合适的地方把 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

你可能感兴趣的:(cgo 命令详解)