10分钟学会Go调用 C++
潘忠显 / 2023-08-16
一、背景
以一个 C++ 的项目中的函数作为被调用目标:https://github.com/rohanmohapatra/hdbscan-cpp 。项目中 HDBSCAN-FourProminentClusterExample 目录下有个 main()
函数的 .cpp 文件,依赖该项目中的其他源文件,可以构建出一个可执行文件,读取仓库中数据文件,然后打印结果。
【目标】写个 main.go
,调用该 Example 中的 C++ main()
函数内容,其实能够调用 main 函数也意味着我们可以调用其他函数。
之前详细且带示例的 《CGO 基本用法与构建过程》的也欢迎查阅。
二、C++项目验证
调用 make
会在根目录下产生一个 main 二进制文件,可以直接运行,得到结果:
那么我们后边的 Go 构建出来的二进制文件,也是要达到这种效果的。
三、修改源文件并构建静态库
因为项目中 Makefile 会构建所有的 .cpp 文件,所以这里为了区分,我们的源文件使用 .cc 结尾。
这里的操作都在 Example 目录下进行。
添加 .h 文件
创建一个 lib.h 的文件,里边只添加一个函数的声明(因为一个可执行文件只能有一个 main 入口,所以这里我们将原来的 main 改名):
int cpp_main();
改动 .cpp 文件
- 将
main()
重命名为cpp_main()
- 增加
#include "lib.h"
- 使用
extern "C" {}
包一下#include "lib.h"
构建 .o 文件
这里只是构建出对象文件,我们为其命名为 lib.o:
gcc -std=c++11 -c lib.cc -o lib.o
打包其他依赖的 .o 文件
上边的 lib.o 中其实只有 lib.cc 中的 cpp_main 函数,如果要让 Go 能运行,需要将其所有依赖的对象文件打到一个静态库中,不然后边会报找不到符号的错误。
另外,之前 make 的时候,可能产生了 main 函数所在的文件对应的 .o 文件,这里要先删掉,避免被一块打进来:
rm FourProminentClusterExample.o # 那个main函数对应的.o
ar rcs libhdbscan.a `find .. -name "*.o"`
执行完上边的指令,就得到了一个可以用的静态库:
- 头文件: lib.h
- 库文件:libhdbscan.a
四、Go中调用C++函数
编写 Go 源代码
这里注意几点:
import "C"
引入 cgo- 在
import "C"
前边,要写上包含哪些头文件并注释 - 在
import "C"
前边,写上编译时候依赖哪些库 - 直接使用
C.cpp_main
即可调用 lib.h 中的 cpp_main 函数
package main
//#cgo LDFLAGS: -L./ -lhdbscan -lstdc++ -lm
//#include "lib.h"
import "C"
func main() {
C.cpp_main()
}
构建
为了区分项目自身构建出来的二进制文件,我们将 Go 构建出来的命名为 go_main,也放在根目录:
go build -o ../go_main main.go
运行
我们切换到根目录,直接运行 go_main,便可以得到跟“C++项目验证”中一样的结果:
五、原理说明
extern "C"
的作用
首先,C++ 和 C 在函数命名和调用约定上有所不同。C++ 支持函数重载和命名空间,因此编译器会对函数名进行修饰以区分不同的函数。而 C 语言没有函数重载和命名空间的概念,函数名是唯一的,符号命名就直接是函数名。
我们仍然以上边修改的 lib.cc 文件构建出来的 lib.o 为例,除了我们声明的 cpp_main
,中间有使用到一个 loadCsv,我们可以通过 nm
命令来看看这些符号:
- cpp_main 就是一个简单符号
- loadCsv后边还有Eib,用来说明该符号对应接收一个 int 和 一个 bool 型入参的函数
loadCsv 的真正定义:
int loadCsv(int numberOfValues, bool skipHeader=false);
而 CGO 就是以 C 符号命名去寻找函数的,所以 C++ 要让 Go 使用,必须封装一层 C 函数。
extern "C"
需要包哪些
Q:为什么只需要用 extern "C" {}
包 #include "lib.h"
而不用包上边的 .hpp
以及标准库?
A:因为给 Go 中用的函数只有 cpp_main
这一个符号,所以只需要将这一个函数,按照 C 符号链接命名规范处理即可。其他的符号,仍然是以 C++ 的符号命名去链接即可。