Golang实践录:调用C++函数的优化

趁着五一放假,趁着有时间,把欠的一些技术集中研究研究,写写文章,好给自己一个交待。
本文继续介绍如何在 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