对,没有看错,在C/C++项目中使用Go代码!

试想一下,已经有了一个熟悉且稳定的某Go库,而在C/C++项目中正好需要它的功能,且这部分并非性能敏感的。显然的,此Go库用在这个C/C++项目,比再从头用C/C++造个轮子,成本、风险均更小更可控。这样的思路和态度对把996工作方式降级为965,具有现实可操作性。

我用Go实现的高性能多组Raft库Dragonboat就自带C++支持,可直接用于C++项目。比如共识和一致性的需求是来自诸如系统配置数据之类的非核心部分,或者说系统里有一致性要求的吞吐确定不高,比如Redis那样一秒才10万次左右的吞吐,那完全可以让这样的Go库在C++项目里直接使用,从而把精力真正用到C++做好其核心的部分。

这里就用Dragonboat做例子,详解具体步骤、性能预期以及一路走来的各坑。Dragonboat项目自带在C++项目中使用的例程,欢迎试用后点Star支持:

lni/dragonboat​github.com

指针的限制

C++内存与Go内存相比较,两者在申请、使用、回收机制上均有显著差别。在C++项目中使用Go代码,使得这两种类型的内存在一个应用程序里同时出现,且均由使用者来操作,那么保证它们的正确性就显然严重违背了本面向偷懒的编程方法中偷懒这一核心目的。官方的cgo文档在Passing Pointers一节倒是对具体的详细的要求有完整定义和介绍:

https://golang.org/cmd/cgo/#hdr-Passing_pointers​golang.org

简单来说,Golang出于GC的考虑,在你向C/C++代码传递Go指针的时候,不允许该指针指向的内存区域那含有别的Go指针。既然是面向偷懒的编程,肯定不能学文档里的这种死规矩做,何况因为程序需要,有时候必须得传递这样的指针所指向的Go对象怎么办?

Dragonboat中的做法是不传递任何Go指针,所有的Go对象,全部转换为interface{}以后保存在一个map[uint64]interface{}中,对每个新建的Go对象赋予一个全局唯一的uint64,使之成为该Go对象的handler,通过C++/Go程序之间传递这个uint64值,来达到传递这个Go对象的效果。也就是说,实际上的Go指针从不跨越Go/C++的边界。

恭喜!只要不钻牛角尖,不死命地去想所谓的“性能”、“代码是否优雅”问题,愿意接受上述方案,就已成功绕开所有在C/C++中使用Go的语言层面的坑了。

导出接口

为了让C/C++代码使用,我们需要把Go所实现的功能导出,并且导出的这些调用接口,应该考虑了上一节介绍的内存指针方面的特殊设计。具体要做的是给已有的Go库做一个wrapper,包装出这样一套供C/C++使用的接口。

举个例子,Dragonboat中,NodeHost这个facade interface结构体向应用提供所有Dragonboat库所支持的功能API,每个应用进程通常创建、持有并使用一个这样的NodeHost实例。那在wrapper中,我们会有如下这个NewNodeHost函数用以创建NodeHost实例:

其中"//export NewNodeHost"表示NewNodeHost函数需要被导出,addManagedObject将所创建的Go管理下的NodeHost对象加入上一节提到的map[uint64]interface{}容器中,并返回一个uint64值给调用者。因为这是一个已导出的函数,它的调用者就是C/C++代码。

需要使用这个NodeHost的时候,只要提供上面NewNodeHost返回的uint64值即可,比如当我们需要停止这个NodeHost工作时,我们提供了如下导出的函数:

从中可见,跨越Go/C++边界传递的,都是内建类型的值,所有的真正的Go调用实际均发生在Go一侧。C++部分只是保存一堆用于标示各Go对象的uint64类型的oid值。当某对象不再需要的时候,在它们对应的C++对象的析构函数中调用RemoveManagedObject()便可。

对所有需要导出的函数如法炮制,1天左右就可以把数万行几十个接口函数的复杂Go模块封装好了。具体其它细节,比如如何来回传递别的内建类型的参数,可查看Dragonboat的代码:

https://github.com/lni/dragonboat/blob/master/binding/binding.go​github.com

C/C++中使用上述Go库

接着以C-shared模式,编译出一个可供C/C++程序使用的的shared library,请注意其中“-buildmode=c-shared”部分。

上述命令执行以后,会生成binding/libdragonboat.so这个shared library和libdragonboat.h这个头文件。

有了上述的binding/libdragonboat.h与binding/libdragonboat.so就可以直接在C/C++项目中使用Go所现实的库了,和使用一个C实现的.so形式的shared library并无太多本质区别。Go的runtime的启动、关闭,协程管道等等内建类型的用,GC那一大堆麻烦事,都会对C/C++全透明。

面向偷懒的编程至此就基本达成了。

完整的示例可以参考Dragonboat所附带的C++ Hello World例程:

lni/dragonboat​github.com

性能

虽说是面向偷懒的编程方法,但其实稍加必要优化以后,性能依旧是不错的。以Dragonboat为例,使用C++20尚未标准化的一个协程库,16字节的负载下,在三台22核2.8GHz的志强服务器上,一秒百万级的写操作很轻松。对标github上纯C++的Raft库,这个性能在当前依旧是很不俗的。

当然,具体的绝对性能有多高并不是这里追求的。把项目非核心部分转到Go的实现上,更方便集中资源用C++处理系统中真正核心的部分,这才是本面向偷懒的编程方法真正的目的所在。