Comprehensive Rust 课程之 Rust in Chromium:第三方 crate 引入与 C++/Rust 互操作实战
Comprehensive Rust 课程之 Rust in Chromium:第三方 crate 引入与 C++/Rust 互操作实战
导读
本文围绕 Comprehensive Rust 课程中 Chromium 模块(src/chromium.md)展开,讲解 Rust 在 Chromium 中的真实集成方式:Rust 被官方支持用于第三方库,同时以**第一方胶水代码(glue code)**连接 Rust 与既有 C++ 代码。读完本文,你将掌握 Chromium 中 Rust 的两种集成模式、gn/ninja 与 cargo 生态的取舍、添加第三方 crate(Rust 库)的完整流程,以及用 CXX 生成 C++/Rust 边界绑定的实操方法,并了解当日课程以"用 Rust 处理 UTF-8 字符串"为切入点的练习路径。
课程提示语原话:今天,我们将调用 Rust 去做一些与字符串相关的简单处理。如果你恰好负责向用户展示 UTF-8 字符串的代码区域,欢迎在你的代码角落中遵循同样的做法,而不必局限于课程演示的精确位置。
一、Rust 在 Chromium 中的定位:第三方库 + 第一方胶水代码
Chromium 对 Rust 的支持策略非常明确:Rust 用于第三方库(third-party libraries),并由第一方胶水代码连接 Rust 与既有 C++ 代码。这一结论来自 src/chromium.md 的开篇陈述,是整个模块的技术基调。
这种定位有两个关键含义:
- 第三方 Rust crate 可以进入 Chromium:Chromium 采用"精选依赖集"(curated set of dependencies)策略,第三方 Rust 库需要经过引入、构建适配和安全审计,才能被纳入
//third_party/rust。 - 第一方代码仍然以 C++ 为主:Rust 与 C++ 之间的语言边界(language boundary)由少量第一方胶水代码承担,这些胶水代码负责类型转换与调用衔接。
课程模块随后通过 setup、policy、cargo、adding-third-party-crates、interoperability-with-cpp、testing 六个子章节,将这一策略拆解为可操作的工程流程。
二、环境准备:先能构建并运行 Chromium
在进入 Rust 集成之前,src/chromium/setup.md 要求先确保本地可以构建并运行 Chromium。任何平台、任何构建参数组合都可以,但代码必须相对较新(commit position 1223636 之后,对应 2023 年 11 月):
gn gen out/Debug
autoninja -C out/Debug chrome
out/Debug/chrome # 或 Mac 上:out/Debug/Chromium.app/Contents/MacOS/Chromium
其中:
gn gen out/Debug生成out/Debug目录下的构建配置;autoninja -C out/Debug chrome以 ninja 增量编译方式构建chrome目标;- 启动产物即为
out/Debug/chrome(Mac 上为.app内的可执行文件)。
官方推荐使用 component、debug 构建以换取最快的迭代速度——这本身就是默认配置。课程同时强调,如果尚未准备好 Chromium 构建环境,需要参考官方《How to build Chromium》指南,并提醒:搭建 Chromium 构建环境本身相当耗时。此外建议安装 Visual Studio Code 以便于浏览代码。
关于练习的编排方式:本模块包含一系列相互递进(build on each other)的练习,它们分散在整个课程的不同时段进行,而不是集中在最后统一完成。如果某个时段未完成,下一时段可继续跟上。
三、Chromium Rust 策略:两种集成模式
Chromium 的 Rust 策略文档位于其仓库的 docs/rust.md,核心结论是:Rust 既可用于第一方代码,也可用于第三方代码(见 src/chromium/policy.md)。
3.1 模式一:纯第一方 Rust 代码
第一种形态是直接在 Chromium 内编写第一方 Rust 代码,与既有 C++ 代码在语言边界处互通:
"C++" Rust
.- - - - - - - - - -. .- - - - - - - - - - -.
: : : :
: Existing Chromium : : Chromium Rust :
: "C++" : : code :
: +---------------+ : : +----------------+ :
: | | : : | | :
: | o-----+-+-----------+-+-> | :
: | | : Language : | | :
: +---------------+ : boundary : +----------------+ :
: : : :
`- - - - - - - - - -' `- - - - - - - - - - -'
C++ 侧与 Rust 侧通过语言边界(language boundary)进行调用,边界处由胶水代码完成类型与调用的转换。
3.2 模式二:第三方 crate + 第一方 wrapper(更常见)
第二种形态是引入第三方 crate。由于极少数 Rust 库会直接暴露 C/C++ API,因此通常还需要少量第一方胶水代码(wrapper)来桥接:
"C++" Rust
.- - - - - - - - - -. .- - - - - - - - - - - - - - - - - - - - - - -.
: : : :
: Existing Chromium : : Chromium Rust Existing Rust :
: "C++" : : "wrapper" crate :
: +---------------+ : : +----------------+ +-------------+ :
: | | : : | | | | :
: | o-----+-+-----------+-+-> o-+----------+--> | :
: | | : Language : | | Crate | | :
: +---------------+ : boundary : +----------------+ API +-------------+ :
: : : :
`- - - - - - - - - -' `- - - - - - - - - - - - - - - - - - - - - - -'
图中可以看到:既有 C++ 代码 → 语言边界 → Chromium 第一方 Rust "wrapper" → crate API → 既有第三方 Rust crate。
3.3 今日课程聚焦
由于第三方 crate 场景更复杂,当日课程将重点放在:
- 引入第三方 Rust 库(crates);
- 编写胶水代码,使这些 crate 能从 Chromium C++ 侧被调用(同样的技术也适用于第一方 Rust 代码)。
四、两种生态的对比:gn/ninja 与 cargo 的取舍
Chromium 的构建体系与 Rust 社区的默认工具链存在本质差异。src/chromium/cargo.md 对此做了系统对比:
| 特性 | C++ 库 | Rust crate |
|---|---|---|
| 构建系统 | 种类繁多 | 统一:Cargo.toml |
| 典型库体积 | 偏大 | 小 |
| 传递依赖(transitive dependencies) | 少 | 多 |
对于 Chromium 工程师而言,这意味着 Rust crate 之间极容易互相依赖(在 Cargo.toml 中加一行即可),因此在引入时往往需要连带引入多个库,这是优势也是成本。
具体到"如何用 Rust 写 Chromium 代码",有三条可选路径:
- 使用
gn+ninja,借助//build/rust/*.gni中的模板(例如后续会遇到的rust_static_library)。好处是使用 Chromium 审计过的工具链与 crate; - 使用
cargo,但自我限制,只使用 Chromium 审计过的工具链与 crate; - 使用
cargo,信任外部工具链和/或从 crates.io 下载的 crate。
课程从此往后的内容聚焦于 gn 与 ninja,因为这是把 Rust 代码构建进 Chromium 浏览器的官方方式;同时强调 Cargo 是 Rust 生态的重要组成部分,应当保留在工具箱中。
4.1 小组练习:评估 cargo 的风险剖面
课程在此设置了一个小型分组讨论练习,让学员评估:
- 头脑风暴
cargo可能带来优势的场景,并评估这些场景的风险剖面; - 讨论在使用
gn/ninja、离线cargo等不同方案时,需要信任哪些工具、库和人群。
讲师备注中给出了一些讨论线索,例如:clap(命令行解析)、serde(序列化/反序列化)、itertools(迭代器工具)等 crate 体验良好,使得编写工具或原型开发非常便捷;rustup 可轻松切换 rustc 版本;Mozilla 的 cargo vet 可简化并共享安全审计(cargo install --locked cargo-vet)。隐含被信任的清单则包括:rustc(及其依赖的 LLVM/Clang、二进制引导编译器)、rustup、cargo、rustfmt、构建与分发预编译工具链的内部基础设施、cargo audit/cargo vet、被 vendored 进 //third_party/rust 的库(由 security@chromium.org 审计)等。
五、添加第三方 crate:从 Cargo.toml 到 gnrt_config.toml
5.1 集中式 Cargo.toml 管理
Chromium 为直接依赖维护了一套集中管理的 crate 依赖清单,位于 third_party/rust/chromium_crates_io/Cargo.toml。其形态与普通 Rust 项目无异(见 src/chromium/adding-third-party-crates/configuring-cargo-toml.md):
[dependencies]
bitflags = "1"
cfg-if = "1"
cxx = "1"
# lots more...
与任何 Cargo.toml 一样,可以指定依赖的更多细节,通常需要指明希望启用的 features。但与其他项目不同的是,把 crate 加进 Chromium 时,往往还需要在另一个文件中补充 Chromium 特定的信息——即下文提到的 gnrt_config.toml。
5.2 gnrt_config.toml:Chromium 特有的 crate 扩展配置
与 Cargo.toml 并排存在的是 third_party/rust/chromium_crates_io/gnrt_config.toml(见 src/chromium/adding-third-party-crates/configuring-gnrt-config-toml.md),它承载 Chromium 对 crate 处理的扩展配置。
每新增一个 crate,至少要指定 group(分组),可选值有三档:
# 'safe': The library satisfies the rule-of-2 and can be used in any process.
# 'sandbox': The library does not satisfy the rule-of-2 and must be used in
# a sandboxed process such as the renderer or a utility process.
# 'test': The library is only used in tests.
即:
safe:该库满足 Chromium 的 rule-of-2(内存安全规则),可在任何进程中使用;sandbox:该库不满足 rule-of-2,必须仅在沙箱化进程(如 renderer 进程或 utility 进程)中使用;test:该库仅用于测试代码。
配置示例:
[crate.my-new-crate]
group = 'test' # only used in test code
此外,视 crate 源码目录布局而定,还可能需要在该文件中指定其 LICENSE 文件的位置。课程后续还会讲到通过该文件解决其他引入问题(对应 adding-third-party-crates/resolving-problems.md 的内容)。
5.3 引入 crate 的完整链路
src/chromium/adding-third-party-crates.md 总结了引入一个 crate 需要完成的三个步骤,课程将依次展开:
- 把 crate 放进 Chromium 源码树(对应
downloading-crates.md、checking-in.md:如何获取 crate 源码并纳入//third_party/rust,涉及Cargo.toml与gnrt_config.toml的配置、gnrt工具的使用,以及保持与上游更新的机制keeping-up-to-date.md); - 为其编写
gn构建规则(cargo.md中提到//build/rust/*.gni的模板,如rust_static_library;遇到依赖解析问题时,参考resolving-problems.md中需在gnrt_config.toml配置的其他项); - 审计其源码以确保足够安全(对应
reviews-and-audits.md:引入前需要代码审查与安全审计)。
课程还特别指出,表格中的对比皆为一般化概括,反例必然存在;但让学生理解"大多数 Rust 代码都依赖其他 Rust 库"这一点很重要,因为这既带来生态红利,也带来依赖治理成本。
六、C++/Rust 互操作:用 CXX 声明边界
6.1 #[cxx::bridge] 与 rust_static_library
Chromium 中,Rust 与 C++ 的互操作基于 CXX 工具链(仓库中携带了其源码副本,见 third_party/cxx)。基本做法(见 src/chromium/interoperability-with-cpp/using-cxx-in-chromium.md):
在 Chromium 中,我们在每个需要使用 Rust 的叶子节点(leaf-node) 定义一个独立的
#[cxx::bridge] mod。通常,每个rust_static_library对应一个这样的模块。
具体操作是在已有的 rust_static_library 目标中,在 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]的文件,而非全部源文件;allow_unsafe = true是必需的——任何 C/C++ 代码按 Rust 的安全标准来看都不"安全":与 C/C++ 来回调用可能对内存做任意操作,并危及 Rust 自身数据布局的安全性(讲师备注指出,过多的unsafe关键字会影响该关键字的信噪比,且存在争议,但严格来说,任何外部代码进入 Rust 二进制都可能引发 Rust 视角下的意外行为;另一个更具体的理由是 CXX 在后台生成的正是手写unsafe与extern "C"函数)。
生成 C++ 头文件后,可直接以常规方式包含:
#include "ui/base/my_rust_file.rs.h"
(头文件会生成在合理的位置。)同时,//base 中提供了一些工具函数用于在 Chromium C++ 类型与 CXX Rust 类型之间转换,例如 SpanToRustSlice(位于 base/containers/span_rust.h)。
6.2 cxx::bridge 模块的形态
CXX 要求整个 C++/Rust 边界都在 .rs 源文件内部的 cxx::bridge 模块中声明。课程中给出的示例片段(见 src/chromium/interoperability-with-cpp/example-bindings.md)来自仓库内 third_party/cxx/book/snippets.rs 中的 cxx_overview 代码段。讲师备注强调了几个容易误解的点:
- 虽然它看起来像普通 Rust
mod,但#[cxx::bridge]过程宏会对其做复杂处理,生成的代码远比表面看到的更精密;不过最终在你的代码里仍会得到一个名为ffi的模块; - 该机制原生支持 C++ 的
std::unique_ptr在 Rust 中的使用; - 原生支持 Rust 切片(slices)在 C++ 中的使用;
- 模块上半部分声明的是"从 C++ 调用 Rust"的方向以及 Rust 类型;
- 模块下半部分声明的是"从 Rust 调用 C++"的方向以及 C++ 类型;
- 常见误解:看起来像是 Rust 在解析 C++ 头文件,实则是误导——该头文件从不会被 Rust 解释,它只是被
#include进生成的 C++ 代码,供 C++ 编译器使用。
其余互操作细节(手写 FFI 与 CXX 的对比、错误处理、类型映射、局限性)可继续阅读 src/chromium/interoperability-with-cpp.md 及其子章节;测试侧则通过 chromium_import! 宏与 gtest interop 验证跨语言行为,见 src/chromium/testing.md。
七、当日练习线索:用 Rust 处理 UTF-8 字符串
回到 src/chromium.md 开篇的任务提示:当日课程的核心实操是调用 Rust 完成与字符串相关的"简单"任务。结合互操作章节可知,其技术路径正是本文所述的完整链路:
- 通过集中式
Cargo.toml+gnrt_config.toml引入(或复用)一个处理字符串的 crate; - 用
rust_static_library编写/包装第一方 Rust 胶水代码,并通过#[cxx::bridge]暴露给 C++; - 在 C++ 侧包含生成的
.rs.h头文件,完成调用——例如把 UTF-8 字符串传给 Rust 处理后再展示给用户。
这也呼应了课程建议:不必拘泥于演示的精确位置,可以在自己负责的展示 UTF-8 字符串的代码区域实践同样配方。
八、总结
Comprehensive Rust 的 Chromium 模块清晰勾勒了 Rust 进入 Chromium 的完整工程图景:
- 定位:Rust 用于第三方库,第一方胶水代码负责桥接既有 C++;
- 构建:以
gn/ninja+rust_static_library为主路径,同时保留cargo作为生态工具; - 引入:通过集中式
Cargo.toml声明依赖、以gnrt_config.toml标注safe/sandbox/test分组并补充许可证信息,再经gnrt生成构建规则,最后完成代码审查与安全审计; - 互操作:用
#[cxx::bridge]声明语言边界,CXX 自动生成 Rust 与 C++ 两侧代码,Chromium 提供cxx_bindings/allow_unsafe等 GN 配置项与base中的类型转换工具。
这套方法论不仅适用于本次"用 Rust 处理字符串"的练习,也适用于任何希望在 Chromium 中引入 Rust 能力的第一方与第三方场景。