comprehensive-rust 教程:深入解析 CXX 的#[cxx::bridge]绑定声明与 Chromium 落地实践
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
CXX 是 Chromium 项目当前采用的 C++/Rust 互操作方案:开发者在一份贴近 Rust 语法的接口定义(#[cxx::bridge]模块)中一次性声明完整的两语言边界,工具链据此在 Rust 与 C++ 两侧生成类型与函数声明。本篇教程围绕src/chromium/interoperability-with-cpp/example-bindings.md展开,从 bridge 模块的语法结构、宏背后的生成机制,到 Rust 切片与std::unique_ptr的跨语言支持,再到 Chromium 中rust_static_library的接入方式与allow_unsafe = true的深层原因,帮助你完整掌握 CXX 绑定的声明、使用与限制。
一、CXX 的核心原则:整条边界必须声明在 bridge 模块中
CXX 对语言边界有极强的约束:整个 C++/Rust 边界必须在.rs源码文件内部的cxx::bridge模块中声明。这意味着你不能像传统手工 FFI 那样,在 Rust 侧写extern "C"、在 C++ 侧写对应的头文件,然后指望两边各凭自觉保持一致;一切由 CXX 工具链统一驱动、统一生成。
CXX 的工作方式可以概括为:你在接口定义语言(interface definition language,形式上与 Rust 高度相似)中描述完整边界,CXX 工具据此在 Rust 和 C++ 两侧同时生成函数与类型的声明。仓库顶层页面 src/chromium/interoperability-with-cpp.md 中配有 CXX 的概览图,展示"同一份接口定义分别生成 C++ 侧代码与 Rust 侧代码、二者再经由最低公共分母的 C ABI 通信"的整体架构:
自动化带来的收益包括:
- 两侧天然一致:如果
#[cxx::bridge]与实际 C++ 或 Rust 定义不匹配,你会直接得到编译错误;而手工维护失同步的绑定只会产生未定义行为(Undefined Behavior)。 - 自动生成 FFI thunk:为不具备 C 特性的功能(如调用 Rust 方法或 C++ 成员函数)自动生成小的、符合 C ABI 的、自由函数形式的胶水;手工绑定则必须手动编写这类顶层自由函数。
- 内建核心类型处理:详见下一节。
二、一个完整的 Example Bindings:逐行拆解 bridge 模块
教程文档 src/chromium/interoperability-with-cpp/example-bindings.md 中给出的示例,实际来自仓库内 third_party/cxx/book/snippets.rs 中以cxx_overview锚点标记的代码片段:
#[cxx::bridge] mod ffi { extern "Rust" { type MultiBuf; fn next_chunk(buf: &mut MultiBuf) -> &[u8]; } unsafe extern "C++" { include!("example/include/blobstore.h"); type BlobstoreClient; fn new_blobstore_client() -> UniquePtr<BlobstoreClient>; fn put(self: &BlobstoreClient, buf: &mut MultiBuf) -> Result<u64>; } } // Definitions of Rust types and functions go here这个片段信息密度很高,值得逐块拆解:
1.#[cxx::bridge]:看起来像普通mod,实际是过程宏
从表面看,mod ffi与普通 Rust 模块毫无二致。但#[cxx::bridge]是一个过程宏(procedural macro),它会对模块体做大量复杂处理:解析extern "Rust"与extern "C++"块、校验两侧签名、生成跨语言胶水代码等。生成产物虽然仍然是一个名为ffi的模块,但其内部代码远比表面所见复杂。
2.extern "Rust"块(上半部分):C++ 调用 Rust 的通道
extern "Rust"声明的是Rust 侧类型与函数,供 C++ 调用:
type MultiBuf;:声明一个不透明类型(opaque type)。其完整定义写在.rs文件里(本例在 bridge 模块之外),C++ 侧只能持有并通过指针/引用使用它,无法看到内部字段。fn next_chunk(buf: &mut MultiBuf) -> &[u8];:声明一个 Rust 函数。注意其签名中使用Rust 切片&[u8]作为返回值——这是 CXX 对 Rust 切片在 C++ 侧的原生支持(见下文"核心类型"小节)。
3.unsafe extern "C++"块(下半部分):Rust 调用 C++ 的通道
extern "C++"声明的是C++ 侧类型与函数,供 Rust 调用,因此标注unsafe(与后续allow_unsafe = true直接相关,详见第四节):
include!("example/include/blobstore.h"):告诉 CXX 在生成的头文件中#include哪个 C++ 头文件。type BlobstoreClient;:声明 C++ 侧的不透明类型BlobstoreClient,Rust 侧只能通过UniquePtr<BlobstoreClient>之类的智能指针间接持有。fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;:工厂函数,返回原生支持std::unique_ptr的 C++ 智能指针。fn put(self: &BlobstoreClient, buf: &mut MultiBuf) -> Result<u64>;:C++ 成员函数调用,可把&mut MultiBuf(Rust 类型)作为参数传入 C++ 方法。
4. 一个常见误区:头文件并不是被 Rust"解析"
初学者常误以为 CXX 在 Rust 侧解析 C++ 头文件。这是误导性的:include!("...")指定的头文件永远不会被 Rust 编译器解释,它只是被原样#include进生成的 C++ 代码中,仅供 C++ 编译器使用。Rust 侧对 C++ 类型的全部认知都来自 bridge 模块中的声明。
三、核心类型支持:std::unique_ptr与 Rust 切片的原生桥接
#[cxx::bridge]示例中透露出两个关键能力,也是 CXX 在 Chromium 被选用的重要原因(详见 src/chromium/interoperability-with-cpp.md 的讲解备注):
| 能力 | 说明 | 手工绑定的痛点对照 |
|---|---|---|
| 智能指针原生支持 | std::unique_ptr<T>、std::shared_ptr<T>、Box<T>可直接跨边界传递 | 手工绑定只能传 C ABI 兼容的裸指针,生命周期与内存安全风险显著增加 |
| Rust 切片原生支持 | &[T]可跨 FFI 边界传递,即使它不保证特定 ABI 或内存布局 | std::span<T>/&[T]需要手工拆解为"指针 + 长度"再重建,且两语言对空切片表示不一致,极易出错 |
| 字符串差异管理 | rust::String与CxxString自动处理两语言字符串表示差异 | 例如rust::String::lossy可从非 UTF-8 输入构造 Rust 字符串,rust::String::c_str可为字符串追加 NUL 终止符 |
仓库中的 third_party/cxx/blobstore 就是一个完整可运行的 CXX 示例工程,其 src/main.rs 展示了比课程片段更完整的桥接声明:
#[allow(unsafe_op_in_unsafe_fn)] #[cxx::bridge(namespace = "org::blobstore")] mod ffi { // Shared structs with fields visible to both languages. struct BlobMetadata { size: usize, tags: Vec<String>, } // Rust types and signatures exposed to C++. extern "Rust" { type MultiBuf; fn next_chunk(buf: &mut MultiBuf) -> &[u8]; } // C++ types and signatures exposed to Rust. unsafe extern "C++" { include!("include/blobstore.h"); type BlobstoreClient; fn new_blobstore_client() -> UniquePtr<BlobstoreClient>; fn put(self: Pin<&mut BlobstoreClient>, parts: &mut MultiBuf) -> u64; fn tag(self: Pin<&mut BlobstoreClient>, blobid: u64, tag: &str); fn metadata(&self, blobid: u64) -> BlobMetadata; } }与课程示例相比,它额外展示了几个实践要点:
#[cxx::bridge(namespace = "org::blobstore")]:通过namespace参数把绑定锚定到 C++ 命名空间,与 include/blobstore.h 中namespace org { namespace blobstore { ... } }的结构对应。- 共享结构体(shared structs):
struct BlobMetadata声明在模块顶层,字段对两种语言同时可见,C++ 侧可直接按结构体成员使用(见 src/blobstore.cc 中BlobstoreClient::metadata的填充方式)。 Pin<&mut BlobstoreClient>:对 C++ 数据的可变引用必须使用Pin。原因是 C++ 数据可能包含自引用指针,不能像 Rust 数据那样随意移动(这一点在 error-handling-qr.md 的讲解备注中也有说明)。- Rust 侧定义:
MultiBuf的真实定义(内部为Vec<Vec<u8>>)与next_chunk的实现都位于 bridge 模块之外的普通 Rust 代码中(src/main.rs)。
对应的 C++ 侧实现 src/blobstore.cc 展示了双向调用的完整闭环:C++ 的put反复调用 Rust 导出的next_chunk(buf)来遍历"不连续文件"的各个分块,直到遇到空切片为止。而 Blobstore 工程的构建文件 BUILD 则展示了 CXX 的 Bazel 接入方式:rust_cxx_bridge规则以src/main.rs为输入生成桥接,cc_library同时依赖blobstore-include与bridge/include,rust_binary则同时依赖:blobstore-sys与:bridge。
四、在 Chromium 中接入:rust_static_library与cxx_bindings
把视角拉回 Chromium。根据 src/chromium/interoperability-with-cpp/using-cxx-in-chromium.md,Chromium 中的实践是:为每一个需要使用 Rust 的"叶子节点"(leaf-node)定义一个独立的#[cxx::bridge] mod,通常一个rust_static_library对应一个 bridge 模块。
在既有的rust_static_librarytarget 中,只需在crate_root与sources之外补上两行配置:
cxx_bindings = [ "my_rust_file.rs" ] # list of files containing #[cxx::bridge], not all source files allow_unsafe = true注意注释里的关键提醒:cxx_bindings列出的是包含#[cxx::bridge]的文件,而不是全部源文件。随后 C++ 头文件会生成在合理位置,C++ 侧直接以"源文件名 +.rs.h"的方式引用:
#include "ui/base/my_rust_file.rs.h"另外,//base中提供了一些工具函数,用于在 Chromium C++ 类型与 CXX Rust 类型之间转换,例如SpanToRustSlice。
为什么需要allow_unsafe = true?
这是学生最常问的问题,官方讲解备注给出了两层回答:
- 宽泛层面:按 Rust 的标准衡量,没有任何 C/C++ 代码是"安全"的。Rust 与 C/C++ 之间的往返调用可能对内存做任意操作,并危及 Rust 自身数据布局的安全性。过多
unsafe关键字确实会损害关键字本身的信噪比(这在社区中是有争议的),但严格来说,把任何外来代码引入 Rust 二进制都可能从 Rust 视角引发意外行为。 - 精确层面:参考 src/chromium/interoperability-with-cpp.md 顶部架构图可知——在幕后,CXX 生成的就是上一节中我们手工编写的
unsafe与extern "C"函数,因此需要在 GN 侧显式放行。
叶子节点策略与 CXX 的限制
CXX 并非万能。根据 src/chromium/interoperability-with-cpp/limitations-of-cxx.md,CXX 本质上只适用于两种情况:
- 你的 Rust–C++ 接口足够简单,能够在一份声明中全部描述;
- 你只使用 CXX 已原生支持的类型,例如
std::unique_ptr、std::string、&[u8]等。
它存在不少限制,例如不支持 Rust 的Option类型。此外还有若干粘滞点值得讨论:
- 错误处理基于 C++ 异常(见下一节);
- 函数指针使用起来很笨拙。
这些限制迫使 Chromium 只在隔离良好的叶子节点中使用 Rust,而不是做任意的 Rust–C++ 互操作。在评估某个 Rust 用例时,一个良好的起点是:先草拟语言边界的 CXX 绑定,看它是否足够简单。补充说明:由于组件构建的链接细节,当前 Chromium 中一个组件的 Rust 代码不能依赖另一个组件的 Rust 代码,这也是把 Rust 限制在叶子节点内的另一个原因。
五、错误处理:Result<T, E>在 Chromium 中的替代方案
CXX 对Result<T, E>的支持依赖于 C++ 异常(详见 src/chromium/interoperability-with-cpp/error-handling.md),因此Chromium 中不能直接使用。官方给出的替代策略如下:
T(成功值)的处理:- 通过输出参数返回,例如
&mut T。前提是T能跨 FFI 边界传递,例如:原始类型(如u32、usize);或者 CXX 原生支持的类型(如UniquePtr<T>,且要有合适的默认值用于失败情形——Box<T>就做不到)。 - 保留在 Rust 侧、通过引用暴露。当
T是 Rust 类型、既不能跨边界传递、也无法存入UniquePtr<T>时,可能需要这种方式。
- 通过输出参数返回,例如
E(错误值)的处理:- 用布尔值表示成败,如
true成功、false失败; - 理论上可以保留错误细节,但到目前为止实践中还没有此需求。
- 用布尔值表示成败,如
仓库还配套了一个真实案例 error-handling-qr.md:Chromium 的 QR 码生成器qr_code_generator就是"用布尔值传达成功/失败、成功结果可跨 FFI 边界传递"的例子:
#[cxx::bridge(namespace = "qr_code_generator")] mod ffi { extern "Rust" { fn generate_qr_code_using_rust( data: &[u8], min_version: i16, out_pixels: Pin<&mut CxxVector<u8>>, out_qr_size: &mut usize, ) -> bool; } }该示例还有几个值得注意的细节(来自讲解备注):
out_qr_size不是向量的大小,而是QR 码的尺寸(即向量大小的平方根,确实略显冗余);- 在调用前初始化
out_qr_size很重要:与 C++ 不同,在 Rust 中构造一个指向未初始化内存的引用本身就是未定义行为(C++ 要等到实际解引用才算 UB); - 关于
Pin的疑问:CXX 之所以对 C++ 数据的可变引用要求Pin,是因为 C++ 数据可能包含自引用指针,不能像 Rust 数据那样被移动。
六、从示例到实战:仓库配套资源的阅读路径
若要深入实践,仓库内提供了从简单到完整的配套材料:
| 资源 | 路径 | 用途 |
|---|---|---|
| CXX 桥接代码片段合集 | third_party/cxx/book/snippets.rs | 本教程示例的来源,含rust_bridge、cpp_bridge、shared_types、rust_result、cpp_exception、cxx_overview等多个锚点 |
| C++ 侧代码片段 | third_party/cxx/book/snippets.cc | 配套的 C++ 侧片段 |
| 完整可运行示例工程 | third_party/cxx/blobstore | 覆盖共享结构体、双向调用、Bazel 构建的完整 CXX 示例 |
| Chromium 互操作章节入口 | src/chromium/interoperability-with-cpp.md | CXX 架构总览与工具收益说明 |
| CXX 接入 Chromium | src/chromium/interoperability-with-cpp/using-cxx-in-chromium.md | GN 配置与allow_unsafe说明 |
| CXX 限制清单 | src/chromium/interoperability-with-cpp/limitations-of-cxx.md | 叶子节点策略与类型限制 |
| 错误处理替代方案 | src/chromium/interoperability-with-cpp/error-handling.md、error-handling-qr.md | Chromium 中规避 C++ 异常依赖的实践 |
| CXX 架构图 | src/android/interoperability/cpp/overview.svg | 同一接口定义双向生成代码的可视化说明 |
结合本教程与上述资源,你可以完成从"读懂#[cxx::bridge]声明"到"在 Chromium 中为叶子节点配置cxx_bindings并编写第一份 Rust–C++ 绑定"的完整实践路径。核心方法论可归纳为三点:边界全部声明在 bridge 模块内(不要依赖手工 FFI)、只使用 CXX 原生支持的类型、先草拟绑定评估接口是否足够简单再决定是否引入 Rust。
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考