CGO是go语言提供的一个c和go集成的特性,允许我们在go代码中直接调用c语言代码,也可以export go的函数给c语言调用。
关于CGO的入门案例我们可以Google搜索到很多,但是都是Hello Word案例。
由于我们核心代码使用Go语言开发,但是想通过编译为C静态库给其它能调用C语言的高级语言调用,例如Object-C、Swift等。这样当我们想把核心功能代码提供给编写MacOS应用、Windows应用、iOS或Android应用时使用,核心功能就不需要用多种语言重复实现,也不必使用C这么底层的语言去编写。
分享案例:我们通过go实现tcp数据包分析功能,go语言提供分析接口并export为C函数,该函数支持传递多个回调函数,go方法异步分析TCP数据包抓包文件,go每分析出一个TCP数据包就调用相应的回调函数通知给调用方处理。
以下是案例代码:
package main
/*
#include <stdlib.h>
typedef void (*AddStream)(char *streamId);
typedef struct {
AddStream addStream;
} AnalysisCallbacks;
*/
import "C"
import (
"fmt"
"unsafe"
)
//export AnalysisPcapFile
func AnalysisPcapFile(filePath *C.char, cbs *C.AnalysisCallbacks) C.int {
goFilePath := C.GoString(filePath)
fmt.Println(goFilePath)
cStreamId := C.CString("127.0.0.1:454545->127.0.0.1:8080")
defer C.free(unsafe.Pointer(cStreamId))
addStreamFun := C.AddStream(cbs.addStrem)
addStreamFun(cStreamId)
return 1
}
func main() {
}
在该案例中,我们在c中声明了AddStream函数,以及AnalysisCallbacks结构体,用结构体来承载c函数传递给go语言。我们真实案例中有很多回调函数,这里为了简单说明,把多余的函数去掉了。我们可以使用同AddStream函数的声明方式,声明更多的回调函数,并可以加到AnalysisCallbacks中。
我们export了AnalysisPcapFile这个go函数,关于该函数的参数声明,因为这个函数是给c调用的,所以参数类型和返回值类型都用C的类型。
如果现在我们执行go build -buildmode=c-archive
,这个案例其实是编译不通过的,因为这两行代码:
addStreamFun := C.AddStream(cbs.addStream)
addStreamFun(cStreamId)
编译会报错:
invalid operation: cannot call non-function addStreamFun (variable of type _Ctype_AddStream)
原因是:typeof定义的c函数是不能转成go函数的,c定义的AddStream对应是go的结构体而非函数,所以不能调用,编译就失败了。
想要调用c传给进来的回调函数,还是得要通过c去调用,我们想到的方法是写个冗余的c函数去调。
将案例改为如下:
package main
/*
#include <stdlib.h>
typedef void (*AddStream)(char *streamId);
typedef struct {
AddStream addStream;
} AnalysisCallbacks;
void gocallAddStream(AnalysisCallbacks *cbs, char *streamId){
cbs->addStream(streamId);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
//export AnalysisPcapFile
func AnalysisPcapFile(filePath *C.char, cbs *C.AnalysisCallbacks) C.int {
goFilePath := C.GoString(filePath)
fmt.Println(goFilePath)
cStreamId := C.CString("127.0.0.1:454545->127.0.0.1:8080")
defer C.free(unsafe.Pointer(cStreamId))
C.gocallAddStream(cbs, cStreamId)
return 1
}
func main() {
}
我们在c中声明了gocallAddStream
方法,用来回调cbs.addStream。如果我们有多个回调函数,就需要写多个gocallAddStream这样的函数了。
写个简单的c代码来测试一下:
#include <stdio.h>
#include "./export_c.h"
void add_stream(char *streamId) {
printf("创建流:%s\n", streamId);
}
int main(void) {
char *filepath = "text.txt\0";
AnalysisCallbacks *cbs = malloc(sizeof(AnalysisCallbacks));
cbs->addStream = add_stream;
const int result = AnalysisPcapFile(filepath, cbs);
printf("%d", result);
free(cbs);
}
#include "./export_c.h"
是导入我们go build -buildmode=c-archive
编译go程序后生成的头文件。
该案例用来测试调用我们export 的AnalysisPcapFile方法。
使用gcc编译运行测试代码,gcc命令后面的export_c.a是我们编译go程序生成的c静态链接库:
wujiuye@wujiuyedeMacBook-Pro export_c % gcc -o main main.c export_c.a
duplicate symbol '_gocallAddStream' in:
/var/folders/t5/6dn6b58j52d9jfcqxn_b0y_h0000gn/T/main-1f244b.o
export_c.a(000000.o)
duplicate symbol '_gocallAddStream' in:
/var/folders/t5/6dn6b58j52d9jfcqxn_b0y_h0000gn/T/main-1f244b.o
export_c.a(000001.o)
ld: 2 duplicate symbols for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
可以看到,编译报错了。
忘记了,好像笔者在用CLion开发工具开发并用CMake编译运行测试程序的时候,可以看到下面这个错误提示,说是要加inline这个关键字。但是直接用gcc命令编译运行看不到。
.....
Definition of function 'void gocallAddStream(AnalysisCallbacks *cbs, char *streamId)' in a header file should have an 'inline' specifier
所以最终的代码是这样的:
package main
/*
#include <stdlib.h>
typedef void (*AddStream)(char *streamId);
typedef struct {
AddStream addStream;
} AnalysisCallbacks;
inline void gocallAddStream(AnalysisCallbacks *cbs, char *streamId){
cbs->addStream(streamId);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
//export AnalysisPcapFile
func AnalysisPcapFile(filePath *C.char, cbs *C.AnalysisCallbacks) C.int {
goFilePath := C.GoString(filePath)
fmt.Println(goFilePath)
cStreamId := C.CString("127.0.0.1:454545->127.0.0.1:8080")
defer C.free(unsafe.Pointer(cStreamId))
C.gocallAddStream(cbs, cStreamId)
return 1
}
func main() {
}
编译静态链接库、编译c demo、运行c demo:
go build -buildmode=c-archive
gcc -o main main.c export_c.a
./main
案例输出:
其它坑
如果我们go代码依赖了其它sdk,这些sdk又依赖c静态链接库,那么我们使用gcc编译或CMake编译,都需要指定链接这些依赖的静态链接库。
举例,我们依赖的一个go组件,它依赖了libpcap这个静态链接库,所以我们使用gcc编译时,需要指定这个库,不然会编译错误。
例如,未指定链接libpcap时:
wujiuye@wujiuyedeMacBook-Pro export_c % gcc -o main main.c export_c.a
Undefined symbols for architecture arm64:
"_pcap_activate", referenced from:
__cgo_19f9821c2670_Cfunc_pcap_activate in export_c.a(000016.o)
(maybe you meant: __cgo_19f9821c2670_Cfunc_pcap_activate)
"_pcap_can_set_rfmon", referenced from:
.......
指定后:
wujiuye@wujiuyedeMacBook-Pro export_c % gcc -o main main.c export_c.a -lpcap
wujiuye@wujiuyedeMacBook-Pro export_c %