ScyllaDB Rust × C++ 互操作实战:基于 cxx::bridge 编写 Rust 包并接入构建系统
【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb
本文基于 ScyllaDB 官方开发文档《Rust and C++》(docs/dev/rust.md),完整讲解如何在以 Seastar 框架编写的 C++ 代码库中引入 Rust 模块:包括用cxx::bridge宏导出 Rust 函数的写法、新包的 7 步接入流程、Cargo.lock 提交规范,以及构建系统(configure.py / CMake / cxxbridge / cargo)如何把 Rust 静态库与 C++ 侧生成的头文件、源文件拼装起来。读完后你能独立创建一个可被 C++ 调用的 Rust 包,并理解两侧 ABI 的衔接原理。
1. 为什么 Scylla 要混用 Rust
Scylla 的核心是 C++,但 Rust 提供了一些 C++ 中缺失的有用特性(文档原文即以此开篇)。Scylla 的实际做法是把 Rust 与 C++ 通过 FFI 桥接起来:Rust 代码编译为采用 C++ ABI 的静态库,C++ 代码则通过工具生成的头文件以“原生命名”调用 Rust 方法。仓库中已有两个真实案例:测试用的inc包和 wasm 引擎绑定包wasmtime_bindings,它们分别对应rust/inc/与rust/wasmtime_bindings/,后者被lang/wasm相关功能使用(见 rust/src/lib.rs 中的extern crate声明与 configure.py 第 1400 行附近的依赖登记)。
2. 互操作实现原理:CXX crate 与 cxxbridge 代码生成
文档“Rust interoperability implementation”一节给出了完整机制,结合源码可以还原成如下链条:
Rust 侧声明:Rust 代码使用
#[cxx::bridge(namespace = "...")]宏,配合mod ffi与extern "Rust"块,标记需要导出到 C++ 的项。例如 rust/inc/src/lib.rs 中:#[cxx::bridge(namespace = "rust")] mod ffi { extern "Rust" { fn inc(x: i32) -> i32; } } fn inc(x: i32) -> i32 { x + 1 }编译产物:Rust 文件编译时生成一个采用 C++ ABI 的静态库,Rust 方法以特殊符号名导出。
C++ 侧生成:
cxxbridge命令行工具从同一份 Rust 源码生成 C++ 头文件(*.hh)与源文件(*.cc)。头文件以“原始命名”暴露所有导出项,可以像普通 C++ 头文件一样#include;*.cc中的 C++ 方法实现内部再转调那些特殊符号名对应的 Rust 实现。链接方式:所有 Rust 模块统一编译为单一静态库。文档明确指出,这是 Rust 链接到 C++ 目前唯一官方支持的方式;未来可能借助
rlib(Rust library)文件支持更多链接方法。
这一机制在构建系统中的落点见 rust/CMakeLists.txt:generate_cxxbridge()函数(第 42-59 行)对给定的.rs输入运行cxxbridge --header --output ${header}与cxxbridge --output ${source}两条命令,分别产出头文件与源文件;add_rust_library()函数(第 11-37 行)则调用cargo build --locked --profile=rust-${mode}编译出lib${name}.a,再包装成 CMake 的Rust::${name}imported target。最终wasmtime_bindings与inc两个 CMake 静态库都由生成代码 + 链接Rust::rust_combined组成(第 79-114 行)。
3. 实操指南:创建一个被 C++ 调用的 Rust 包(7 步流程)
文档给出了以新建包new_pkg、导出fn inc(x: i32) -> i32到命名空间xyz为例的完整流程。以下步骤全部保留,并逐一对照仓库中的实际文件补充说明:
创建包:在
rust目录下执行cargo new new_pkg --lib登记依赖:在 rust/Cargo.toml 的
[dependencies]列表中追加new_pkg = { path = "new_pkg", version = "0.1.0" }参考现有条目:
inc = { path = "inc", version = "0.1.0" }与wasmtime_bindings = { path = "wasmtime_bindings", version = "0.1.0" }。注意该文件顶部还声明了crate-type = ["staticlib"],这正是第 2 节所说的“编译为单一静态库”的配置来源。加入工作区入口:在 rust/src/lib.rs 中追加
extern crate new_pkg;。该文件当前内容为extern crate inc;与extern crate wasmtime_bindings;,是三个包汇入同一个rust_combined静态库的汇集点。编写包代码:在
new_pkg/Cargo.toml中配置依赖(尤其是cxxcrate),在new_pkg/src/lib.rs(及其他new_pkg/src/*.rs)中编写 Rust 代码。声明 FFI 导出:在
new_pkg/src/lib.rs中添加#[cxx::bridge(namespace = "xyz")] mod ffi { extern "Rust" { fn inc(x: i32) -> i32; } }namespace参数决定 C++ 侧的调用命名空间,即 C++ 里写成xyz::inc(...)。注意extern "Rust"块只是声明,真正的函数体仍需定义(见 rust/inc/src/lib.rs 中fn inc的实现)。登记构建依赖:在 configure.py 中,把
new_pkg/src/lib.rs添加到需要使用该 Rust 导出的 C++ 目标的依赖列表。configure.py 生成的 ninja 规则会把rust/下的每个.rs文件映射到对应的gen/rust/*.o编译产物(见 configure.py 第 2736-2737 行:obj = dep[:idx].replace('rust/','') + '.o')。在 C++ 中使用:
#include "rust/new_pkg.hh",然后调用xyz::foo()。以测试包为例,test/boost/rust_test.cc 正是这样写的:#include "rust/inc.hh" BOOST_AUTO_TEST_CASE(test_inc) { int k = 1; BOOST_REQUIRE(rust::inc(k) == 2); }其构建依赖在 configure.py 第 1837 行登记:
deps['test/boost/rust_test'] += ['rust/inc/src/lib.rs']——即第 6 步在真实项目中的形态。
3.1 cxx::bridge 可以放在 lib.rs 之外
文档还说明:cxx::bridge段不必非要放在lib.rs,也可以放在例如abc.rs中。此时必须注意两点:
- 在
lib.rs中添加mod abc;,确保该 bridge 与整个包一起参与编译; - 被导出函数的定义必须在
abc.rs的可见范围内——可以直接写在同一文件,也可以通过mod/use引入。
最后,configure.py的依赖登记要用这个文件(abc.rs所在包路径)替代lib.rs。
4. 构建系统如何串联 Rust:configure.py、CMake 与 cargo
从源码结构看,Scylla 同时存在两套构建路径:configure.py 生成的 Ninja 构建与 CMake 构建,二者的 Rust 处理逻辑一致。
Ninja 路径(configure.py):
- cxxbridge 规则:
cxxbridge --include rust/cxx.h对输入.rs生成头文件或源文件(configure.py 第 2562-2565 行),--include rust/cxx.h参数把 cxx crate 的通用头cxx.h并入生成头。 - 静态库规则:
cargo build --locked --manifest-path=rust/Cargo.toml --target-dir=... --profile=rust-{mode}(configure.py 第 2682 行),产物为librust_combined.a,其规则声明了对rust/Cargo.lock的依赖(第 2898 行),保证依赖锁定生效。 - 所有需要 FFI 头文件的 C++ 目标会把
$builddir/{mode}/gen/rust/cxx.h加入生成头依赖(第 2863 行)。
CMake 路径(rust/CMakeLists.txt):
add_rust_library(rust_combined)先按 CMake 的 build type 选出rust-dev/rust-debug/rust-release等 profile,再执行cargo build --locked --target-dir=... --profile=rust-${build_mode},把librust_combined.a拷贝到构建目录并注册为Rust::rust_combined。generate_cxxbridge(wasmtime_bindings ...)与generate_cxxbridge(inc ...)分别对 rust/wasmtime_bindings/src/lib.rs 和 rust/inc/src/lib.rs 执行代码生成,并各自建一个 CMake 静态库链接到Rust::rust_combined。- 构建时还支持通过
Scylla_RUSTC_WRAPPER注入RUSTC_WRAPPER环境变量以启用 sccache(CMakeLists.txt 第 4-9 行);启用预编译头时,绑定库还会复用scylla-precompiled-header,且注释解释了 PCH 的 sanitizer 编译标志必须与目标一致的原因(第 89-96 行)。
Cargo profile 与构建模式对齐:rust/Cargo.toml 定义了五个 Scylla 专属 profile——rust-dev(opt-level=2、去符号、关 overflow-checks)、rust-debug(opt-level=1、禁增量)、rust-sanitize(opt-level="s")、rust-release(保留 debug 信息)、rust-coverage。注释明确说明:cargo profilerust-xyz要与 ninja 的xyzmode 配套使用,从而保证 Rust 部分的优化/调试级别与 C++ 侧同一构建模式匹配。
5. 版本锁定:cxx crate 与 cxxbridge CLI 必须严格同步
这是一个容易被忽视但决定能否链接成功的关键约束,仓库中有两处强注释相互印证:
- rust/inc/Cargo.toml:
cxx = { version = "=1.0.83", features = ["c++20"] },用=精确钉死版本,注释说明 cxx crate 生成 FFI 的 Rust 侧,cxxbridge CLI 生成 C++ 侧,两者版本不一致会导致生成的 cxxbridge 符号在链接期无法解析;升级 cxx 必须同步升级 install-dependencies.sh 中的 cxxbridge-cmd 并重建工具链镜像。 - install-dependencies.sh 第 482 行:
cargo install cxxbridge-cmd --version 1.83 --root /usr/local之前的注释(第 477-481 行)重申了同样的 lockstep 要求,当前版本为 1.0.83。
因此在给新包添加cxx依赖时,务必沿用=1.0.83这类精确版本写法,而不是1.x之类的宽松区间。
6. 提交变更:Cargo.lock 的提交规范
文档“Submitting changes”一节规定:Scylla 跟踪rust/Cargo.lock文件,它记录最近一次成功构建所使用的精确依赖版本。以下三类依赖变更发生后,必须提交更新后的 Cargo.lock:
- 添加新的本地包(作为 Scylla 的依赖使用);
- 更新已有包中某个依赖的版本;
- 给某个包添加新依赖。
Cargo.lock 可通过cargo update命令再生。这一规范之所以严格,是因为构建时cargo build全程带--locked标志(见第 4 节两条规则),锁文件与Cargo.toml不一致会直接导致构建失败。
7. 要点回顾
- 互操作核心是
cxxcrate:#[cxx::bridge(namespace = "...")]+mod ffi+extern "Rust"标记导出项,Rust 侧编译为 C++ ABI 静态库,C++ 侧由cxxbridgeCLI 从同一份.rs生成*.hh/*.cc,头文件可像普通 C++ 头一样包含。 - 新包接入共 7 步:
cargo new→ 登记rust/Cargo.toml→rust/src/lib.rs加extern crate→ 编写包代码 →cxx::bridge声明 →configure.py登记依赖 → C++ 中#include生成头文件并调用。 cxx::bridge可放在lib.rs之外的文件中,但必须在lib.rs中声明mod且函数定义对该文件可见。- 当前唯一官方支持的链接方式是统一静态库(
crate-type = ["staticlib"]→librust_combined.a);未来可能支持rlib。 - 升级
cxx必须与cxxbridge-cmd(install-dependencies.sh,当前 1.0.83)版本锁定同步,否则链接期符号解析失败。 - 任何 Cargo 依赖变更都要提交由
cargo update生成的新Cargo.lock,构建以--locked模式强制执行。
参考路径:docs/dev/rust.md、rust/Cargo.toml、rust/src/lib.rs、rust/inc/src/lib.rs、rust/CMakeLists.txt、test/boost/rust_test.cc、configure.py、install-dependencies.sh。
【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考