1.1 最简CGO程序
// hello.go
package main
import "C"
func main() {
println("hello cgo")
}
- 代码通过
import "C"
语句启用CGO特性,主函数只是通过Go内置的println函数输出字符串,其中并没有任何和CGO相关的代码。 - 虽然没有调用CGO的相关函数,但是
go build
命令会在编译和链接阶段启动gcc编译器,这已经是一个完整的CGO程序了。
1.2 基于C标准库函数输出字符串
// hello.go
package main
//#include <stdio.h>
import "C"
func main() {
C.puts(C.CString("Hello, World\n"))
}
- 通过
import "C"
语句启用CGO特性, - 同时包含C语言的
<stdio.h>
头文件。 - 然后通过CGO包的
C.CString
函数将Go语言字符串转为C语言字符串, - 最后调用CGO包的
C.puts
函数向标准输出窗口打印转换后的C字符串。 - 我们没有在程序退出前释放
C.CString
创建的C语言字符串; - 还有我们改用
puts
函数直接向标准输出打印,之前是采用fputs
向标准输出打印。 - 没有释放使用
C.CString
创建的C语言字符串会导致内存泄漏。
1.3 使用自己的C函数
// hello.go
package main
/*
#include <stdio.h>
static void SayHello(const char* s) {
puts(s);
}
*/
import "C"
func main() {
C.SayHello(C.CString("Hello, World\n"))
}
- 现在我们先自定义一个叫
SayHello
的C函数来实现打印,然后从Go语言环境中调用这个SayHello
函数 - 可以将
SayHello
函数放到当前目录下的一个C语言源文件中(后缀名必须是.c
)。因为是编写在独立的C文件中,为了允许外部引用,所以需要去掉函数的static
修饰符。 - 然后在CGO部分先声明
SayHello
函数,其它部分不变: - 如果是以静态库或动态库方式引用
SayHello
函数的话,需要将对应的C源文件移出当前目录(CGO构建程序会自动构建当前目录下的C源文件,从而导致C函数名冲突)
1.4 用Go重新实现C函数
CGO不仅仅用于Go语言中调用C语言函数,还可以用于导出Go语言函数给C语言函数调用。
// hello.go
package main
import "C"
import "fmt"
//export SayHello
func SayHello(s *C.char) {
fmt.Print(C.GoString(s))
}
- 我们通过CGO的
//export SayHello
指令将Go语言实现的函数SayHello
导出为C语言函数。 - 为了适配CGO导出的C语言函数,我们禁止了在函数的声明语句中的const修饰符。
- 需要注意的是,这里其实有两个版本的
SayHello
函数:一个Go语言环境的;另一个是C语言环境的。 - cgo生成的C语言版本SayHello函数最终会通过桥接代码调用Go语言版本的SayHello函数。
2.1 CGO基础
- 在macOS和Linux下是要安装GCC,在windows下是需要安装MinGW工具。
- 同时需要保证环境变量
CGO_ENABLED
被设置为1,这表示CGO是被启用的状态。
2.2import "C"
语句
- Go代码中出现了
import "C"
语句则表示使用了CGO特性,紧跟在这行语句前面的注释是一种特殊语法,里面包含的是正常的C语言代码。
package main
/*
#include <stdio.h>
void printint(int v) {
printf("printint: %d\n", v);
}
*/
import "C"
func main() {
v := 42
C.printint(C.int(v))
}
- 开头的注释中写了要调用的C函数和相关的头文件,头文件被include之后里面的所有的C语言元素都会被加入到”C”这个虚拟的包中。
- 需要注意的是,import “C"导入语句需要单独一行,不能与其他包一同import。
- 向C函数传递参数也很简单,就直接转化成对应C语言类型传递就可以。
- Go是强类型语言,所以cgo中传递的参数类型必须与声明的类型完全一致,而且传递前必须用”C”中的转化函数转换成对应的C类型,不能直接传入Go中类型的变量。
- 同时通过虚拟的C包导入的C语言符号并不需要是大写字母开头,它们不受Go语言的导出规则约束。
2.2 #cgo
语句
- 在
import "C"
语句前的注释中可以通过#cgo
语句设置编译阶段和链接阶段的相关参数。 - 编译阶段的参数主要用于定义相关宏和指定头文件检索路径。
- 链接阶段的参数主要是指定库文件检索路径和要链接的库文件。
// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include
// #cgo LDFLAGS: -L/usr/local/lib -lpng
// #include <png.h>
import "C"
-
CFLAGS部分,
-D
部分定义了宏PNG_DEBUG,值为1; -
-I
定义了头文件包含的检索目录。LDFLAGS部分 -
-L
指定了链接时库文件检索目录, -
-l
指定了链接时需要链接png库。 -
因为C/C++遗留的问题,C头文件检索目录可以是相对目录,但是库文件检索目录则需要绝对路径。
-
在库文件的检索目录中可以通过
${SRCDIR}
变量表示当前包目录的绝对路径:
// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo
#cgo
语句主要影响CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS几个编译器环境变量。- LDFLAGS用于设置链接时的参数,除此之外的几个变量用于改变编译阶段的构建参数(CFLAGS用于针对C语言代码设置编译参数)。
- 对于在cgo环境混合使用C和C++的用户来说,可能有三种不同的编译选项:
- CFLAGS对应C语言特有的编译选项、
- CXXFLAGS对应是C++特有的编译选项、
- CPPFLAGS则对应C和C++共有的编译选项。
- 但是在链接阶段,C和C++的链接选项是通用的,因此这个时候已经不再有C和C++语言的区别,它们的目标文件的类型是相同的。
2.3 build tag 条件编译
#cgo
指令针对不同平台定义的宏,只有在对应平台的宏被定义之后才会构建对应的代码。
比如下面的源文件只有在设置debug构建标志时才会被构建:
// +build debug
package main
var buildMode = "debug"
可以用以下命令构建:
go build -tags="debug"
go build -tags="windows debug"
我们可以通过-tags
命令行参数同时指定多个build标志,它们之间用空格分隔。
当有多个build tag时,我们将多个标志通过逻辑操作的规则来组合使用。
3.1 类型转换
- Go语言中访问C语言的符号时,一般是通过虚拟的“C”包访问,比如
C.int
对应C语言的int
类型。 - 有些C语言的类型是由多个关键字组成,但通过虚拟的“C”包访问C语言类型时名称部分不能有空格字符,比如
unsigned int
不能直接通过C.unsigned int
访问。 - 因此CGO为C语言的基础数值类型都提供了相应转换规则,比如
C.uint
对应C语言的unsigned int
。
应关系表2-1所示。
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 |
表 2-1 Go语言和C语言类型对比
在<stdint.h>
文件中,不但每个数值类型都提供了明确内存大小,而且和Go语言的类型命名更加一致。Go语言类型<stdint.h>
头文件类型对比如表2-2所示。
C语言类型 | CGO类型 | Go语言类型 |
---|---|---|
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 |
表 2-2 <stdint.h>
类型对比
3.2 结构体、联合、枚举类型
- C语言的结构体、联合、枚举类型不能作为匿名成员被嵌入到Go语言的结构体中。
- 在Go语言中,我们可以通过
C.struct_xxx
来访问C语言中定义的struct xxx
结构体类型。
/*
struct A {
int i;
float f;
};
*/
import "C"
import "fmt"
func main() {
var a C.struct_A
fmt.Println(a.i)
fmt.Println(a.f)
}
/*
struct A {
int type; // type 是 Go 语言的关键字
};
*/
import "C"
import "fmt"
func main() {
var a C.struct_A
fmt.Println(a._type) // _type 对应 type
}
- 如果结构体的成员名字中碰巧是Go语言的关键字,可以通过在成员名开头添加下划线来访问:
- 但是如果有2个成员:一个是以Go语言关键字命名,另一个刚好是以下划线和Go语言关键字命名,那么以Go语言关键字命名的成员将无法访问(被屏蔽):
/*
struct A {
int type; // type 是 Go 语言的关键字
float _type; // 将屏蔽CGO对 type 成员的访问
};
*/
import "C"
import "fmt"
func main() {
var a C.struct_A
fmt.Println(a._type) // _type 对应 _type
}
对应零长数组的成员,无法在Go语言中直接访问数组的元素,但其中零长的数组成员所在位置的偏移量依然可以通过unsafe.Offsetof(a.arr)
来访问。
/*
struct A {
int size: 10; // 位字段无法访问
float arr[]; // 零长的数组也无法访问
};
*/
import "C"
import "fmt"
func main() {
var a C.struct_A
fmt.Println(a.size) // 错误: 位字段无法访问
fmt.Println(a.arr) // 错误: 零长的数组也无法访问
}
对于联合类型,我们可以通过C.union_xxx
来访问C语言中定义的union xxx
类型。但是Go语言中并不支持C语言联合类型,它们会被转为对应大小的字节数组。
/*
#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
var b2 C.union_B2;
fmt.Printf("%T\n", b2) // [8]uint8
}
对于枚举类型,我们可以通过C.enum_xxx
来访问C语言中定义的enum xxx
结构体类型。
/*
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语言中,枚举类型底层对应int
类型,支持负数类型的值。我们可以通过C.ONE
、C.TWO
等直接访问定义的枚举值。
4.1 函数调用
4.1.1 C函数的返回值
/*
static int div(int a, int b) {
return a/b;
}
*/
import "C"
import "fmt"
func main() {
v := C.div(6, 3)
fmt.Println(v)
}
因为C语言不支持返回多个结果,因此<errno.h>
标准库提供了一个errno
宏用于返回错误状态。我们可以近似地将errno
看成一个线程安全的全局变量,可以用于记录最近一次错误的状态码。
改进后的div函数实现如下:
#include <errno.h>
int div(int a, int b) {
if(b == 0) {
errno = EINVAL;
return 0;
}
return a/b;
}
CGO也针对<errno.h>
标准库的errno
宏做的特殊支持:在CGO调用C函数时如果有两个返回值,那么第二个返回值将对应errno
错误状态。
/*
#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)
}
运行这个代码将会产生以下输出:
2 <nil>
0 invalid argument
我们可以近似地将div函数看作为以下类型的函数:
func C.div(a, b C.int) (C.int, [error])
5 内部机制
-
CGO特性主要是通过一个叫cgo的命令行工具来辅助输出Go和C之间的桥接代码。本节我们尝试从生成的代码分析Go语言和C语言函数直接相互调用的流程。
-
我们可以在构建一个cgo包时增加一个
-work
输出中间生成文件所在的目录并且在构建完成时保留中间文件。 -
包中有4个Go文件,其中nocgo开头的文件中没有
import "C"
指令,其它的2个文件则包含了cgo代码。 -
cgo命令会为每个包含了cgo代码的Go文件创建2个中间文件,比如 main.go 会分别创建 main.cgo1.go 和 main.cgo2.c 两个中间文件。
-
然后会为整个包创建一个
_cgo_gotypes.go
Go文件,其中包含Go语言部分辅助代码。 -
此外还会创建一个
_cgo_export.h
和_cgo_export.c
文件,对应Go语言导出到C语言的类型和函数。
6.C++ 类包装
- CGO是C语言和Go语言之间的桥梁,原则上无法直接支持C++的类。
- CGO不支持C++语法的根本原因是C++至今为止还没有一个二进制接口规范(ABI)。
- 一个C++类的构造函数在编译为目标文件时如何生成链接符号名称、方法在不同平台甚至是C++的不同版本之间都是不一样的。
- 但是C++是兼容C语言,所以我们可以通过增加一组C语言函数接口作为C++类和CGO之间的桥梁,这样就可以间接地实现C++和Go之间的互联。
- 当然,因为CGO只支持C语言中值类型的数据类型,所以我们是无法直接使用C++的引用参数等特性的。
6.1 C++ 类到 Go 语言对象
- 实现C++类到Go语言对象的包装需要经过以下几个步骤:首先是用纯C函数接口包装该C++类;
- 其次是通过CGO将纯C函数接口映射到Go函数;
- 最后是做一个Go包装对象,将C++类到方法用Go对象的方法实现。