comprehensive-rust 实战:在 Android Soong 构建系统中用 genrule + cxxbridge 生成 CXX 互操作绑定
本篇技术指南以 comprehensive-rust 课程的 Android 章节为基础,聚焦于「在 Android 构建环境中(Soong 构建系统)将 Rust 与 C++ 通过 CXX 桥接」的关键一环:如何编写两个 genrule,分别生成 CXX 头文件(.rs.h)与 CXX 源码文件(.rs.cc),并让它们作为 cc_library_static 的输入参与构建。读完本文,你将掌握 cxxbridge 工具在 Android 构建系统中的用法、genrule 的完整配置写法、命名约定,以及如何把它接入真实的 Android.bp 模块(仓库内 third_party/cxx/blobstore 提供了可对照的完整示例)。
背景:为什么在 Android 中需要生成 CXX 绑定
在 comprehensive-rust 课程的 Android 互操作章节中,Rust 与 C++ 之间的调用通过 FFI(Foreign Function Interface)实现。而课程选用的核心工具是 CXX 生态:它通过一个 #[cxx::bridge] 属性宏声明桥接模块,自动为两侧生成匹配的类型与函数定义。
在常规的 Cargo 项目中,CXX 的生成过程由 cxx-build 这类构建脚本驱动;而在 Android 平台上,构建系统是 Soong,.bp 文件描述的模块没有 Cargo 式的构建脚本。因此课程给出了 Android 专属的解法——用两条 genrule 显式调用独立的 cxxbridge 命令行工具,把「代码生成」这一步变成构建图中的两个规则节点。这就是关联文档 android-cpp-genrules.md 的核心内容。
桥接模块本身的声明方式,参见 The Bridge Module:你需要在 Rust 源码里用 #[cxx::bridge] 标注一个模块(通常叫 ffi),其中通过 extern "Rust" 块暴露 Rust 侧类型与函数给 C++(见 Rust Bridge Declarations),通过 unsafe extern "C++" 块声明 C++ 侧类型与函数给 Rust 使用(见 C++ Bridge Declarations)。而 genrule 的作用,就是把这份声明翻译成真正可编译的 C++ 代码。
两条 genrule 的完整写法
在 Android Soong 的 Android.bp 文件中,你需要为同一个 Rust 源文件声明两条 genrule,一条生成头文件、一条生成源码文件。原文档给出的模板如下:
// Generate a C++ header containing the C++ bindings
// to the Rust exported functions in lib.rs.
genrule {
name: "libcxx_test_bridge_header",
tools: ["cxxbridge"],
cmd: "$(location cxxbridge) $(in) --header > $(out)",
srcs: ["lib.rs"],
out: ["lib.rs.h"],
}
// Generate the C++ code that Rust calls into.
genrule {
name: "libcxx_test_bridge_code",
tools: ["cxxbridge"],
cmd: "$(location cxxbridge) $(in) > $(out)",
srcs: ["lib.rs"],
out: ["lib.rs.cc"],
}
逐项拆解这两条规则:
| 属性 | 第一条 genrule | 第二条 genrule | 说明 |
|---|---|---|---|
name |
libcxx_test_bridge_header |
libcxx_test_bridge_code |
Soong 模块名,需在项目内唯一;后续被 generated_headers / generated_sources 引用 |
tools |
["cxxbridge"] |
["cxxbridge"] |
声明本规则依赖名为 cxxbridge 的工具模块,构建时该工具二进制会被提供到执行环境 |
cmd |
$(location cxxbridge) $(in) --header > $(out) |
$(location cxxbridge) $(in) > $(out) |
唯一的差别是第一条多了 --header 标志,用于输出头文件 |
srcs |
["lib.rs"] |
["lib.rs"] |
输入是包含 #[cxx::bridge] 的 Rust 源文件 |
out |
["lib.rs.h"] |
["lib.rs.cc"] |
输出文件名,遵循下述命名约定 |
这里的关键点在于两条规则之间的差异只有 --header 这一个命令行标志:
cxxbridge lib.rs --header输出 C++ 头文件,其中包含暴露给 C++ 的 Rust 函数/类型声明(对应extern "Rust"段);cxxbridge lib.rs输出 C++ 实现源码,即 Rust 侧调用 C++ 函数时需要链接进去的胶水代码(对应unsafe extern "C++"段)。
关于生成物内容的具体形态,可对照课程 Generated C++ 一节:头文件里会生成继承自 ::rust::Opaque 的不透明类型、以及 ::rust::Slice<...> 等桥接签名的 C++ 声明。
关于 cxxbridge 工具与命名约定
原文档在 <details> 中补充了两个重要的实践知识,写作配置时务必留意:
其一,cxxbridge 是独立工具。 它是一个独立的命令行工具,专门用于生成桥接模块的 C++ 侧代码,并且已经随 Android 系统内置、可作为 Soong 的 tool 使用。正因为如此,你不需要像 Cargo 项目那样引入 cxx-build 构建脚本,只需在 tools: ["cxxbridge"] 中声明依赖即可。这条 genrule 会以 $(in)(即 srcs 中的 lib.rs)为输入,把 stdout 重定向到 $(out)。
其二,命名约定并非强制。 按惯例,如果你的 Rust 源文件叫 lib.rs,那么生成的头文件应命名为 lib.rs.h、源码文件应命名为 lib.rs.cc。这个约定便于后续被 cc_library_static 的 generated_headers / generated_sources 引用时直观对应,但它不是被强制执行的规则——out 字段里的名字你可以自行指定(比如 my_header.rs.h),只要 cmd 中的重定向输出与 out 一致即可。
另外注意:既然生成的 C++ 声明与 #[cxx::bridge] 中的签名严格对应,如果修改了桥接模块,就必须重新生成并重新编译依赖它的 C++ 库;构建系统通过 genrule 的输入输出依赖关系自动保证这一点,只要 srcs 里写对了 Rust 源文件。
让生成物进入 cc_library_static:真实仓库示例
原文档指出,这两条 genrule 的输出随后会「作为 cc_library_static 的输入」。这一步的完整接线方式,仓库内的真实例子位于 third_party/cxx/blobstore/Android.bp,它演示了一个同时包含 Rust 二进制与 C++ 静态库的完整模块,摘录如下:
cc_library_static {
name: "blobstore_cpp",
srcs: ["src/blobstore.cc"],
generated_headers: [
"cxx-bridge-header",
"blobstore_bridge_header"
],
generated_sources: ["blobstore_bridge_code"],
}
genrule {
name: "blobstore_bridge_header",
tools: ["cxxbridge"],
cmd: "$(location cxxbridge) $(in) --header > $(out)",
srcs: ["src/main.rs"],
out: ["main.rs.h"],
}
genrule {
name: "blobstore_bridge_code",
tools: ["cxxbridge"],
cmd: "$(location cxxbridge) $(in) > $(out)",
srcs: ["src/main.rs"],
out: ["main.rs.cc"],
}
rust_binary {
name: "blobstore",
srcs: ["src/main.rs"],
rustlibs: ["libcxx"],
static_libs: ["blobstore_cpp"],
}
对照原文档模板,可以看到在实际项目中的对应关系:
generated_headers:cc_library_static通过它引入 genrule 输出的头文件。这里同时列出了cxx-bridge-header(CXX 自身的运行时头文件模块)与blobstore_bridge_header(我们自己的 genrule 产物main.rs.h),两者缺一不可——前者提供rust::String、rust::Slice等运行时类型定义,后者提供桥接声明。generated_sources:引入 genrule 输出的实现源码main.rs.cc,它会被编译进静态库,其中包含 Rust 侧调用 C++ 时所需的符号。rust_binary:最终的 Rust 可执行文件通过static_libs: ["blobstore_cpp"]链接上面的 C++ 静态库,并通过rustlibs: ["libcxx"]引入 CXX 的 Rust 运行时支持。
也就是说,一条完整的链路是:main.rs(#[cxx::bridge] 声明)→ 两条 genrule(cxxbridge 生成头与源码)→ cc_library_static(编译 C++ 侧实现与生成的胶水代码)→ rust_binary(链接进 Rust 程序)。你可以在 third_party/cxx/blobstore/src/main.rs 中查看被这两条 genrule 作为输入的桥接模块本体——它声明了 org::blobstore 命名空间下的共享结构体、extern "Rust" 段(如 next_chunk 函数与 MultiBuf 类型)和 unsafe extern "C++" 段(如 new_blobstore_client、put 等方法)。
实操要点与易错项
结合文档与仓库源码,给出几条在 Android 中落地时的实用提醒:
- 两条 genrule 必须成对出现。头文件与实现源码来自同一次桥接声明的两侧,缺失任何一条都会导致链接期符号缺失或头文件声明找不到实现。
--header标志不要漏。漏掉它,$(out)里得到的将是 C++ 源码而非头文件,generated_headers引入后编译必然报错。- 输出名与
$(out)保持一致。out中的文件名要和cmd重定向的$(out)一一对应;沿用lib.rs→lib.rs.h/lib.rs.cc的惯例最省心。 generated_headers记得带上cxx-bridge-header。仓库示例表明,仅引入业务 genrule 头文件是不够的,CXX 的运行时头文件模块也必须加入,否则::rust::前缀的类型定义无法解析。- Android 项目里无法用
cargo-expand预览展开结果。桥接模块的生成 Rust 代码在常规 Cargo 项目中可以用cargo expand ::ffi查看(见 The Bridge Module),但这一点不适用于 Android 工程;此时生成的 C++ 代码就是观察桥接结果的主要窗口。
与 CXX 类型映射和错误处理的关系
生成的 C++ 绑定中出现的类型并非随意的:课程 Additional Types 一节给出了 Rust 与 C++ 类型的对应表,例如 String ↔ rust::String、&str ↔ rust::Str、Vec<T> ↔ rust::Vec<T>、UniquePtr<T> ↔ std::unique_ptr<T> 等。理解这张表有助于你在阅读 genrule 生成的头文件时判断签名是否正确——例如 Rust 的 String 并不直接映射到 std::string,两者内存布局与不变量都不一致,因此桥接层需要专门类型转换。
同时,如果桥接声明中使用了返回 Result 的 C++ 函数,生成的胶水代码会负责在 C++ 侧捕获异常并转换为 Err 返回给 Rust;对未声明返回 Result 的 extern "C++" 函数,一旦抛出异常会触发 std::terminate(等价于异常穿过 noexcept 函数),详见 C++ Error Handling。这些语义由 cxxbridge 生成的代码承担,genrule 只需保证生成物被正确编译进 cc_library_static 即可。
延伸阅读
- Building in Android(本文所依据的原始文档)
- Android C++ 互操作总览 所在章节:课程将「构建 Android 工程」作为 C++ 互操作链路的一环,与 bridge 声明、生成的 C++ 等内容串联讲解
- 可对照的完整工程:third_party/cxx/blobstore/Android.bp 与 third_party/cxx/blobstore/src/main.rs
- Android 互操作的入门语境见 Interoperability,Android 章节总入口见 android.md
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