概述
一、CGO快速入门
(一)启用CGO特性
在golang代码中加入import “C”语句就可以启动CGO特性。这样在进行go build命令时,就会在编译和连接阶段启动 gcc 编译器。
// go.1.15
// test1.go
package main
import "C" // import "C" 更像是一个关键字,CGO工具在预处理时会删掉这一行
func main() {
}
使用 -x 选项可以查看go程序编译过程中执行的所有指令。可以看到golang编译器已经为test1.go创建了CGO编译选项:
[root@VM-centos ~/cgo_test/golink2]# go build -x test1.go
WORK=/tmp/go-build330287398
mkdir -p $WORK/b001/
cd /root/cgo_test/golink2
CGO_LDFLAGS='"-g" "-O2"'
# CGO编译选项
/usr/lib/golang/pkg/tool/linux_amd64/cgo -objdir $WORK/b001/ -importpath command-line-arguments -- -I $WORK/b001/ -g -O2 ./test1.go
cd $WORK
gcc -fno-caret-diagnostics -c -x c - -o /dev/null || true
gcc -Qunused-arguments -c -x c - -o /dev/null || true
gcc -fdebug-prefix-map=a=b -c -x c - -o /dev/null || true
gcc -gno-record-gcc-switches -c -x c - -o /dev/null || true
.......
(二)Hello Cgo
通过import“C”语句启用CGO特性后,CGO会将上一行代码所处注释块的内容视为C代码块,被称为序文(preamble)。
// test2.go
package main
//#include <stdio.h> // 序文中可以链接标准C程序库
import "C"
func main() {
C.puts(C.CString("Hello, Cgon"))
}
在序文中可以使用C.func的方式调用C代码块中的函数,包括库文件中的函数。对于C代码块的变量,类型也可以使用相同方法进行调用。
test2.go通过CGO提供的C.CString函数将Go语言字符串转化为C语言字符串,最后再通过C.puts 调用<stdio.h>中的puts函数向标准输出打印字符串。
(三)cgo工具
当你在包中引用 import “C”,go build 就会做很多额外的工作来构建你的代码,构建就不仅仅是向go tool compile 传递一堆.go文件了,而是要先进行以下步骤:
-
cgo工具就会被调用,在C转换Go、Go转换C之间生成各种文件。
-
系统的C编译器会被调用来处理包中所有的C文件。
-
所有独立的编译单元会被组合到一个.o文件。
-
生成的.o文件会在系统的连接器中对它的引用进行一次检查修复。
cgo是一个Go语言自带的特殊工具,可以使用命令go tool cgo来运行。它可以生成能够调用C语言代码的Go语言源文件,也就是说所有启用了CGO特性的Go代码,都会首先经过cgo的“预处理”。
对test2.go,cgo工具会在同目录生成以下文件:
_obj--|
|--_cgo.o // C代码编译出的链接库
|--_cgo_main.c // C代码部分的main函数
|--_cgo_flags // C代码的编译和链接选项
|--_cgo_export.c //
|--_cgo_export.h // 导出到C语言的Go类型
|--_cgo_gotypes.go // 导出到Go语言的C类型
|--test1.cgo1.go // 经过“预处理”的Go代码
|--test1.cgo2.c // 经过“预处理”的C代码
二、CGO的N种用法
CGO作为Go语言和C语言之间的桥梁,其使用场景可以分为两种:Go调用C程序和C调用Go程序。
(一)Go调用自定义C程序
// test3.go
package main
/*
#cgo LDFLAGS: -L/usr/local/lib
#include <stdio.h>
#include <stdlib.h>
#define REPEAT_LIMIT 3 // CGO会保留C代码块中的宏定义
typedef struct{ // 自定义结构体
int repeat_time;
char* str;
}blob;
int SayHello(blob* pblob) { // 自定义函数
for ( ;pblob->repeat_time < REPEAT_LIMIT; pblob->repeat_time++){
puts(pblob->str);
}
return 0;
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
// 在GO程序中创建的C对象,存储在Go的内存空间
cblob := C.blob{}
cblob.repeat_time = 0
// C.CString 会在C的内存空间申请一个C语言字符串对象,再将Go字符串拷贝到C字符串
cblob.str = C.CString("Hello, Worldn")
// &cblob 取C语言对象cblob的地址
ret := C.SayHello(&cblob)
fmt.Println("ret", ret)
fmt.Println("repeat_time", cblob.repeat_time)
// C.CString 申请的C空间内存不会自动释放,需要显示调用C中的free释放
C.free(unsafe.Pointer(cblob.str))
}
输出:
Hello, World
Hello, World
Hello, World
ret 0
repeat_time 3
CGO会保留序文中的宏定义,但是并不会保留注释,也不支持#program,C代码块中的#program语句极可能产生未知错误。
CGO中使用#cgo关键字可以设置编译阶段和链接阶段的相关参数,可以使用${SRCDIR}来表示Go包当前目录的绝对路径。
使用C.结构名或C.struct_结构名可以在Go代码段中定义C对象,并通过成员名访问结构体成员。
test3.go中使用C.CString将Go字符串对象转化为C字符串对象,并将其传入C程序空间进行使用,由于C的内存空间不受Go的GC管理,因此需要显示的调用C语言的free来进行回收。详情见第三章。
(二)Go调用C/C++模块
-
简单Go调C
直接将完整的C代码放在Go源文件中,这种编排方式便于开发人员快速在C代码和Go代码间进行切换。
// demo/test4.go
package main
/*
#include <stdio.h>
int SayHello() {
puts("Hello World");
return 0;
}
*/
import "C"
import (
"fmt"
)
func main() {
ret := C.SayHello()
fmt.Println(ret)
}
但是当CGO中使用了大量的C语言代码时,将所有的代码放在同一个go文件中即不利于代码复用,也会影响代码的可读性。此时可以将C代码抽象成模块,再将C模块集成到Go程序中。
-
Go调用C模块
将C代码进行抽象,放到相同目录下的C语言源文件hello.c中:
// demo/hello.c
#include <stdio.h>
int SayHello() {
puts("Hello World");
return 0;
}
在Go代码中,声明SayHello()函数,再引用hello.c源文件,就可以调起外部C源文件中的函数了。同理也可以将C源码编译打包为静态库或动态库进行使用。
// demo/test5.go
package main
/*
#include "hello.c"
int SayHello(); # 这一行代码可以没有的
*/
import "C"
import (
"fmt"
)
func main() {
ret := C.SayHello()
fmt.Println(ret)
}
test5.go中只对 SayHello 函数进行了声明,然后再通过链接C程序库的方式加载函数的实现。那么同样的,也可以通过链接C++程序库的方式,来实现Go调用C++程序。
-
Go调用C++模块
基于test4。可以抽象出一个hello模块,将模块的接口函数在hello.h头文件进行定义:
// demo/hello.h
int SayHello();
再使用C++来重新实现这个C函数:
// demo/hello.cpp
#include <iostream>
extern "C" {
#include "hello.h"
}
int SayHello() {
std::cout<<"Hello World";
return 0;
}
最后再在Go代码中,引用hello.h头文件,就可以调用C++实现的SayHello函数:
// demo/test6.go
package main
/*
#include "hello.h"
*/
import "C"
import (
"fmt"
)
func main() {
ret := C.SayHello()
fmt.Println(ret)
}
上面的代码运行会报错:
/tmp/go-build2969327321/b001/_x002.o: In function `_cgo_19b63b86f14f_Cfunc_SayHello':
/tmp/go-build/cgo-gcc-prolog:52: undefined reference to `SayHello'
collect2: error: ld returned 1 exit status
CGO提供的这种面向C语言接口的编程方式,使得开发者可以使用是任何编程语言来对接口进行实现,只要最终满足C语言接口即可。
(三)C调用Go模块
C调用Go相对于Go调C来说要复杂多,可以分为两种情况: 一是原生Go进程调用C,C中再反调Go程序。另一种是原生C进程直接调用Go。
-
Go实现的C函数
如前述,开发者可以用任何编程语言来编写程序,只要支持CGO的C接口标准,就可以被CGO接入。那么同样可以用Go实现C函数接口。
在test6.go中,已经定义了C接口模块hello.h:
// demo/hello.h
void SayHello(char* s);
可以创建一个hello.go文件,来用Go语言实现SayHello函数:
// demo/hello.go
package main
//#include <hello.h>
import "C"
import "fmt"
//export SayHello
func SayHello(str *C.char) {
fmt.Println(C.GoString(str))
}
CGO的//export SayHello指令将Go语言实现的SayHello函数导出为C语言函数。这样再Go中调用C.SayHello时,最终调用的是hello.go中定义的Go函数SayHello:
// demo/test7.go
// go run ../demo
package main
//#include "hello.h"
import "C"
func main() {
C.SayHello(C.CString("Hello World"))
}
Go程序先调用C的SayHello接口,由于SayHello接口链接在Go的实现上,又调到Go。
看起来调起方和实现方都是Go,但实际执行顺序是Go的main函数,调到CGO生成的C桥接函数,最后C桥接函数再调到Go的SayHello。这部分会在第四章进行分析。
-
原生C调用Go
C调用到Go这种情况比较复杂,Go一般是便以为c-shared/c-archive的库给C调用。
// demo/hello.go
package main
import "C"
//export hello
func hello(value string)*C.char { // 如果函数有返回值,则要将返回值转换为C语言对应的类型
return C.CString("hello" + value)
}
func main(){
// 此处一定要有main函数,有main函数才能让cgo编译器去把包编译成C的库
}
如果Go函数有多个返回值,会生成一个C结构体进行返回,结构体定义参考生成的.h文件
生成c-shared文件命令:
go build -buildmode=c-shared -o hello.so hello.go
在C代码中,只需要引用go build生成的.h文件,并在编译时链接对应的.so程序库,即可从C调用Go程序
// demo/test8.c
#include <stdio.h>
#include <string.h>
#include "hello.h" //此处为上一步生成的.h文件
int main(){
char c1[] = "did";
GoString s1 = {c1,strlen(c1)}; //构建Go语言的字符串类型
char *c = hello(s1);
printf("r:%s",c);
return 0;
}
编译命令:
gcc -o c_go main.c hello.so
C函数调入进Go,必须按照Go的规则执行,当主程序是C调用Go时,也同样有一个Go的runtime与C程序并行执行。这个runtime的初始化在对应的c-shared的库加载时就会执行。因此,在进程启动时就有两个线程执行,一个C的,一 (多)个是Go的。
三、类型转换
想要更好的使用CGO必须了解Go和C之间类型转换的规则。
(一)数值类型
在Go语言中访问C语言的符号时,一般都通过虚拟的“C”包进行。比如C.int,C.char就对应与C语言中的int和char,对应于Go语言中的int和byte。
C语言和Go语言的数值类型对应如下:
Go语言的int和uint在32位和64位系统下分别是4个字节和8个字节大小。它在C语言中的导出类型GoInt和GoUint在不同位数系统下内存大小也不同。
如下是64位系统中,Go数值类型在C语言的导出列表:
// _cgo_export.h
typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef __SIZE_TYPE__ GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;
需要注意的是在C语言符号名前加上_Ctype_,便是其在Go中的导出名,因此在启用CGO特性后,Go语言中禁止出现以_Ctype_开头的自定义符号名,类似的还有_Cfunc_等。
可以在序文中引入_obj/_cgo_export.h来显式使用cgo在C中的导出类型:
// test9.go
package main
/*
#include "_obj/_cgo_export.h" // _cgo_export.h由cgo工具动态生成
GoInt32 Add(GoInt32 param1, GoInt32 param2) { // GoInt32即为cgo在C语言的导出类型
return param1 + param2;
}
*/
import "C"
import "fmt"
func main() {
// _Ctype_ // _Ctype_ 会在cgo预处理阶段触发异常,
fmt.Println(C.Add(1, 2))
}
如下是64位系统中,C数值类型在Go语言的导出列表:
// _cgo_gotypes.go
type _Ctype_char int8
type _Ctype_double float64
type _Ctype_float float32
type _Ctype_int int32
type _Ctype_long int64
type _Ctype_longlong int64
type _Ctype_schar int8
type _Ctype_short int16
type _Ctype_size_t = _Ctype_ulong
type _Ctype_uchar uint8
type _Ctype_uint uint32
type _Ctype_ulong uint64
type _Ctype_ulonglong uint64
type _Ctype_void [0]byte
为了提高C语言的可移植性,更好的做法是通过C语言的C99标准引入的<stdint.h>头文件,不但每个数值类型都提供了明确内存大小,而且和Go语言的类型命名更加一致。
(二)切片
Go中切片的使用方法类似C中的数组,但是内存结构并不一样。C中的数组实际上指的是一段连续的内存,而Go的切片在存储数据的连续内存基础上,还有一个头结构体,其内存结构如下:
因此Go的切片不能直接传递给C使用,而是需要取切片的内部缓冲区的首地址(即首个元素的地址)来传递给C使用。使用这种方式把Go的内存空间暴露给C使用,可以大大减少Go和C之间参数传递时内存拷贝的消耗。
// test10.go
package main
/*
int SayHello(char* buff, int len) {
char hello[] = "Hello Cgo!";
int movnum = len < sizeof(hello) ? len:sizeof(hello);
memcpy(buff, hello, movnum); // go字符串没有'