Chromium 中的 C++/Rust 互操作:基于 CXX 的接口定义与实践指南
发布时间:2026/9/10 12:51:40 作者:尧图编辑部 阅读量:1,286

Chromium 中的 C/Rust 互操作基于 CXX 的接口定义与实践指南【免费下载链接】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本文基于 Google Rust 课程comprehensive-rust的 Chromium 章节系统讲解 Chromium 团队如何在 Rust 与 C 之间构建类型安全的语言边界以接口定义语言#[cxx::bridge]声明整条边界由 CXX 工具链自动生成双端代码并结合 GN 构建配置、错误处理模式与真实代码案例PNG 解码器、QR 码生成器给出可直接落地的实战方案。读完本文你将理解 CXX 的底层原理、Chromium 中接入 CXX 的完整配置流程以及在没有 C 异常的前提下处理跨语言错误的三种替代模式。为什么 Chromium 选择 CXXRust 社区为 C/Rust 互操作提供了多种方案且新工具仍在不断涌现。当前以本课程所记录的代码库状态为准Chromium 使用的是名为 CXX 的工具。其核心思路是你在一种接口定义语言IDL中描述整条语言边界——这种语言与 Rust 非常接近——然后由 CXX 工具为函数和类型同时生成 Rust 与 C 两侧的声明。这意味着开发者不再需要手写两份彼此独立的绑定代码而是维护一份单一的事实来源。CXX 的完整使用示例可参见 CXX 官方教程与类型参考本文不展开外部链接仓库内的讲解以 example-bindings.md、limitations-of-cxx.md 等章节为准。课程原文档以讲解示意图上图开场这张图揭示了 CXX 的实质——背后所做的与你此前在 unsafe-rust 章节 中手工编写extern C绑定做的事完全相同只不过由工具自动完成。自动化带来以下三个关键收益工具保证 C 侧与 Rust 侧始终一致如果#[cxx::bridge]与实际 C 或 Rust 定义不匹配你会直接得到编译错误而手写绑定一旦两端失同步得到的将是 Undefined Behavior未定义行为。工具自动生成 FFI thunk即那些小型的、兼容 C ABI 的自由函数用于支持非 C 特性的跨语言调用例如从 FFI 调用 Rust 或 C 的方法。手写绑定时这类顶层自由函数必须由你手工编写。工具与运行时库原生处理一批核心类型[T]可以跨 FFI 边界传递尽管它不保证任何特定 ABI 或内存布局。手写绑定时std::spanT/[T]必须手工拆解为指针 长度再重新组装——而两种语言对空切片的表示方式略有差异这一步极易出错。智能指针如std::unique_ptrT、std::shared_ptrT以及Box得到原生支持。手写绑定只能传递 C ABI 兼容的裸指针这会显著增加生命周期与内存安全风险。rust::String与CxxString类型能够理解并维护两种语言在字符串表示上的差异例如rust::String::lossy可以从非 UTF-8 输入构造 Rust 字符串rust::String::c_str可以为字符串补上 NUL 结尾。深入 CXX 桥接代码从语法到生成结果CXX 要求整条 C/Rust 边界都声明在.rs源文件内的#[cxx::bridge]模块中。课程仓库在 third_party/cxx/book/snippets.rs 中保存了取自 CXX 官方文档的示例锚点其中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() - UniquePtrBlobstoreClient; fn put(self: BlobstoreClient, buf: mut MultiBuf) - Resultu64; } }解读这段代码需要注意几个要点extern Rust块声明由 Rust 侧实现、供 C 调用的函数与类型图的上半部分。C 侧拿到的是这些函数的 C ABI 封装。unsafe extern C块声明由 C 侧实现、供 Rust 调用的函数与类型图的下半部分。include!宏指定 C 头文件位置仅供生成后的 C 代码#include使用。虽然它看起来像一个普通的 Rustmod但#[cxx::bridge]过程宏对其做了大量复杂处理生成的代码远比表面上看到的复杂不过最终仍会在你的代码中产出一个名为ffi的模块。示例同时体现了 CXX 对C 的std::unique_ptr在 Rust 中的原生支持以及对Rust 切片在 C 中的原生支持。课程还专门澄清了一个常见误解这段代码看起来像是 Rust 在解析一个 C 头文件但这是误导——这个头文件永远不会被 Rust 解释它只是被#include进生成的 C 代码中供 C 编译器使用。仓库中还保留了其他方向的桥接示例rust_bridge锚点snippets.rs展示如何在 Rust 中定义不透明类型type MyType;、方法fn foo(self)与自由函数fn bar() - BoxMyTypeshared_types锚点snippets.rs展示如何在桥接模块内直接声明共享结构体与枚举PlayingCard、Suit这类类型会在双端自动生成对应定义。在 Chromium 中接入 CXXGN 构建配置Chromium 使用 GN 构建系统。接入 CXX 时做法是为每一个希望使用 Rust 的“叶子节点”定义一个独立的#[cxx::bridge] mod——通常每个rust_static_library对应一个。只需在已有的rust_static_librarytarget 中与crate_root、sources并列添加两个参数详见 using-cxx-in-chromium.mdcxx_bindings [ my_rust_file.rs ] # list of files containing #[cxx::bridge], not all source files allow_unsafe true要点如下cxx_bindings列出包含#[cxx::bridge]模块的源文件清单而非全部源文件。构建系统会为清单中的每个文件运行 CXX 代码生成器。allow_unsafe true允许该 Rust 库中出现unsafe代码原因详见下一小节。头文件生成位置C 头文件会生成在合理位置C 侧可直接包含#include ui/base/my_rust_file.rs.h注意头文件名与 Rust 源文件名一一对应my_rust_file.rs→my_rust_file.rs.h。类型转换工具//base中提供了一些在 Chromium C 类型与 CXX Rust 类型之间互相转换的工具函数例如SpanToRustSlice用于将 Chromium 的base::span转换为 Rust 切片。为什么仍然需要allow_unsafe true这是学员常问的问题课程给出了宽、窄两层回答宽泛的回答按照常规 Rust 标准任何 C/C 代码都不算“安全”。从 Rust 来回调用 C/C 可能对内存做任意操作并破坏 Rust 自身数据布局的安全性。unsafe关键字如果过多地出现在 C/C 互操作代码中会损害该关键字的信噪比这一话题在社区中本身也存在争议The CXX Debate 一文有讨论但从严格意义上讲将任何外部代码引入 Rust 二进制都可能导致 Rust 视角下的意外行为。具体的回答回到本页顶部的示意图——CXX 在幕后生成的正是我们之前在章节中手工编写的那种 Rustunsafe函数与extern C函数参见 unsafe-rust.md。allow_unsafe是为这些自动生成的代码放行。CXX 的限制与 Chromium 的 leaf node 策略CXX 本质上适合以下两类场景详见 limitations-of-cxx.md你的 Rust-C 接口足够简单能够完整地在桥接模块中声明全部内容你只使用 CXX 原生支持的类型例如std::unique_ptr、std::string、[u8]等。使用 CXX 时最有用的一页资料是其类型参考Type Reference外部链接不在此展开。它同时有诸多限制——例如不支持 Rust 的Option类型。其他已知的棘手之处还包括错误处理基于 C 异常详见下一节函数指针使用起来很别扭。这些限制决定了 Chromium 的策略只在隔离良好的“叶子节点”leaf nodes中使用 Rust而不是做任意的 Rust-C 互操作。此外受组件构建component build中链接细节的限制当前一个组件中的 Rust 代码不能依赖另一个组件中的 Rust 代码——这进一步强化了“Rust 仅用于叶子节点”的约束。因此在 Chromium 中评估一个 Rust 用例时一个好的起点是先起草语言边界上的 CXX 绑定看看它是否足够简单。如果边界声明复杂到难以用 CXX 表达通常意味着这个位置不适合引入 Rust。在 Chromium 中处理错误绕过 C 异常的三种模式CXX 对ResultT, E的支持依赖 C 异常详见 error-handling.md而 Chromium 禁用了 C 异常因此这条路在 Chromium 内走不通。替代方案需要把ResultT, E拆成“成功值”和“失败信息”分别处理T成功值部分的处理方式通过 out 参数返回例如mut T。这要求T能够跨 FFI 边界传递例如原始类型如u32或usizeCXX 原生支持的类型如UniquePtrT且该类型在失败场景下有合适的默认值可用——这与BoxT不同BoxT没有合适的“失败默认值”。保留在 Rust 侧通过引用暴露。当T是 Rust 类型、既无法跨 FFI 边界传递、也无法存入UniquePtrT时可采用此方案。E失败信息部分的处理方式用布尔值表示成败例如true表示成功false表示失败理论上可以保留更详细的错误信息但到目前为止实践中尚无此需求。下面两个来自 Chromium 的真实案例分别演示了这两种模式。案例一PNG 解码器——成功值无法跨边界时的Result替代课程以 Chromium 中一个 PNG 解码器原型error-handling-png.md说明当成功结果无法跨 FFI 边界传递时该怎么做。#[cxx::bridge(namespace gfx::rust_bindings)] mod ffi { extern Rust { /// This returns an FFI-friendly equivalent of ResultPngReadera, /// (). fn new_png_readera(input: a [u8]) - BoxResultOfPngReadera; /// C bindings for the crate::png::ResultOfPngReader type. type ResultOfPngReadera; fn is_err(self: ResultOfPngReader) - bool; fn unwrap_as_muta, b( self: b mut ResultOfPngReadera, ) - b mut PngReadera; /// C bindings for the crate::png::PngReader type. type PngReadera; fn height(self: PngReader) - u32; fn width(self: PngReader) - u32; fn read_rgba8(self: mut PngReader, output: mut [u8]) - bool; } }该设计的几个关键点PngReader与ResultOfPngReader都是 Rust 类型——这类对象必须借助BoxT的间接层才能跨 FFI 边界。不能使用out_parameter: mut PngReader因为CXX 不允许 C 按值存储 Rust 对象。本案例说明即使 CXX 不支持任意泛型或模板我们仍可通过**手动特化monomorphize**的方式让泛型跨边界。示例中ResultOfPngReader就是一个非泛型类型它把调用转发到ResultT, E的相应方法如is_err、unwrap、as_mut上等价于ResultPngReadera, ()的 FFI 友好版本。错误信息部分用is_err() - bool布尔值表达成功值则通过unwrap_as_mut返回的mut PngReader引用在 Rust 侧访问。案例二QR 码生成器——布尔返回 out 参数QR 码生成器是“成功结果可以跨 FFI 边界传递、用布尔值表达成败”的实例error-handling-qr.md。该桥接对应 Chromium 中的components/qr_code_generator/qr_code_generator_ffi_glue.rs#[cxx::bridge(namespace qr_code_generator)] mod ffi { extern Rust { fn generate_qr_code_using_rust( data: [u8], min_version: i16, out_pixels: Pinmut CxxVectoru8, out_qr_size: mut usize, ) - bool; } }这里有三处值得深入理解的语义out_qr_size的含义它并不是输出像素向量的长度而是QR 码本身的尺寸事实上它与向量大小略冗余——它是向量大小的平方根。传入min_version可以指定 QR 码的最低版本。初始化out_qr_size的重要性调用 Rust 函数之前必须先初始化该 out 参数。因为创建指向未初始化内存的 Rust 引用本身就是 Undefined Behavior——这与 C 不同C 中只有真正解引用未初始化内存才构成 UB。Pinmut CxxVectoru8的用途CXX 对 C 数据的可变引用需要Pin原因是C 数据不能像 Rust 数据那样被移动——它可能包含自引用指针。Pin保证该内存位置在借出期间不会被移动。小结Chromium 的 Rust 集成方法论综合本主题各文档interoperability-with-cpp.md 及其子章节Chromium 的 Rust-C 互操作方法论可以概括为四条原则单一事实来源用#[cxx::bridge]以声明式 IDL 描述整条语言边界由 CXX 生成双端代码从机制上杜绝手写绑定的失同步与 UB 风险。叶子节点隔离受 CXX 表达能力不支持Option、泛型需手动特化、函数指针别扭与组件构建链接限制的约束Rust 只用于边界简单、隔离良好的叶子节点。构建即声明在rust_static_library中通过cxx_bindings列出桥接源文件allow_unsafe true为 CXX 生成的unsafe/extern C代码放行C 侧直接包含生成的*.rs.h头文件。无异常的错误通道在禁用 C 异常的 Chromium 中用“布尔值表达成败 out 参数/引用暴露成功值 手动特化 Result 包装类型”三种模式组合替代 CXX 的ResultT, E。这套方法论不仅在课程中用于教学也直接映射到 Chromium 的真实代码如qr_code_generator_ffi_glue.rs等可作为在大型 C 代码库中安全、渐进式引入 Rust 的参考模板。若需继续深入可进一步阅读仓库中的 using-cxx-in-chromium.md、example-bindings.md、error-handling.md 以及 chromium.md 欢迎页也可对照 Android 章节的 cpp.md 了解同一套 CXX 思路在 Android 侧的落地方式。【免费下载链接】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),仅供参考