CGO 是什么?
CGO 是 GO 语言里面的一个特性,CGO 属于 GOLANG 的高级用法,主要是通过使用 GOLANG 调用 CLANG 实现的程序库
使用
我们可以使用
import "C"
来使用 CGO 这个特性
一个最简单的 CGO 使用
package main //#include <stdio.h> import "C" func main(){ C.puts(C.CString("Hello, Cgo\n")) }
import "C"
的上方可以写需要导入的库 C 库,需要注释起来,CGO 会将此处的注释内容当做 C 的代码,被称为序文(preamble)
上述代码的功能解释
使用 CGO 包的
C.CString
函数将 Go 语言字符串转为 C 语言字符串最后调用 CGO 包的
C.puts
函数向标准输出窗口打印转换后的 C 字符串
使用 go build -x main.go
编译一下
加上 -x
可以打印出编译过程中执行的指令
# go build -x main.go WORK=/tmp/go-build594331603 mkdir -p $WORK/b001/ cat >$WORK/b001/importcfg.link << 'EOF' # internal packagefile command-line-arguments=/root/.cache/go-build/fb/fbb37eeb6735cb453f6d92e2e3f46f14d9dceb5baa1cdd10aae11d1d47d60e55-d packagefile runtime/cgo=/usr/local/go/pkg/linux_amd64/runtime/cgo.a packagefile syscall=/usr/local/go/pkg/linux_amd64/syscall.a packagefile runtime=/usr/local/go/pkg/linux_amd64/runtime.a packagefile errors=/usr/local/go/pkg/linux_amd64/errors.a packagefile internal/bytealg=/usr/local/go/pkg/linux_amd64/internal/bytealg.a packagefile internal/oserror=/usr/local/go/pkg/linux_amd64/internal/oserror.a packagefile internal/race=/usr/local/go/pkg/linux_amd64/internal/race.a packagefile internal/unsafeheader=/usr/local/go/pkg/linux_amd64/internal/unsafeheader.a packagefile sync=/usr/local/go/pkg/linux_amd64/sync.a packagefile internal/cpu=/usr/local/go/pkg/linux_amd64/internal/cpu.a packagefile runtime/internal/atomic=/usr/local/go/pkg/linux_amd64/runtime/internal/atomic.a packagefile runtime/internal/math=/usr/local/go/pkg/linux_amd64/runtime/internal/math.a packagefile runtime/internal/sys=/usr/local/go/pkg/linux_amd64/runtime/internal/sys.a packagefile internal/reflectlite=/usr/local/go/pkg/linux_amd64/internal/reflectlite.a packagefile sync/atomic=/usr/local/go/pkg/linux_amd64/sync/atomic.a EOF mkdir -p $WORK/b001/exe/ cd . /usr/local/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=Vv0to6CWqbWf5_KTN66F/K36AEO-x4qJ_LJbz5wgG/HVbBbLSaW0sTSwlN8TzN/Vv0to6CWqbWf5_KTN66F -extld=gcc /root/.cache/go-build/fb/fbb37eeb6735cb453f6d92e2e3f46f14d9dceb5baa1cdd10aae11d1d47d60e55-d /usr/local/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal mv $WORK/b001/exe/a.out main rm -r $WORK/b001/
尝试自己写一个 C 函数,让 GO 来调用他
Go语言环境中调用这个 SayHello
函数
package main /* #include <stdio.h> static void SayHello(const char* s) { puts(s); } */ import "C" func main(){ C.SayHello(C.CString("hello xiaomotong study cgo\n")) }
尝试自己写一个 C 文件,然后 GO 中进行导入和调用
xmtC.h
void SayHi(const char * str);
xmtC.c
(必须是同级目录下的 .c 文件,cgo 使用 go build 编译的时候,会默认在同级目录下找.c
文件进行编译,如果咱们是需要将 C 文件做成静态库 或者 动态库的方式,那么就不要将 C 的源码文件放到同级目录下了,避免重名)
#include <stdio.h> #include "xmtC.h" void SayHi(const char * str){ puts(str); }
main.go
package main //void SayHi(const char * str); import "C" func main(){ C.SayHi(C.CString("hello xiaomotong study cgo\n")) }
直接运行go build
进行编译,运行可执行程序即可
# go build # ls cgo main.go xmtC.c # ./cgo hello xiaomotong study cgo
通过面向C语言接口的编程技术,我们不仅仅解放了函数的实现者,同时也简化的函数的使用者。现在我们可以将 SayHi 当作一个标准库的函数使用(和puts函数的使用方式类似)
咱们也可以在 go 文件中写成这个样子
package main //#include <xmtC.h> import "C" func main(){ C.SayHi(C.CString("hello xiaomotong study cgo\n")) }
合并 C 和 GO 的代码
在Go1.10
中CGO新增加了一个_GoString_
预定义的C语言类型,用来表示Go语言字符串
// +build go1.10 package main //void SayHi(_GoString_ s); import "C" import ( "fmt" ) func main() { C.SayHi("hello xiaomotong study cgo\n") } //export SayHi func SayHi(s string) { fmt.Print(s) }
上述代码的具体执行逻辑顺序是这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ynH1W4H-1659626343404)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f70064ec5d9d4b1b9ec0ce5579f8d35f~tplv-k3u1fbpfcp-zoom-1.image)]
CGO 环境
使用 CGO 需要一定的环境环境支持
- linux 下 需要有
gcc/g++
的编译环境 - windows 下需要有 MinGW 工具
- 需要把 GO 的环境变量
CGO_ENABLED
置位 1
上述的例子中,我们有几个需要注意的点:
import "C"
语句不能和其他的 import 语句放在一起,需要单独一行放置- 上述我们在GO里面传递的值,例如
C.CString("hello xiaomotong study cgo\n")
是调用了 C 的虚拟包,将字符串转换成 C 的字符串传入进去 - Go是强类型语言
所以 cgo 中传递的参数类型必须与声明的类型完全一致,而且传递前必须用 ”C” 中的转化函数转换成对应的C类型,不能直接传入Go中类型的变量
通过虚拟的 C 包导入的C语言符号并不需要是大写字母开头,它们不受Go语言的导出规则约束
#cgo 用法
我们可以使用 #cgo
语句设置编译阶段和链接阶段的相关参数
- 编译阶段的参数
主要用于定义相关宏和指定头文件检索路径
- 链接阶段的参数
主要是指定库文件检索路径和要链接的库文件
例如我们可以这样
// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include // #cgo LDFLAGS: -L/usr/local/lib -lpng // #include <png.h> import "C"
CFLAGS
- -DPNG_DEBUG
定义宏 PNG_DEBUG ,设置为 1
- -I
定义头文件的检索目录是 ./include
LDFLAGS
- -L
指定链接时库文件检索目录 ,可以通过写 ${SRCDIR}
来表示当前包的绝对路径
- -l
指定链接时需要的库,此处是 png 库
条件编译 build tag
就是在我们 go build
的时候,添加一些条件参数,当然这个条件参数在对应的文件中是需要有的,
例如上述我们使用 Go1.10
的时候,就在文件中添加了 // +build go1.10
我们可以这样用:
go build -tags="debug" go build -tags="debug test" go build -tags="linux,386"
go build 的时候加上 -tags
参数,若有多个我们可以一起写,用空格
间隔,表示 或
,用逗号
间隔表示与
GO 和 C 数据类型相互转换
cgo 官方提供了如下的数据类型转换:
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 |
需要注意 3 个点:
- CGO 中,C 语言的
int
和long
类型都是对应4个字节的内存大小,size_t
类型可以当作Go语言uint
无符号整数类型对待 - CGO 中,C 语言的
int
固定为4字节的大小 , GO 语言的int
和uint
却在32位和64位系统下分别对应 4 个字节和 8 个字节大小 - 例如数据类型中间有空格,
unsigned int
不能直接通过C.unsigned int
访问,可以使用typedef
关键字提供一个规则的类型命名,这样更利于在CGO中访问
字符串和切片类型
CGO生成的 _cgo_export.h
头文件中有 GO 里面字符串,切片,通道,字典,接口等数据类型对应的表示方式,但是我们一般使用有价值的就是字符串和切片了
因为 CGO 没有提供其他数据类型的辅助函数
typedef struct { const char *p; GoInt n; } GoString;
咱们导出函数的时候可以这样写:
使用 _GoString_
预定义类型,这样写可以降低在 cgo 代码中可能对 _cgo_export.h
头文件产生的循环依赖的风险
_GoString_
是 Go1.10 针对 Go 专门加的字符
extern void helloString(_GoString_ p0);
我们可以使用官方提供的函数计算字符串的长度 和 获取字符串的地址:
size_t _GoStringLen(_GoString_ s); const char *_GoStringPtr(_GoString_ s);
struct ,union,enum
GO 语言中访问 C 语言的 struct ,union,enum,可以查看下表的对应关系
C语言 | GO 语言 |
struct xx | C.struct_xx |
union xx | C.union_xx |
enum xx | C.enum_xx |
**对于结构体 struct **
结构体的内存布局按照 C 语言的通用对齐规则
在32位Go语言环境 C 语言结构体也按照32位对齐规则,在64位Go语言环境按照64位的对齐规则
对于指定了特殊对齐规则的结构体,无法在 CGO 中访问
GO 中可以这样访问 C 的结构体
package main /* struct struct_TEST { int i; float f; }; */ import "C" import "fmt" func main() { var a C.struct_TEST a.i = 1 a.f = 2 fmt.Println(a.i) fmt.Println(a.f) }
需要注意如下 2 个大点:
- 结构体成员的名字和 GO 中关键字的名字一样咋处理
例如上述结构体成员名字是这样的
struct struct_TEST { int type; float f; };
那么我们访问 type 的时候,可以这样访问a._type
即可
若结构体是这样的呢?
struct struct_TEST { int type; float _type; };
我们访问的时候仍然是这样访问, a._type
,不过实际访问到的是 float _type;
,通过 GO 就没有办法访问到 int type;
GO 中也无法访问 C 中的 零长数组 和 位字段,例如
struct struct_TEST { int size: 10; // 位字段无法访问 float arr[]; // 零长的数组无法访问 };
- 在 C 语言中,无法直接访问 Go 语言定义的结构体类型
对于枚举 enum
枚举类型底层对应int
类型,支持负数类型的值 , 我们可以直接使用 C.xx 来进行访问
例如枚举类型为:
enum TEST { ONE, TWO, };
使用这个类型我们可以用 c C.enum_TEST
给这个变量复制的时候,我们可以这样做:c = C.ONE
对于联合体 union
Go 语言中并不支持 C 语言联合类型,它们会被转为对应大小的字节数组
例如
union B1 { int i; float f; };
union B1
会被转换成为 4 个字节大小的 字节数组 [4]uint8
GO 中操作联合体变量有 3 种方式:
- 在C语言中定义辅助函数
- Go语言的
encoding/binary
手工解码成员**(需要注意大端小端问题)** - 使用
unsafe
包强制转型为对应类型
举个例子
package main /* #include <stdint.h> union TEST { int i; float f; }; */ import "C" import ( "fmt" "unsafe" ) func main() { var b C.union_TEST *(*C.int)(unsafe.Pointer(&b)) = 1 fmt.Println("b.i:", *(*C.int)(unsafe.Pointer(&b))) fmt.Println("b.f:", *(*C.float)(unsafe.Pointer(&b))) }
我们读取和写入联合体变量的时候,使用 unsafe
包性能是最好的,通过unsafe
获取指针,然后转成对应的数据类型的指针即可
参考资料:
欢迎点赞,关注,收藏
朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rnf9NE3P-1659626343417)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0201d40a68894f2f825d2c66ea76ada1~tplv-k3u1fbpfcp-zoom-1.image)]
好了,本次就到这里
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是小魔童哪吒,欢迎点赞关注收藏,下次见~