CGO

需要 import “C” 二进制的桥接接口 因为内存模型的不同, Go 的内存可伸缩而 C 的内存是稳定的,Go访问一段静态内存很容易,但是 C 访问 Go 的内存可能会出现问题,为了简化并高效处理此种向C语言传入Go语言内存的问题,cgo针对该场景定义了专门的规则:在CGO调用的C语言函数返回前,cgo保证传入的Go语言内存在此期间不会发生移动,C语言函数可以大胆地使用Go语言的内存

// #include <stdio.h>
import "C"

这句话不仅启用了 CGO 特性,还引入了 C 的标准IO库

C.CString("hello, world") // 将go的string转成C的const char *

调用自己自定义的函数需要 C.自定义函数名(参数...) 来进行调用 函数的实现可以直接写在注释里,或者先写在 .c 文件中再在 .go 文件中进行注释声明,然后就可以正常使用了。更规范的方式将声明写在 .h 文件中并在注释中 include 相应的 .h 文件

C 语言中常见类型在 Go 中的表示

// char
type C.char
type C.schar (signed char)
type C.uchar (unsigned char)

// short
type C.short
type C.ushort (unsigned short)

// int
type C.int
type C.uint (unsigned int)

// long
type C.long
type C.ulong (unsigned long)

// longlong
type C.longlong (long long)
type C.ulonglong (unsigned long long)

// float
type C.float

// double
type C.double

// struct XXX
type C.struct_XXX
// var a struct.XXX
// a.field 如果 field 的命名与关键字冲突,加上_前缀即可解决

// union XXX
// 与 struct 访问类似,在 Go 中使用对应大小的字节数组

传参的时候需要通过包装函数将 Go 类型转成对应 C 的类型才能传入 通过虚拟的 C 包导入的类型不受 Go 中首字母大写的约束 编译和链接参数可以通过注释来进行定义,例如

// #cgo [FLAGS] [VAL]

区分不同平台

/*
#cgo windows CFLAGS: -DCGO_OS_WINDOWS=1
#cgo darwin CFLAGS: -DCGO_OS_DARWIN=1
#cgo linux CFLAGS: -DCGO_OS_LINUX=1

#if defined(CGO_OS_WINDOWS)

#elif defined(CGO_OS_DARWIN)

#elif defined(CGO_OS_LINUX)
#else
#endif
*/

Build tag 条件编译

// +build tag1

只在 go build -tags=".." 中有这个 tag 的时候才会被编译

Go调用C

假如需要将 Go 语言的变量转换成 C 的类型,包装函数内部会自行进行拷贝和转换,如果是 string 类型,会将底层的字节切片转换成C语言对应的字节数组,其中会隐式地调用C语言的 malloc 进行动态内存分配,最终通过 free 函数进行释放,因此内存分配数据拷贝的是CGO中最主要的两个开销

解决指针转换:使用 unsafe.Pointer 包装去除原本的类型信息然后再转成其他类型的指针即可

解决数值到指针:先转成 uintptr 然后再使用 unsafe.Pointer 包装然后再转成其它

函数调用:因为C不支持多返回值,因此错误处理的解决方案是默认将第二个返回值对应 C 中的全局错误变量 errno,对应的是 syscall.Errno 错误类型

void 返回值的函数的错误返回值可以使用上述方式取得,void 返回值在 Go 中会使用一个 [0]byte 空数组来表示

内部机制

cgo 会为每个包含了 CGO 代码的Go文件分别创建一个 .go 文件和一个 .c 文件两个中间文件,会为整个包创建一个类型相关的 .go 中间文件,一套 .c 和 .h 文件对应 Go 导出到C的类型和函数

最终 go 运行时会通过调用 runtime.cgocall 来调用 C 语言编写的函数,并在其调用结束时获取返回值

内存模型

如果 C 中需要通过指针访问 Go 中的一段内存,那么传入的内存可能会是不安全的,因为 Go 的内存是动态的

例如 C.CString() 会将 Go 中的字符串对应的内存数据拷贝到新创建的C语言的内存空间中

因此为了避免这种效率低下的问题,cgo保证了在调用c函数返回前传入的 Go 内存在这期间不会发生移动,前提是必须直接传入而不是保存在临时变量中间接传入

导出非 main 包函数

默认 C 函数必须是 main 包导出的才行,如果是单独的子包需要导出函数需要自行编写导出 C 函数对应的头文件 在实现函数上添加注释 //export 函数名,接着生成 .a 静态库

$ go build -buildmode=c-archive -o main.a

编译C程序的时候记得包含静态库的目录,在程序中只需进行重新声明就可以使用