为 C++程序写 rustbinding
AutoCxx 与 CWrapper+Bindgen
在代码的世界中,还是 c和 cpp站绝大多数,现在提一个比较常见的需求:提供一个 c++的程序,最终需要再 rust中调用 c++程序提供的接口。
一般来说有两个方法
- 直接使用 cxx autocxx为 rust代码生成一份 unsafe的代码,然后直接调用
- 第二种方法比较路径稍长,先针对 c++代码的 header 写一份 c风格的头文件cwrapper,然后针对 c的头文件写一份 c头文件的实现。接下来编译自己的cwrapper,生成一份新的动态库。接下来使用 bindgen 根据 cwrapper生成一份 unsafe rust。最后在 rust代码中调用。
总体来说 cxx 或者 autocxx 可能性能会更好一些,但是 autocxx并不能搞定一切。第二种方法胜在稳定,毕竟 c的 abi比较稳定。本文将采用后一种方法。
速成材料
技术基础是:会 rust,不会 c++或者 c。所以需要速成,了解 c和 c++。如果彻底不会 c++,写 bingen 无从谈起。
下面是一些材料
- https://www.youtube.com/watch?v=KJgsSFOSQv0&t=76s
- https://www.youtube.com/watch?v=ZzaPdXTrSb8
- https://www.runoob.com/cplusplus/cpp-variable-scope.html
第一个为 feeCodeCamp的 c课程,三小时速成。
因为 c++比较难,可以学习第二个教程一小时速成,接下来看菜鸟教程的文档。注意不要纠结细节,否则无法速成。学成 C++已经数年以后 🥳,毕竟最终目的并非写 c++。
或者也可以看 freecodecamp的 c++教程,大概四小时看完。
CPP部分
库文件准备
https://github.com/TigerInYourDream/cppExample
c++部分代码已经上传 github。常见的c++项目大概使用 cmake编译,因为速成材料中没有讲 cmake,所以直接用 g++或者 clang编译。
1 |
|
1 | //下面的是头文件 |
c++源文件和头文件在此。一个非常简单的代码,为了在后续使用 c风格的 wrapper。特意使用了 namespace class这些 c没有的特性。大致解释下代码 分别有构造函数,析构函数(类似与 rust的 Drop)和一个成员函数(或者这个称为方法)。
clang++ -c -fPIC MyClass.cpp -o MyClass.o
clang++ -dynamiclib -o libMyClass.dylib MyClass.o
-dynamiclib 选项表示生成动态库。
-o libMyClass.dylib 指定输出文件的名称为 libMyClass.dylib。
c++的二进制产物生成分两步
- 编译
- 链接
编译生成 .o的编译产物,然后链接生成动态库。因为我的编程环境为 mac,所以我使用 clang++ 且选择生成 dylib。如果是 linux考虑使用 g++和生成 so(这一类更常见)
C包装
首先根据暴露的库文件包装一个 c风格的头
1 |
|
c的头文件如上所示,关键是使用不透明指针
不透明指针(Opaque Pointer)是一种特殊类型的指针,它隐藏了所指向的具体数据类型的详细信息。不透明指针只提供了指针的操作,而不暴露指针所指向的数据的类型和结构。
typedef struct MyClassOpaque MyClassOpaque; 是一种特别的写法 实质相当于 对 struct MyClassQpaque 的别名,以后用MyClassOpaque 不用带 Struct关键字。
typedef MyClassOpaque* MyClassHandle;
直接定义不透明指针
有了这个C风格的头还不够,还需要一份实现代码
1 |
|
实际实现的代码如上。使用 exrern “C” 包装 相当于 rust的 extern “C” 和 nomangle。注意对应实现析构函数的 destroy函数,注意 delete内存。
接下来
1 | clang++ -c -fPIC MyClassWrapper.cpp -o MyClassWrapper.o |
就是编译 接下来链接第一次生成c++的动态库 MyClass。 注意生成的动态库有 lib前缀。他们的依赖关系如下MyClassWrapper 链接 MyClass. 这个库的链接非常重要。
现在就有了一份 c风格的头文件和两个动态库 MyClass MyClassWrapper
本人对 c++并不熟悉如果有其他注意的点好改进,欢迎提出改进
Rust部分
https://github.com/TigerInYourDream/bindexample
rust部分直接选择使用 bindgen.生成 rust代码
1 | ├── Cargo.lock |
rust项目的结构如上所示。include中为 c风格的头文件。主要注意的点存在于 build.rs中
1 | extern crate bindgen; |
技巧
- 生成代码可以直接生成到 src目录下,否则会直接生成到 build目录下,也就是环境变量 OUT_DIR输出的环境的。可以生成到src 手动引用。这样生成的代码可以像正常代码一样可以被正常引用,也可以直接使用 rust analysis 分析。 如果使用 cspell记得单独排除这个文件。
- 可以在根目录下外加一个 wrapper.h文件,在 wrapper中指定外部的头文件。或者也可以参考 bingen的最佳实践。目前个人最佳实践是这样。
- 三个打印分别是 link-search 目录,下面两个是具体搜索的库,不要带前缀和后缀。
运行的注意点
- 注意 build.rs只会管 build时刻的链接目录,运行的时候并不会管。如果编译的时候提示找不到动态库,可以修改 search目录,或者仔细观察目录,把库的目录直接移动到项目根目录下(因为根目录也是默认的库搜索路径),还有很多其他路径,可以可以删除观察。
- cargo r氛围两阶段,一个是 build阶段,build阶段 build.rs中的设置是有用的。第二个阶段为运行,相当于执行 ./xxxx。 所以直接 cargo r -r 会找不到库路径
1 | export DYLD_LIBRARY_PATH=/path/to/dylib:$DYLD_LIBRARY_PATH |
可以使用 just来设置环境变量 和 一组编译运行计划来简化命令行。因为 just和和编写 bindgen 无关,随意不在本文提起。
然后就可以执行了