本文继续介绍如何在 Golang 中调用 C++ 函数。
起因 前面文章介绍的方式,在运行时需要指定动态库位置,或将动态库放置系统目录,对笔者而言,还是略有麻烦,本文将使用dl
系列函数,在运行时加载动态库,这样就去掉了路径的依赖。
实现 为减少篇幅,仅摘录必要的源码。
封装 在动态库版本源码基础上,额外添加封装动态库头文件 c_callso.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #ifdef __cplusplus extern "C" { #endif int cso_init(char* soname); int cso_uninit(); // 结构体指针,传入传出 int CSetPointA(Point* point, Point* point1); // 调用内部的类 //int FooCall(void); #ifdef __cplusplus } #endif
对应实现文件主要代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <dlfcn.h> void* g_sohandle = NULL; int cso_init(char* soname) { g_sohandle = dlopen(soname, RTLD_LAZY); if (g_sohandle == NULL) return -1; return 0; } int cso_uninit() { if (g_sohandle != NULL) dlclose(g_sohandle); return 0; } int CSetPointA(Point* point, Point* point1) { typedef int (*ptr)(Point*, Point*); printf("in c file call so\n"); ptr fptr = (ptr)dlsym(g_sohandle, "FooSetPointA"); return (*fptr)(point, point1); }
其中,CSetPointA 函数就是对接 FooSetPointA 函数的,仅做简单的封装。
调用 Golang 测试完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 package main /* #cgo LDFLAGS: -ldl #include <stdlib.h> #include "c_callso.h" #include "c_callso.c" */ import "C" import ( "fmt" "unsafe" ) var ( csoname = "./libfoo.so1" //csoname = "./aXi3n0fr1.rd" ) func so_test() { fmt.Println("go c++ so test") soname := C.CString(csoname) ret := C.cso_init(soname) if ret != 0 { fmt.Println("cso_init failed ", ret) return } defer C.free(unsafe.Pointer(soname)) defer C.cso_uninit() // C形式 结构体 var myPoint, myPoint1 C.Point myPoint.x = 100; myPoint.y = 200; myPoint.pinname = C.CString("Hello ") // 指针形式 defer C.free(unsafe.Pointer(myPoint.pinname)) // 固定长度数组,麻烦点 arr := [16]C.char{} mystr := "Hell " for i := 0; i < len(mystr) && i < 15; i++ { arr[i] = C.char(mystr[i]) } myPoint.inname = arr // 数组形式 fmt.Println("Golang | org struct ", myPoint, "single: ", myPoint.x, myPoint.y, myPoint.pinname) // 结构体指针 传入传出 ret = C.CSetPointA(&myPoint, &myPoint1) // 注:C++中使用字符串数组形式,转成string var carr []byte //carr = C.GoBytes(myPoint1.name, 100) for i := range myPoint1.name { if myPoint1.name[i] != 0 { carr = append(carr, byte(myPoint1.name[i])) } } gostr := string(carr) // 转成go的string fmt.Println("Golang | c++ call ret: ", ret, myPoint1.x, gostr, myPoint1.name) // 注:直接用指针形式转换,此处的指针值,与在C中申请的值,是一致的 // 注:如果指针没有分配内存,返回string为空,用unsafe.Pointer返回<nil> gostr = C.GoString(myPoint1.pname) defer C.free(unsafe.Pointer(myPoint1.pname)) fmt.Println("Golang | out pointer:", gostr, unsafe.Pointer(myPoint1.pname)) } func main() { so_test() }
与前面文章示例不同的地方,主要是调用了 C.cso_init 初始化动态库,最终调用 cso_uninit 释放。
结果分析 运行时,只需要保持动态库的位置和名称与 Golang 中指定的一致即可,无须设置 LD_LIBRARY_PATH 环境变量。
1 2 3 4 5 6 7 8 go c++ so test Golang | org struct {100 200 [72 101 108 108 32 0 0 0 0 0 0 0 0 0 0 0] 0xe2fc10 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] <nil>} single: 100 200 0xe2fc10 in c file call so C++ | got buf: Hell C++ | pname: Hello C++ | ptr: 0xe2fc30 Golang | c++ call ret: 0 101 name in c++ [110 97 109 101 32 105 110 32 99 43 43 0 0 0 0 0] Golang | out pointer: Hell | name in c++ malloc 0xe2fc30
实践总结 动态库初始化函数cso_init
等保留,动态库对外提供的业务接口,尽量少,这样减少 golang 和 C++ 之间的代码接口数量。
总结 本文的方法,却增加了源码级别的复杂度,不一定都符合要求,因此仅作参考。 Linux 的动态库,其名称一般为 libXXX.so
,但经测试,任意名称也是可以的。
李迟 2021.5.2