Golang实践录:调用C++函数

趁着五一放假,趁着有时间,把欠的一些技术集中研究研究,写写文章,好给自己一个交待。
本文介绍如何在 Golang 中调用 C++ 函数。

起因

因工作需求,需要将一个工具由终端行的运行方式迁移到 web 上,核心代码由 c++ 动态库实现,另一部门的同事使用 Java 实现了一个版本,部门同事安排我做部署,由于服务器是离线的,且由专人管理,JDK 和 Tomcat 安装稍麻烦,个人操作自由度不够,——一是没有研究过 Java,二来部署麻烦。因此,决定使用 Golang 实现。预计展开的内容有:Golang 调用 C++ 动态库;Golang Web 服务及整合 html/css资源;(大)前端框架使用。

本文主要研究 C++ 动态库及函数的调用。

思路

Golang 只支持 C 语言的编译,对于 C++ 的编译,有2种方法:
1、不使用类,在 C++ 代码头文件添加extern "C" {,将函数声明为 C 格式。
2、如出现类的情况,再用另外的文件将其封装成 C 格式函数。

实现

C/C++代码

没有类的文件,但后缀名为cpp:

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
// bar.h文件:
#ifndef BAR_H
#define BAR_H

#ifdef __cplusplus
extern "C" {
#endif

int bar();

#ifdef __cplusplus
}
#endif

#endif

// bar.cpp文件:
#include <stdio.h>
#include "bar.h"


int bar()
{
printf("C | hell bar\n");

#ifdef MACRO_TEST
printf("C | macro...\n");
#endif
return 0;
}

有类的文件:

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
// foo.h for class

#ifndef FOO_H
#define FOO_H

class CFoo
{
public:
CFoo(int value): m_value(value){};
~CFoo(){};
void Bar();

private:
int m_value;
};

#endif

// foo.cpp

#include <stdio.h>
#include <iostream>
#include "foo.h"

void CFoo::Bar(void)
{
printf("C++ Class | %s(): num: %d\n", __func__, m_value);
//std::cout<<this->a<<std::endl;
}

封装代码:

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// foo.h
#ifndef OUT_H
#define OUT_H

#ifdef __cplusplus
extern "C" {
#endif

typedef struct Point{
int x;
int y;
char inname[16]; // 传入buff
char* pinname; // 传入指针
char name[16]; // 传出buff
char* pname; // 传出指针
}Point;

// 普通类型赋值
int FooSetValue(int a, unsigned int b, float c, char* str);

void PrintString(char* str);

// 结构体
int FooSetPointC(Point point);
// 结构体指针
int FooSetPoint(Point* point);
// 结构体指针,传入传出
int FooSetPointA(Point* point, Point* point1);

// 调用内部的类
int FooCall(int num);

#ifdef __cplusplus
}
#endif

#endif

// out.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "out.h"
#include "foo.h"

int FooSetValue(int a, unsigned int b, float c, char* str)
{
printf("C++ | base type: %d %d %.4f %s\n", a, b, c, str);
return 0;
}

void PrintString(char* str)
{
trueprintf("C++ | string = %s\n", str);
}

int FooSetPointC(Point point)
{
printf("C++ | the point in c for value: %d %d \n", point.x, point.y);
return 0;
}

int FooSetPoint(Point* point)
{
printf("C++ | the point in c: %d %d \n", point->x, point->y);

point->x = 250;
point->y = 500;
strcpy(point->name, "name in c++");
return 24;
}

int FooSetPointA(Point* point, Point* point1)
{
printf("C++ | got buf: %s\n", point->inname);

if (point->pinname != NULL) printf("C++ | pname: %s\n", point->pinname);

point1->x = point->x+1;
point1->y = point->y+1;
strcpy(point1->name, "name in c++");

point1->pname = new char[16];
sprintf(point1->pname, "%s | name in c++ malloc", point->inname);
//strcpy(point1->pname, "name in c++ malloc ");
printf("C++ | ptr: %p\n", point1->pname);
return 0;
}

int FooCall(int num)
{
CFoo * ret = new CFoo(num);
ret->Bar();
return 0;
}

使用 Makefile 将上面文件编译为 libfoo.so 动态库。

动态库调用

完整测试代码如下:

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
package main

/*
#cgo CFLAGS: -I.
#cgo LDFLAGS: -L. -lfoo

#include <stdlib.h>

#include "out.h"
*/
import "C"

import (
"fmt"
"unsafe"
)

func so_test() {
fmt.Println("go c++ so test")

// 简单函数调用
cstr := C.CString("call C func")
defer C.free(unsafe.Pointer(cstr))
var i C.int
i = 100
C.FooSetValue(i, C.uint(250), C.float(3.14159), cstr)
C.PrintString(cstr);

// 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)

// 结构体传值
C.FooSetPointC(myPoint)

// 结构体指针 传入传出
ret := C.FooSetPointA(&myPoint, &myPoint1)

// 注:C++中使用字符串数组形式,转成string
var carr []byte
//carr = C.GoBytes(myPoint1.name, 16)

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))

C.FooCall(250)
C.FooCall(C.int(250))
}

func main() {
so_test()
}

源码要点如下:
1、需设置编译参数 LDFLAGS,指定库位置和名称,本例中是当前目录的 libfoo.so。
2、需包含相应的头文件,stdlib.h 为 free 函数所在文件。
3、内嵌的 C 源码在包的前面,且import "C"后需空一行。
4、传递到 C 函数的内存,使用C.CString申请,C 申请的内存使用C.GoString获取,均需要手动释放。

结果分析

在运行前,需要设置动态库路径:

1
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD

否则运行时无法找到动态库:

1
./test: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
go c++ so test
C++ | base type: 100 250 3.1416 call C func
C++ | string = call C func
Golang | org struct {100 200 [72 101 108 108 32 0 0 0 0 0 0 0 0 0 0 0] 0x25b2a30 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] <nil>} single: 100 200 0x25b2a30
C++ | the point in c for value: 100 200
C++ | got buf: Hell
C++ | pname: Hello
C++ | ptr: 0x25b2a50
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 0x25b2a50
C++ Class | Bar(): num: 250
C++ Class | Bar(): num: 250

从上述结果中可看出,C 中申请的内存,其指针与在 Go 中获取的指针是一样的,即 0x25b2a50。结构体中的 nil 是因为字段 pname 未赋值。

源码编译

前面的动态库代码,不能全部内嵌到 Go 代码中,因此选取其中的 bar.h/cpp,测试代码如下:

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
package main

/*
#cgo CFLAGS: -I. -DMACRO_TEST

#include <stdlib.h>

#include "bar.h"
#include "bar.cpp"
*/
import "C"

import (
"fmt"

)

func cpp_test() {
fmt.Println("go c++ so test")
C.bar();
}

func main() {
cpp_test()
}

源码要点:
1、可用 CFLAGS 指定头文件,添加宏定义等。
2、将所有的 C 源码包含到代码中。(存疑:似乎应该是头文件,在编译过程中自动找对应的实现文件,这里包含进来,相当于所有源码都在 Go 代码中)

结果分析

运行结果如下:

1
2
3
go c++ so test
C | hell bar
C | macro...

使用此方法,如果修改 C 代码,还需更新包含 C 代码的 go 文件,否则不会被编译。

总结

上面对2种形式的调用进行了实践,在功能和使用上各有千秋,对于简单的 C 语言代码(包含C++形式的简单函数),直接使用内嵌的形式会更高效。
本文使用的动态库例子,在运行前还需要设置运行路径,当然可以将动态库放到系统目录的,但笔者认为不是正道,下面将去掉动态库路径的依赖。

Go 编译时,如果包含有类的文件,编译失败,出错信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# command-line-arguments
In file included from ./bar.cpp:2:0,
from ./main_one.go:17:
./foo.h:6:1: error: unknown type name 'class'
class CFoo
^
./foo.h:7:1: error: expected '=', ',', ';', 'asm' or '__attribute__' before '{' token
{
^
In file included from ./main_one.go:17:0:
./bar.cpp: In function 'FooCall1':
./bar.cpp:18:5: error: unknown type name 'CFoo'
CFoo * ret = new CFoo(num);
^
./bar.cpp:18:18: error: 'new' undeclared (first use in this function)
CFoo * ret = new CFoo(num);
^
./bar.cpp:18:18: note: each undeclared identifier is reported only once for each function it appears in
./bar.cpp:18:22: error: expected ',' or ';' before 'CFoo'
CFoo * ret = new CFoo(num);
^
./bar.cpp:19:8: error: request for member 'Bar' in something not a structure or union
ret->Bar();

李迟 2021.5.2