CGO 是什么?
CGO 是 GO 语言里面的一个特性,CGO 属于 GOLANG 的高级用法,主要是通过使用 GOLANG 调用 CLANG 实现的程序库
使用
我们可以使用
import "C"
来使用 CGO 这个特性
一个最简单的 CGO 使用
package main
//#include
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
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
#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
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)
}
上述代码的具体执行逻辑顺序是这样的:
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
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
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
获取指针,然后转成对应的数据类型的指针即可
参考资料:
欢迎点赞,关注,收藏
朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力
好了,本次就到这里
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是小魔童哪吒,欢迎点赞关注收藏,下次见~