Jason Pan

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 二进制文件,可以直接运行,得到结果:

cpp-project-result

那么我们后边的 Go 构建出来的二进制文件,也是要达到这种效果的。

三、修改源文件并构建静态库

因为项目中 Makefile 会构建所有的 .cpp 文件,所以这里为了区分,我们的源文件使用 .cc 结尾。

这里的操作都在 Example 目录下进行。

添加 .h 文件

创建一个 lib.h 的文件,里边只添加一个函数的声明(因为一个可执行文件只能有一个 main 入口,所以这里我们将原来的 main 改名):

int cpp_main();

改动 .cpp 文件

  1. main() 重命名为 cpp_main()
  2. 增加 #include "lib.h"
  3. 使用 extern "C" {} 包一下 #include "lib.h"

code-diff-of-lib-for-go

构建 .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"`

执行完上边的指令,就得到了一个可以用的静态库:

四、Go中调用C++函数

编写 Go 源代码

这里注意几点:

  1. import "C" 引入 cgo
  2. import "C" 前边,要写上包含哪些头文件并注释
  3. import "C" 前边,写上编译时候依赖哪些库
  4. 直接使用 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++项目验证”中一样的结果:

go-call-cpp-result

五、原理说明

extern "C" 的作用

首先,C++ 和 C 在函数命名和调用约定上有所不同。C++ 支持函数重载和命名空间,因此编译器会对函数名进行修饰以区分不同的函数。而 C 语言没有函数重载和命名空间的概念,函数名是唯一的,符号命名就直接是函数名。

我们仍然以上边修改的 lib.cc 文件构建出来的 lib.o 为例,除了我们声明的 cpp_main,中间有使用到一个 loadCsv,我们可以通过 nm 命令来看看这些符号:

c-and-cpp-symbol-in-lib

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++ 的符号命名去链接即可。