在 Go 语言中,使用 cgo 无非就两种主要场景:一种是 Go 生成动态库so文件(或静态文件),供其他语言如c来使用;另一种是使用 C 语言封装动态库,供 Go 调用。说白了就是谁负责生产so文件,谁来使用的问题。
首先来梳理下 Go 生成 c-shared 类动态库供其他语言使用的具体流程和细节。整体步骤大概就分为三个步骤:
- 开发接口函数;
- 构建so文件;
- 接入使用.
再开始之前需要做些前期准备,这里我们设计一个极简的 Go 工具函数库项目:crypto,项目目录如下:
crypto/
├── go.mod
└── main.go
项目功能简单,所以接口函数直接定义在 main.go 文件中, 文件名没有特殊要求,但是注意:对于一个要 build so 库的项目,Go 还是有些基础要求:
- 整个导出项目必须要有一个
main的package包; - 既然需要有
main包,那么就必须要有一个main入口函数,即便是个空函数; - 如果要激活
cgo,还需要有import "C"语句,否则不会生成对应的.h头文件。
所以,最终的 main.go 最低要求代码结构如下:
// 以下三项必不可少
package main
import "C"
func main() {}
1. 开发接口函数
接口函数即是希望被调用的函数。如果想在其他语言中使用 Go 定义的函数,需要把函数使用 //export 注释标记。
package main
import "C"
func main() {}
//export encode_data
func encode_data(data *C.char) *C.char {
// 逻辑代码
}
这里我们定义了一个 encode_data 接口函数,又有另一些细节需要注意:
注意点一:
//export 其后的名称和下一行的函数名必须要保持一致,但函数命名是驼峰还是snake不做要求,也不要求以大写字母开头。如上函数最终生成的头文件函数如下:
extern char* encode_data(char* data);
注意点二:
此外,要注意函数形参和返回参数数据类型问题,Go的数据类型和C的不一样,尤其是指针、内存管理都不同,也不能直接使用。所以接口 Go 函数不能直接返回Go指针类型的数据,比如 string
// 错误做法
// string 是Go指针类型,C不能使用的。
func encode_data(data *C.char) string {}
如上代码直接返回一个 Go 字符串 string 类型,Go 字符串内部是包含Go指针的。如果直接供 C 调用会抛出:cgo result has Go pointer 错误。
正确的做法是使用cgo提供的包装函数如 C.CString() 来转一下,要么直接构造C对应的指针类型结构数据。
func encode_data(data *C.char) *C.char {
return C.CString("c string") // 使用C类型指针数据
}
注意点三:
但这还没完!!虽然使用 C.CString 函数可以轻松返回 C类型的字符串(或者叫字符数组指针),但是这里会触发另一的潜在的问题。使用C.CString 构造的数据是在堆内存中,而不是在静态区。所以官方明确说明,这些数据需要手动释放:
var cmsg *C.char = C.CString("hi")
defer C.free(unsafe.Pointer(cmsg))
但在这个场景中,我们还不能立即释放,因为我们需要返回给调用方,他们还需要使用!
func encode_data(data *C.char) *C.char {
var cmsg *C.char = C.CString("c string")
//defer C.free(unsafe.Pointer(cmsg)) <------- 不能释放,不然调用方拿不到数据了
return cmsg
}
所以只能由调用方来释放这个数据,所以你需要和调用方进行沟通,让他们手动释放这类似的数据,虽然理解这个字符串被放在了堆里有些费劲。
int main() {
char * encodeData = encode_data("test"); // 返回数据在heap中,不在静态区
// 记得要释放
free(encodeData);
}
2. 构建so文件
如果设计好函数接口,接下来的事情就很简单,直接使用 go build 工具即可搞定。
# 构建动态库文件
go build -o libcrypto.so -buildmode=c-shared main.go
# 构建静态库文件
go build -o libcrypto.a -buildmode=c-archive main.go
推荐输出文件命令以 lib* 作为前缀,这是 linux下约定俗成的标准, go build 会生成对应的头文件和库文件:
libcrypto.h
libcrypto.so #或 libcrypto.a
另外,如果你的导出函数功能设计还没准备好,想提前导出.h头文件,也是没问题的,可以直接使用:
go tool cgo -exportheader libcrypto.h main.go
go tool cgo 和 go build 关于cgo还有很多更详细操作参数,具体可查看 help 手册。
3. 接入使用
有了 .h 文件和 .so 库,接入使用并不复杂,不光是 C 语言,lua、c++ 其实都可以使用。只需要在项目中使用这个头文件定义的函数即可,并在编译连接和运行环节配置好这个 libcrypto.so 库的路径,或者直接把它注册到系统路径下。
#include <stdio.h>
#include "libs/libcrypto.h"
#include <stdlib.h>
int main() {
char * encoded = encode_data("hi cgo");
printf("code: %s \n", encoded);
// 注意内存释放
free(encoded);
return 0;
}
编译运行
# 编译
gcc -L./libs -lcrypto main.c -o app
# 运行
LD_LIBRARY_PATH=./libs/:${LD_LIBRARY_PATH} ./app
传输结果:
c string
这里演示项目是的 gcc,复杂项目推荐 cmake,友情提示:macOS 下的动态库变量是:DYLD_LIBRARY_PATH。
其外
在函数接口设计环节我们强调过,C.CString 语言的的字符串数据是在堆内存, 而不是在静态区,所以需要我们手动执行 free 函数进行手动内存回收,这里我们使用另一个牛逼的内存溢出检测工具来验证一下:valgrind。
工具的介绍和安装请自行
LD_LIBRARY_PATH=./libs valgrind --leak-check=full ./app
输出结果
==29641== HEAP SUMMARY:
==29641== in use at exit: 3,465 bytes in 7 blocks
==29641== total heap usage: 13 allocs, 6 frees, 3,641 bytes allocated
==29641==
==29641== 9 bytes in 1 blocks are definitely lost in loss record 1 of 6
==29641== at 0x4C29E03: malloc (vg_replace_malloc.c:309)
==29641== by 0x4ECE904: _cgo_50f941cac2e5_Cfunc__Cmalloc (_cgo_export.c:47)
==29641== by 0x4EC7C07: runtime.asmcgocall (/usr/local/go1.15/src/runtime/asm_amd64.s:656)
==29641== by 0x4EA0249: runtime.newextram (/usr/local/go1.15/src/runtime/proc.go:1607)
==29641== by 0xC0000005FF: ???
==29641== by 0x4E9F41F: ??? (in /home/liangqi1/crypto/libs/libcrypto.so)
==29641== by 0x1FFF00016F: ???
...
...
==29641== LEAK SUMMARY:
==29641== definitely lost: 9 bytes in 1 blocks
==29641== indirectly lost: 0 bytes in 0 blocks
==29641== possibly lost: 3,456 bytes in 6 blocks
==29641== still reachable: 0 bytes in 0 blocks
==29641== suppressed: 0 bytes in 0 blocks
如上部分只保留了关键代码,如上溢出总结里:definitely lost 表示绝对存在内存溢出,溢出了9字节的数据,恰好是demo函数返回的:c string 外加一个C语言结束符号\0,刚好9字节。
由于 valgrind 对于.so动态内部函数栈的追踪也无能为力,所以定位不到具体是代码哪一行,只能是??? 表示。不过可以通过打开或注释掉 free() 函数代码,对比排查。