go编译为c静态链接库给c语言调用,c语言传递函数给go语言回调

原创 吴就业 138 1 2024-06-04

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/5555f4ff39b945a795dbd211b71edc98

作者:吴就业
链接:https://wujiuye.com/article/5555f4ff39b945a795dbd211b71edc98
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

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

案例输出:

截屏2024-06-04 10.49.41

其它坑

如果我们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 % 
#前端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

SwiftUI macOS开发,Button和TextField控件设置背景色不生效的问题

SwiftUI macOS开发,Button和TextField控件设置背景色不生效的问题。

SwiftUI Chart如何修改字体颜色

使用Charts绘制的图表,默认文字都是灰色的,当我们app的主题颜色是固定深色的情况下,图表的文字会看不清,怎么修改呢?

Mac APP开发,Swift调用C静态链接库的函数

开发Mac APP,Swift如何调用C函数,以及如何传递回调函数,是一个比较复杂的Swift调用C的场景。