首页
/ Comprehensive Rust 之 Android 平台 Rust 与 C++ 安全互操作:CXX Bridge 完整指南

Comprehensive Rust 之 Android 平台 Rust 与 C++ 安全互操作:CXX Bridge 完整指南

2026-09-09 18:49:57作者:范靓好Udolf

本文基于 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 生成对应定义,实现"一个桥接声明,两套语言绑定":

CXX 桥接整体架构:Bridge 模块同时向 Rust 与 C++ 两侧生成代码

该图源文件为 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 代码里真正实现 MyTypefoobar,桥接声明只是"挂牌"给 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)
        }
    }
}

两个值得强调的安全机制:

  1. 程序员不需要"保证"自己在桥接里手写的签名是准确的——CXX 会对签名执行静态断言(static assertions),确保它们与 C++ 中实际声明的签名精确对应,从根上杜绝了手写 FFI 时签名不一致的经典 bug;
  2. 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。课程给出了三点原因:

  1. std::string 不保证 Rust String 所要求的 UTF-8 不变式;
  2. 两种类型在内存中的布局不同,无法直接在语言间传递;
  3. 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) 即可优雅处理;
  • 如果异常从未声明返回 Resultextern "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.tomlsrc/main.rs(含 main() 调用 ffi::new_blobstore_client()、上传分块数据、打 tag、读回 metadata 的完整流程)。在本地执行:

cargo run -p cxx-demo   # 具体包名以 Cargo.toml 为准

说明:该示例的 C++ 侧实现(blobstore.h 等)位于 third_party/cxx/blobstore/includethird_party/cxx/blobstore/src,依赖完整的 C++ 工具链与 CXX 生成的胶水代码。Android 平台则按上一节的 genrule 方式接入 Soong 构建;本地独立验证 C++ 侧代码生成时,可在构建后查看 target/cxxbridge 目录。

参考与延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395