CGO 基本用法与构建过程
潘忠显 / 2021-06-25
Go 语言使用 CGO 的工具来调用 C 语言函数,也可以将 Go 语言函数导出为 C 动态库接口给其它语言使用。
本文结合代码(cgo-stage-by-stage),介绍一些 demo 的构建,帮助理解 CGO 的基本用法及其构建过程。
本文的所有示例使用 Go 1.6 构建,安装或升级过程可以参考安装 Go 1.6。
一、基本用法
Stage 1 中主要介绍使用 CGO 的基本方式,每个例子都可以在对应的目录下(stage1-*),使用 go build -o main .
指令进行构建。也可以使用根目录下的工具脚本进行一步构建 sh tools.sh build
。
对应每个 Case,主要会展示:
- 只有
import "C"
- Go 中使用 C 标准库
- 使用 Go 注释中定义的 C 函数
- 使用
extern
的 C 函数 - Go 中使用的 C 头文件和源文件
- Go 中使用的 C++ 头文件和源文件
- 在 Go 中使用 Go 中定义的 C 函数
- Go 函数的导出(export)
- C 代码中调用导出的 Go 函数
这 9 个 Case 主要展示了 import "C"
的作用、Go 中引用 C 代码、C 种引用 Go 代码,接下来做详细的说明。
1. import "C"
的作用
Go 中使用 CGO 是通过一条 import "C"
指令来指明。
“C” 是一个伪包(“pseudo-package”),import C
之前的注释是 C 代码,可以包含:
- 函数、变量的声明和定义 —— 可以通过
C.
来调用 - 静态变量 —— 不能在 Go 代码中使用
#cgo
指示的 CFLAGS, CPPFLAGS, CXXFLAGS, FFLAGS, LDFLAGS 等 flag
2. Go 中引用 C 代码
可以做的:
- Go 中关键字的 C 的结构字段名称可以通过以下划线作为前缀来访问
- 使用
C.char
、C.short
、C.longlong
等方式使用标准 C 中的类型 - 使用
struct_
、union_
或enum_
前缀访问结构体等类型,如C.struct_stat
- 任何 C 类型 T 的大小都可以作为 C.sizeof_T 使用,如 C.sizeof_struct_stat
- 可以在 Go 文件中使用特殊名称
_GoString_
的参数类型声明 AC 函数 - 调用任何 C 函数都会两个返回值,其中一个是 C 函数的返回,另外一个是作为错误的 C errno 变量。如果返回是void的C函数,可以使用
_
跳过。
n, err = C.sqrt(-1)
_, err := C.voidFunc()
var n, err = C.sqrt(1)
不能做的:
- Go 在一般情况下不支持 C union 类型,C union 表示为具有相同长度的 Go 字节数组。
- Go 结构体不能嵌入具有 C 类型的字段
- 不支持调用 C 函数指针,但是可以声明保存 C 函数指针的 Go 变量,并在 Go 和 C 之间来回传递。
- 在 C 中,作为固定大小数组编写的函数参数实际上需要指向数组第一个元素的指针。C 编译器知道这种调用约定并相应地调整调用,但 Go 不能。在 Go 中,必须显式地将指针传递给第一个元素:Cf(&C.x[0])。
- 不支持调用可变参数 C 函数
特殊情况:
- Cgo 将 C 类型转换为等效的、未导出的 Go 类型,进而导致相同的 C 类型在不同的 Go 包中实际上是不同的类型。因此 Go 包不应在其导出的 API 中暴露 C 类型。
- C.malloc 不直接调用 C 库
malloc
而是调用一个 Go 辅助函数,该函数封装了 C 库malloc
,保证永远不会返回 nil。类似 Go 本身内存不足的情况,如果 C 的 malloc 指示内存不足,则辅助函数会使程序 Crash。
3. C 中引用 Go 函数
Go 中定义的函数,通过会通过 CGO 导出(export) 成 C 代码,所有定义都能在 header 文件 _cgo_export.h
中找到。在 C 代码中 include "_cgo_export.h"
,使用 extern
函数定义进行使用。
如 Go 中定义函数(//export MyFunction
该注释是必须的):
//export MyFunction
func MyFunction(arg1, arg2 int, arg3 string) int64 {...}
在 C 中使用:
extern GoInt64 MyFunction(int arg1, int arg2, GoString arg3);
上边的 Go 代码中的 int
类型和 string
类型分别被映射到 C 中的 int
和 GoString
类型,但并不是所有的 Go 中的类型都能进行映射到 C 类型:
-
不支持 Go 的结构类型,可以改用 C 的结构类型
-
不支持 Go 的数组类型,可以使用 C 的指针
-
_GoString_
既能用于字符串从 Go 传递到 C 并返回Go,C 代码中无法进行创建
二、使用 CGO 构建时发生了什么
无论是 Go 调用 C 代码,还是导出 Go 函数供 C 调用,都能通过一条 go build
指令,进行构建成功。这条指令屏蔽了中间的过程,包括编译、汇编、链接。就像调用一条 gcc main.c
,就能直接得到二进制文件一样。本章将两种情况下的构建细节进行拆解,看看详细的构建过程。
本章内容参考自 Yugui Sonoda 的两篇文章,Yugui 还提供了两个示例和 Makefile,为了更好的理解构建过程,我将 make 的过程改写为 build.sh 的脚本。
本章代码对应的示例代码和构建脚本在 Stage 2-1 和 Stage 2-2 中。
1. Go 调用导入的 C 函数
Stage 2-1 中包括了两个源文件:
- import_example.go,导入 C 函数并调用的源文件
- main.go,main函数,没有
import "C"
依赖关系:main.go -printSqrt()
-> import_example.go -sqrt()
-> libm.a
1.1 构建过程
a. 生成代码
env CGO_LDFLAGS=-lm go tool cgo -objdir $tmpobjdir -importpath ./ import_example.go
利用 cgo
命令在内部调用 C 的预处理器处理 import "C"
及其中的 C 代码片段,并在 $tmpobjdir
目录下构建出以下文件:
-
_cgo_export.c
/_cgo_export.h
-
_cgo_gotypes.go
,整个包下只有一个,包含用于调用 C 函数的辅助代码 -
import_example.cgo1.go
,将原始.go
文件中 C 相关函数替换成_cgo_gotypes.go
文件中定义的函数 -
import_example.cgo2.c
,原始.go
文件中import "C"
相关的代码片段及其封装 -
_cgo_flags
存放 CGO_LDFLAGS 等环境变量 -
_cgo_.o
、_cgo_main.c
b. 编译 C 代码
将自动生成的 .c
文件进行编译,得到对应文件名的 .o
文件
gcc -c _cgo_export.c _cgo_main.c import_example.cgo2.c
c. C代码链接
将产生的 .o
文件进行链接,产生 _cgo_.o
文件,因为例子中有使用到 libm 库中的 sqrt
函数,因此在链接的时候需要携带 -lm
的选项,将 libm.a 静态的链接进去。
gcc -o _cgo_.o _cgo_export.o _cgo_main.o import_example.cgo2.o -lm
d. 生成导入声明
将前面产生的 _cgo_.o
作为 cgo
的输入,产生 _cgo_import.go
文件,用于后续传递指令给 Go 链接器。
go tool cgo -objdir $tmpobjdir -dynpackage main \
-dynimport $tmpobjdir/_cgo_.o -dynout $tmpobjdir/_cgo_import.go
e. 编译 Go 代码
将之前产生的包含 C 函数调用相关的 _cgo_gotypes.go
、已替换 C 调用的 import_example.cgo1.go
以及 用于指示链接的 _cgo_import.go
以及主函数文件 main.go
文件进行编译,并生成 example1.a
的静态库。
go tool compile -o example1.a -p ./ -pack \
$tmpobjdir/_cgo_gotypes.go $tmpobjdir/import_example.cgo1.go $tmpobjdir/_cgo_import.go main.go
f. 重新链接 C 代码
将之前编译的 C 对象文件重新链接成另外一个 _all.o
文件。与 _cgo_.o
的不同的,由于指定了 -Wl,-r
,_all.o
并没有链接 _cgo_main.o
以及所依赖的库 (-r
的意思是"Produce a relocatable object as output. This is also known as partial linking")。
gcc -nostdlib -o _all.o _cgo_export.o import_example.cgo2.o -Wl,-r
g. 打包 C 对象符号
将 _all.o
添加到 example1.a
中:
go tool pack r example1.a $tmpobjdir/_all.o
h. 链接 .a 中的对象
最后链接 ar 文件中的对象和依赖包中的对象,产生 ELF 文件 example1
:
go tool link -o example1 example1.a
1.2 派生文件过程与关系图
这一过程展示了 go build
的整个过程中对 C 遍历器工具链的使用:
- 产生桥接代码
- 使用 C 编译器和连接器解析 C 代码中所有的依赖,并产生
_cgo_.o
文件 - 解析
_cgo_.o
文件并提取以供_cgo_import.go
文件使用 - 将 Go 和 C 各自生成的对象文件合并成同一个文件
- 链接成可执行文件,而可执行文件依赖于一些 C 动态库
2. Go 导出函数供 C 调用
除了 Go 等调用 C 函数外,Go 还能将 Go 中定义的函数导出,以供 C 语言调用。
Stage 2-2 会介绍 C 调用导出 Go 函数的构建过程,其中包括了源文件:
export_example.go
,导出goVersion()
函数main.go
,调用 C 函数print_go_version()
use_exported.c
,调用 Go 函数goVersion()
函数实现print_go_version()
use_exported.h
,声明print_go_version()
函数
既依赖关系: main.go -print_go_version()
-> used_exported.c -goVersion()
-> export_example.go。
其实 Case 2 中不仅包含了 C 调用 Go 函数的过程,还包含了 Go 调用 C 函数的过程。
2.1 构建过程
a. 生成桥接代码
go tool cgo -objdir $tmpobjdir export_example.go main.go
这条指令产生的文件中包括一个 _cgo_export.h
,该文件由三个主要部分组成:
import "C"
之前的 C 代码,包括//#include <stdio.h>
等- 基本 Go 类型的 C 表示
//export
声明的 Go 函数对应的函数原型
b. 编译 C 代码
gcc -c _cgo_export.c _cgo_main.c export_example.cgo2.c main.cgo2.c ../use_exported.c -I.. -I.
c. 链接 C 代码
gcc -o _cgo_.o _cgo_export.o _cgo_main.o export_example.cgo2.o main.cgo2.o use_exported.o
d. 生成导出声明
go tool cgo -objdir $tmpobjdir -dynpackage main \
-dynimport $tmpobjdir/_cgo_.o -dynout $tmpobjdir/_cgo_export.go
e. 编译 Go 代码
go tool compile -o example2.a -p ./ -pack \
$tmpobjdir/_cgo_gotypes.go $tmpobjdir/export_example.cgo1.go \
$tmpobjdir/_cgo_export.go $tmpobjdir/main.cgo1.go
f. 重新链接 C 代码
gcc -nostdlib -o _all.o _cgo_export.o export_example.cgo2.o main.cgo2.o use_exported.o -Wl,-r
g. 打包 C 对象符号
go tool pack r example2.a $tmpobjdir/_all.o
h. 链接 .a 中的对象
go tool link -o example2 example2.a
2.2 派生文件过程与关系图
构建步骤、文件关系图和导入 C 代码的情形基本上相同,有几点不同之处是:
go tool cgo
由于源文件由一个变成了两个(因为两个go中都由import "C"
),文件数量有所增加- 增加了由
use_exported.c
创建出的use_exported.o
,而其中 #include 了文件_cgo_export.h
是在go tool cgo
时生成的
3. 胶水代码
在 Go 导出函数供 C 调用的示例中,go tool cgo
产生的 _cgo_export.c
,通过这个文件,export_example.go 文件中可以调用函数 func goVersion() string
,但在 C 文件中调用的 goVersion()
函数原型和 func goVersion() string
并不相同。
_cgo_export.c
文件内容如下:
/* Created by cgo - DO NOT EDIT. */
#include "_cgo_export.h"
extern void crosscall2(void (*fn)(void *, int), void *, int);
extern void _cgo_wait_runtime_init_done();
extern void _cgoexp_5e3e4b09c83e_goVersion(void *, int);
GoString goVersion()
{
_cgo_wait_runtime_init_done();
struct {
GoString r0;
} __attribute__((__packed__, __gcc_struct__)) a;
crosscall2(_cgoexp_5e3e4b09c83e_goVersion, &a, 16);
return a.r0;
}
其中,crosscall2()
函数实际是使用 runtime.cgo.crosscall2()
,该函数是用汇编代码实现的,实现的功能相当于调用了 _cgoexp_5e3e4b09c83e_goVersion()
,调用时传入结构体 a
。
_cgo_wait_runtime_init_done
函数如字面含义所示,是保持阻塞直到 runtime 包被初始化。而这里被调用的函数 _cgoexp_5e3e4b09c83e_goVersion()
是在 _cgo_gotypes.go
文件中被定义的:
/* ... */
//go:linkname _cgo_runtime_cgocallback runtime.cgocallback
func _cgo_runtime_cgocallback(unsafe.Pointer, unsafe.Pointer, uintptr)
/* ... */
//go:cgo_export_dynamic goVersion
//go:linkname _cgoexp_5e3e4b09c83e_goVersion _cgoexp_5e3e4b09c83e_goVersion
//go:cgo_export_static _cgoexp_5e3e4b09c83e_goVersion
//go:nosplit
//go:norace
func _cgoexp_5e3e4b09c83e_goVersion(a unsafe.Pointer, n int32) {
fn := _cgoexpwrap_5e3e4b09c83e_goVersion
_cgo_runtime_cgocallback(**(**unsafe.Pointer)(unsafe.Pointer(&fn)), a, uintptr(n));
}
func _cgoexpwrap_5e3e4b09c83e_goVersion() (r0 string) {
defer func() {
_cgoCheckResult(r0)
}()
return goVersion()
}
最后会调用 _cgo_runtime_cgocallback()
函数(对应runtime.cgocallback()
)通过调整栈,来完成 C 语言到 Go 函数的回调工作。
通过以上的胶水代码,Go 中定义的 goVersion()
就可以被 C 函数 print_go_version()
调用了。
4. 函数调用关系图
之前提到过,Case 2 中不仅包含了 C 调用 Go 函数的过程,还包含了 Go 调用 C 函数的过程,因此分析胶水代码以及调用关系时,只需要分析 Case 2 就能较全面的了解。
这里刻意的将 main.go 和 export_example.go 两个 .go 文件放在最左侧,将 use_exported.c 放在最右侧,CGO 工具生成的中间代码放在中间:
三、其他
1. 安装 Go 1.6
wget https://storage.googleapis.com/golang/go1.6.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.6.linux-amd64.tar.gz
echo 'export PATH=/usr/local/go/bin:$PATH' >> ~/.bashrc
echo 'export GOROOT=/usr/local/go' >> ~/.bashrc
source ~/.bashrc