Comprehensive Rust 之 Android 平台 Rust 与 C++ 安全互操作:CXX Bridge 完整指南
本文基于 Google Android 团队维护的 Rust 课程 comprehensive-rust 中 Android 互操作章节 展开。课程本身(Cargo 工作区 + mdBook 构建)收录了 CXX crate 的完整桥接示例,本文以该章节为主线,结合仓库内 blobstore 完整示例 与 CXX 教材代码片段,从 Bridge 声明、类型映射、错误处理到 Android 构建,给出可运行的实战方案,帮助读者在 Android 平台上以内存安全方式打通 Rust 与 C++。
为什么需要 CXX Bridge:整体思路
Rust 与 C++ 之间的互操作,最大的挑战不在语法,而在于两类语言的内存模型与所有权语义差异:C++ 的引用没有借用检查器约束,异常与 Rust 的 Result、panic 属于两套不同的错误传播体系,std::string 与 Rust String 的布局也不一致。CXX crate 提供了一种基于类型安全的 "桥接" 方案——它不要求程序员手写容易出错的 extern "C" 薄封装,而是通过声明式描述自动生成两个语言侧的胶水代码。
CXX 的整体思路如下图所示,#[cxx::bridge] 声明的桥接模块位于中间,向左为 C++ 生成类型与函数声明,向右为 Rust 生成对应定义,实现"一个桥接声明,两套语言绑定":
该图源文件为 overview.svg,收录于 src/android/interoperability/cpp.md 章节顶部。
桥接模块:#[cxx::bridge] 的声明式核心
CXX 依赖一个对两侧要暴露的函数签名的描述。这个描述放在一个 Rust 模块中,并用 #[cxx::bridge] 属性宏标注,通常命名为 ffi:
#[cxx::bridge(namespace = "org::blobstore")]
mod ffi {
// 共享结构体,字段对两种语言可见
struct BlobMetadata {
size: usize,
tags: Vec<String>,
}
// Rust 类型与签名,暴露给 C++(extern "Rust")
extern "Rust" {
type MultiBuf;
fn next_chunk(buf: &mut MultiBuf) -> &[u8];
}
// C++ 类型与签名,暴露给 Rust(extern "C++")
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;
}
}
上面的代码来自仓库中的完整示例 third_party/cxx/blobstore/src/main.rs,是一个真实可编译的 CXX 演示项目(blobstore),涵盖三种声明形式:
- 共享结构体(
struct BlobMetadata):字段对两种语言可见; extern "Rust"块:声明 Rust 侧的类型与函数,暴露给 C++ 调用;unsafe extern "C++"块:声明 C++ 侧的类型与函数,暴露给 Rust 调用。
桥接模块的运作机制
- 桥接模块通常放在 crate 内的
ffi模块中(如上面示例),作为一个集中的互操作边界; - 从桥接模块中的声明出发,CXX 会生成匹配的 Rust 与 C++ 类型/函数定义,将这些条目同时暴露给两种语言;
- 要查看生成的 Rust 代码,可用
cargo-expand展开过程宏:对于大多数示例使用cargo expand ::ffi只展开ffi模块(注意:Android 工程中此方式不适用,因为构建流程由 Soong 管理,不走 cargo 展开); - 要查看生成的 C++ 代码,去
target/cxxbridge目录下查看。
Rust 侧桥接声明(extern "Rust"):把 Rust 能力暴露给 C++
课程通过 CXX 教材代码片段 展示了 extern "Rust" 的三种典型形态:
#[cxx::bridge]
mod ffi {
extern "Rust" {
type MyType; // 不透明类型(opaque type)
fn foo(&self); // `MyType` 上的方法
fn bar() -> Box<MyType>; // 自由函数
}
}
struct MyType(i32);
impl MyType {
fn foo(&self) {
println!("{}", self.0);
}
}
fn bar() -> Box<MyType> {
Box::new(MyType(123))
}
关键语义:
extern "Rust"中声明的条目,引用的是父模块作用域内已存在的条目——也就是说,你必须在桥接模块之外的 crate 代码里真正实现MyType、foo、bar,桥接声明只是"挂牌"给 C++ 看;- CXX 代码生成器利用
extern "Rust"段生成一个 C++ 头文件,内含对应的 C++ 声明。生成的头文件路径与包含桥接的 Rust 源文件路径相同,只是扩展名变为.rs.h。例如lib.rs对应生成lib.rs.h。
C++ 侧桥接声明(extern "C++"):把 C++ 能力暴露给 Rust
以 blobstore 为例,unsafe extern "C++" 声明:
#[cxx::bridge]
mod ffi {
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;
}
}
这段声明会(大致)生成如下 Rust 代码:
#[repr(C)]
pub struct BlobstoreClient {
_private: ::cxx::private::Opaque,
}
pub fn new_blobstore_client() -> ::cxx::UniquePtr<BlobstoreClient> {
extern "C" {
#[link_name = "org$blobstore$cxxbridge1$new_blobstore_client"]
fn __new_blobstore_client() -> *mut BlobstoreClient;
}
unsafe { ::cxx::UniquePtr::from_raw(__new_blobstore_client()) }
}
impl BlobstoreClient {
pub fn put(&self, parts: &mut MultiBuf) -> u64 {
extern "C" {
#[link_name = "org$blobstore$cxxbridge1$BlobstoreClient$put"]
fn __put(
_: &BlobstoreClient,
parts: *mut ::cxx::core::ffi::c_void,
) -> u64;
}
unsafe {
__put(self, parts as *mut MultiBuf as *mut ::cxx::core::ffi::c_void)
}
}
}
两个值得强调的安全机制:
- 程序员不需要"保证"自己在桥接里手写的签名是准确的——CXX 会对签名执行静态断言(static assertions),确保它们与 C++ 中实际声明的签名精确对应,从根上杜绝了手写 FFI 时签名不一致的经典 bug;
unsafe extern "C++"块允许你声明"从 Rust 调用是安全"的 C++ 函数——把"要不要标记 unsafe"的决定权交还给桥接作者:只有那些契约清晰、可从 Rust 安全调用的 C++ 函数才放进unsafe块。
注意:课程代码片段中同时展示了
extern "C++"与unsafe extern "C++"两种写法(见 snippets.rs),unsafe前缀表示"C++ 侧函数本身不抛异常、内存安全由调用方信任"。
共享类型与共享枚举:跨语言的数据契约
共享结构体
#[cxx::bridge]
mod ffi {
#[derive(Clone, Debug, Hash)]
struct PlayingCard {
suit: Suit,
value: u8, // A=1, J=11, Q=12, K=13
}
enum Suit {
Clubs,
Diamonds,
Hearts,
Spades,
}
}
使用限制与注意事项:
- 只支持 C 风格(unit)枚举,即无字段、无关联数据的纯变体枚举;
- 共享类型上可
#[derive()]的 trait 数量有限(上例为Clone, Debug, Hash)。对应的功能也会为 C++ 代码生成:例如 derive 了Hash,CXX 也会为对应的 C++ 类型生成std::hash的实现,保证两侧哈希语义一致。
共享枚举的生成结果
桥接声明(snippets.rs):
#[cxx::bridge]
mod ffi {
enum Suit {
Clubs,
Diamonds,
Hearts,
Spades,
}
}
生成的 Rust(snippets.rs):
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(transparent)]
pub struct Suit {
pub repr: u8,
}
#[allow(non_upper_case_globals)]
impl Suit {
pub const Clubs: Self = Suit { repr: 0 };
pub const Diamonds: Self = Suit { repr: 1 };
pub const Hearts: Self = Suit { repr: 2 };
pub const Spades: Self = Suit { repr: 3 };
}
生成的 C++(snippets.cc):
enum class Suit : uint8_t {
Clubs = 0,
Diamonds = 1,
Hearts = 2,
Spades = 3,
};
为什么 Rust 侧生成的是 struct 而不是 enum? 课程给出了关键解释:C++ 中 enum class 持有"未列出的变体值"并不构成 UB(例如 Suit(42) 是合法的),而 Rust 的 enum 对非法判别值持零容忍态度。为了保证两种语言的表示行为完全一致,CXX 在 Rust 侧把枚举生成为一个包装数值的透明结构体,从而在边界两侧对任意整数判别值都保持同样的语义。这一点在 Rust 侧使用共享枚举时需格外留意:模式匹配不再适用,你需要通过 Suit::Clubs.repr 之类的常量与字段来比较。
附加类型映射:String、Vec、UniquePtr 等核心对应关系
除共享类型外,CXX 还内置了一组跨语言类型映射,可直接用于共享结构体的字段以及 extern 函数的参数与返回值:
| Rust 类型 | C++ 类型 |
|---|---|
String |
rust::String |
&str |
rust::Str |
CxxString |
std::string |
&[T] / &mut [T] |
rust::Slice |
Box<T> |
rust::Box<T> |
UniquePtr<T> |
std::unique_ptr<T> |
Vec<T> |
rust::Vec<T> |
CxxVector<T> |
std::vector<T> |
特别注意:Rust 的 String 并不会直接映射到 std::string。课程给出了三点原因:
std::string不保证 RustString所要求的 UTF-8 不变式;- 两种类型在内存中的布局不同,无法直接在语言间传递;
std::string需要移动构造函数(move constructor),这与 Rust 的移动语义不匹配,因此std::string无法按值传给 Rust。
这也是为什么表格中 Rust 侧访问 std::string 需要 CxxString、访问 std::vector 需要 CxxVector<T>——它们本质是"指向 C++ 侧对象的视图"。
错误处理:Result 与异常的双向桥接
CXX 将 Rust 的 Result 与 C++ 的异常机制在桥接层做了双向翻译,两侧各有一个方向。
Rust 侧:Result 翻译为 C++ 异常
#[cxx::bridge]
mod ffi {
extern "Rust" {
fn fallible(depth: usize) -> Result<String>;
}
}
fn fallible(depth: usize) -> anyhow::Result<String> {
if depth == 0 {
return Err(anyhow::Error::msg("fallible1 requires depth > 0"));
}
Ok("Success!".into())
}
语义要点:
- 返回
Result的 Rust 函数在 C++ 侧被翻译为异常; - 抛出的异常总是
rust::Error类型,它主要暴露获取错误消息字符串的能力,错误消息来自错误类型的Display实现; - 从 Rust 向 C++ panic 展开(panic unwinding)会立即终止进程——Rust 的 panic 绝不能越过 C++ 边界。
C++ 侧:C++ 异常翻译为 Result
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
include!("example/include/example.h");
fn fallible(depth: usize) -> Result<String>;
}
}
fn main() {
if let Err(err) = ffi::fallible(99) {
eprintln!("Error: {}", err);
process::exit(1);
}
}
语义要点:
- 声明为返回
Result的 C++ 函数,在 C++ 侧捕获抛出的任何异常,并将其作为Err值返回给调用它的 Rust 函数,Rust 侧用if let Err(err)即可优雅处理; - 如果异常从未声明返回
Result的extern "C++"函数中抛出,程序会调用 C++ 的std::terminate——行为等价于同一个异常穿过一个noexcept的 C++ 函数,即直接终止进程。因此:任何可能抛异常的 C++ 函数,都必须在桥接声明中返回Result,否则等于埋下进程崩溃的地雷。
生成的 C++ 代码:从桥接到头文件
以 blobstore 中 extern "Rust" 声明为例:
#[cxx::bridge]
mod ffi {
extern "Rust" {
type MultiBuf;
fn next_chunk(buf: &mut MultiBuf) -> &[u8];
}
}
大致生成的 C++ 代码如下(完整内容见 generated-cpp.md):
struct MultiBuf final : public ::rust::Opaque {
~MultiBuf() = delete;
private:
friend ::rust::layout;
struct layout {
static ::std::size_t size() noexcept;
static ::std::size_t align() noexcept;
};
};
::rust::Slice<::std::uint8_t const> next_chunk(::org::blobstore::MultiBuf &buf) noexcept;
可以观察到几个重要设计:
MultiBuf是一个不透明类型(Opaque):C++ 侧只能持有其引用/指针,不能构造、析构(~MultiBuf() = delete;),生命周期完全由 Rust 侧管理;- 类型按命名空间
org::blobstore生成(对应桥接声明里的namespace = "org::blobstore"),避免符号冲突; next_chunk被生成为noexcept的 C++ 自由函数,签名与 Rust 侧fn next_chunk(buf: &mut MultiBuf) -> &[u8]一一对应,&[u8]映射为rust::Slice<const uint8_t>。
在 Android 中构建:Soong genrule 集成
在 Android 平台(Soong 构建系统)上使用 CXX,课程给出了明确的构建方案:创建两个 genrule——一个生成 CXX 头文件,一个生成 CXX 源文件,然后将它们作为输入接入 cc_library_static:
// 生成一个 C++ 头文件,包含指向 lib.rs 中 Rust 导出函数的 C++ 绑定
genrule {
name: "libcxx_test_bridge_header",
tools: ["cxxbridge"],
cmd: "$(location cxxbridge) $(in) --header > $(out)",
srcs: ["lib.rs"],
out: ["lib.rs.h"],
}
// 生成 Rust 调用的 C++ 代码
genrule {
name: "libcxx_test_bridge_code",
tools: ["cxxbridge"],
cmd: "$(location cxxbridge) $(in) > $(out)",
srcs: ["lib.rs"],
out: ["lib.rs.cc"],
}
要点说明:
cxxbridge是生成桥接模块 C++ 侧的独立命令行工具,内置于 Android 平台,并作为 Soong tool 直接可用;- 按惯例,若 Rust 源文件是
lib.rs,则头文件命名为lib.rs.h、源文件命名为lib.rs.cc(此命名约定不强制,可自定义out); - 两个 genrule 的产物最终作为输入接入
cc_library_static,把生成的 C++ 侧绑定编译进原生库,从而与 Rust 侧(同样由 CXX 生成的 Rust 绑定)在链接层对接。
在本地快速体验:完整可运行示例
仓库内 blobstore 是完整的 CXX 示例项目(位于 third_party/cxx/blobstore),包含 Cargo.toml 与 src/main.rs(含 main() 调用 ffi::new_blobstore_client()、上传分块数据、打 tag、读回 metadata 的完整流程)。在本地执行:
cargo run -p cxx-demo # 具体包名以 Cargo.toml 为准
说明:该示例的 C++ 侧实现(
blobstore.h等)位于 third_party/cxx/blobstore/include 与 third_party/cxx/blobstore/src,依赖完整的 C++ 工具链与 CXX 生成的胶水代码。Android 平台则按上一节的 genrule 方式接入 Soong 构建;本地独立验证 C++ 侧代码生成时,可在构建后查看target/cxxbridge目录。
参考与延伸阅读
- 本章节入口:src/android/interoperability/cpp.md(含 overview 架构图)
- 章节子页:
- 配套示例与源码:
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00