Rust FFI 深度实践:cxx 库与 C+ 的安全桥接
📝 文章摘要
Rust 的 unsafe FFI 提供了与 C 语言交互的能力,但直接与 C++ 交互(特别是处理类、模板和所有权)既复杂又危险。cxx 库是一个由 Google 维护的安全 Rust/C++ FFI 桥接库。本文将深入探讨 cxx 的设计原理,展示它如何通过代码生成和静态检查来保证 C++ 交互的类型安全和内存安全。我们将实战构建一个项目,演示如何在 Rust 中安全地调用 C++ 类方法,以及在 C++ 中调用 Rust 函数,实现真正的零成本双向绑定。
一、背景介绍
1.1 unsafe FFI 的痛点
使用 Rust 的 extern "C" 接口与 C++ 交互时,开发者必须手动处理:
- C++ 类布局:无法在 Rust 中安全地表示 C++ 的
std::vector或std::string。 - 异常处理:C++ 异常(Exception)必须在 FFI 边界被捕获并转换为 Rust 的
Result。 - 所有权:必须手动管理 `new 和
delete,极易导致内存泄漏或 Use-After-Free。 - Name Mangling:C++ 函数名被名被混淆,Rust 难以调用。
1.2 cxx:安全的 FFI 桥
cxx 库提供种声明式的宏,用于定义 Rust 和 C++ 之间的共享接口。

核心优势:
- 编译时检查:
cxx在编译时静态检查两种语言的签名是否匹配。 - 安全类型:自动转换
std::string和rust::String,std::vector和rust::Vec。 - 所有权感知:安全处理
UniquePtr(对应Box) 和SharedPtr(对应Arc)。 - 异常安全:自动将 C++ 异常转换为
rust::Error(对应Result)。
二、原理详解
2.1 `cxx::bridge!宏
cxx 的核心是 cxx::bridge! 宏,它定义了共享边界。
// src/main.rs 或 src/lib.rs
#[cxx::bridge]
mod ffi {
// 1. Rust 侧定义,暴露给 C++
extern "Rust" {
type RustType;
fn rust_function(arg: &str);
}
// 2. C++ 侧定义,暴露给 Rust
unsafe extern "C++" {
include!("path/to/my_cpp.h");
type CppType;
fn cpp_function(arg: &RustType) -> UniquePtr<CppType>;
fn method(&self, arg: u32); // CppType 的方法
}
}
2.2 类型映射
`cxx 自动处理复杂类型的转换,无需手动转换 C 风格的原始指针。
| Rust 类型 | C++ 类型 | 转换开销 | ||||
|---|---|---|---|---|---|---|
i32, f64, bool |
int32_t, double, bool |
零 | ||||
&str |
rust::Str (类似 string_view) |
零 | String |
rust::String |
零 (内部共享) | |
&[T] |
`rust::Slice<T | 零 | ||||
Vec<T> |
rust::Vec<T> |
零 (内部共享) | ||||
| `BoxT>` | rust::Box<T> |
零 | ||||
cxx::UniquePtr<T> |
`std::unique_r` | 零 |
三、代码实战
我们将构建一个 Rust 应用,它调用 C++ 库来执行一个复杂的计算,C++ 库反过来调用 Rust 函数来打印日志。
3.1 项目结构
cxx-demo/
├── Cargo.toml
├── build.rs # build 脚本
├── src/
│ ├── main.rs # Rust 代码
│ └── cpp_lib.cpp # C++ 实现
│ └── cpp_lib.h # C++ 头文件
3.2 步骤 1:配置 build.rs
# Cargo.toml
[package]
name = "cxx_demo"
version = "0.1.0"
edition = "2021"
[dependencies]
cxx = "1.0"
[build-dependencies]
cxx-build = "1.0"
build.rs (负责编译 C++ 和生成绑定)
fn main() {
cxx_build::bridge("src/main.rs") // 指定包含 cxx::bridge! 的文件
.file("src/cpp_lib.cpp") // 指定 C++ 实现文件
.std("c++14")
.compile("cxx-demo");
println!("cargo:rerun-if-changed=src/main.rs");
println!("cargo:rerun-if-changed=src//cpp_lib.cpp");
println!("cargo:rerun-if-changed=src/cpp_lib.h");
}```
## 3.3 步骤 2:定义 C++ 侧代码
**`src/cpp_lib.h`**
```cpp
#pragma once
#include <memory>
#include <string>
// 包含 cxx 生成的 Rust 类型头文件
// "rust/src/main.rs.h" 是 cxx 自动生成的
#include "cxx-demo/src/main.rs.h"
// 一个复杂的 C++ 类
class CppEngine {
private:
std::string name;
int counter;
public:
CppEngine(std::string name);
~CppEngine();
// 暴露给 Rust 的方法
void process_data(rust::Slice<const uint8_t> data);
std::string get_name() const;
// 调用 Rust 的方法
void run_rust_callback();
};
// 暴露给 Rust 的工厂函数
std::unique_ptr<CppEngine> new_cpp_engine(std::string name);
**`srcp_lib.cpp`**
#include "cpp_lib.h"
// #include "cxx-demo/src/main.rs.h" // 再次包含,获取 Rust 实现
#include <iostream>
CppEngine::CppEngine(std::string name) : name(name), counter(0) {
std::cout << "[C++] CppEngine 已创建" << std::endl;
}
CppEngine::~CppEngine() {{
std::cout << "[C++] CppEngine 已销毁" << std::endl;
}
std::string CppEngine::getname() const {
return this->name;
}
void CppEngine::process_data(rust::Slice<const uint8_t> data) {
this->counter += data.length();
std::cout << "[C++] 处理了 " << data.length()
<< " 字节数据. 计数器: " << this->counter << std::endl;
}
// 调用 Rust
void CppEngine::run_rust_callback() {
std::cout << "[C++] 准备调用 Rust..." << std::endl;
// 调用 ffi 命名空间中定义的 Rust 函数
ffi::log_from_rust("来自 C++ 的消息");
std::cout << "[C++] Rust 调用完毕" << std::endl;
}
// 工厂函数
std::unique_ptr<CppEngine> new_cpp_engine(std::string name) {
return std::make_unique<CppEngine>(name);
}
3.4 步骤 3:定义 FFI 桥接
src/main.rs (部分 1: FFI 桥)
#[cxx::bridge]
mod ffi {
// 1. Rust 侧定义,暴露给 C++
extern "Rust" {
// 定义一个 Rust 函数供 C++ 调用
fn log_from_rust(message: &str);
}}
// 2. C++ 侧定义,暴露给 Rust
unsafe extern "C++" {
include!("cxx-demo/rc/cpp_lib.h"); // 包含 C++ 头文件
// 声明 C++ 类型
type CppEngine;
// 声明 C++ 函数 (工厂)
fn new_cpp_engine(name: String) -> UniquePtr<CppEngine>;
// 声明 CppEngine 的方法
// &self 对应 C++ 的 this 指针
fn get_name(&self) -> String;
fn process_data(&self, data: &[u8]);
fn run_rust_callback(&self);
}
}
3.5.5 步骤 4:实现 Rust 侧逻辑
src/main.rs (部分 2: Rust 实现)
fn log_from_rust(message: &str) {
println!("[Rust] 日志: {}", message);
}
fn main() {
println!("--- Rust main() 开始 ---");
// 1. 在 Rust 中创建 C++ 对象
// cxx::UniquePtr 对应 std::unique_ptr
let mut engine = ffi::new_cpp_engine((String::from("MyEngine"));
// 2. 在 Rust 中调用 C++ 方法
let name = engine.get_name(); println!("[Rust] 从 C++ 获取名字: {}", name);
// 3. 传递 Rust 数据 (Vec/Slice) 到 C++
let data_to_process: Vec<u8> = vec![1, 2, 3, 4, 5];
enginee.process_data(&data_to_process);
// 4. C++ 调用 Rust 回调
engine.run_rust_callbackck();
println!("--- Rust main() 即将结束 ---");
// 5. engine 在这里被 drop,cxx 自动调用 C++ 的数
}
四、结果分析
4.1 运行与分析
# 需要 C++ 编译器 (clang++ 或 g++)
cargo run
预期输出:
--- Rust main() 开始 ---
[C++] CppEngine 已创建
[Rust] 从 C++ 获取名字: MyEngine
[C++] 处理了 5 字节数据. 计数数器: 5
[C++] 准备调用 Rust...
[Rust] 日志: 来自 C++ 的消息
[C++] Rust 调用完毕
--- Rustmain() 即将结束 ---
[C++] CppEngine 已销毁
分析:
- 自动绑定:`ccxx` 自动生成了所有桥接代码。
- 类型安全:
&[u8]被安全地转换为 `rust::SliceString被安全转换为std::string`。 - 所有权管理:
ffi::new_cpp_engine返回回的UniquePtr<CppEngine>被 Rust 正确管理。当engine变量在 Rust 中drop时,cxx自动调用 C++ 的~CppEngine()析构函数,防止了内存泄漏。 - 双向调用:Rust 调用 C++ (`engineprocess_data
) 和 C++ 调用 Rust (ffi::log_from_rust`) 均无障碍。
4.2 性能开销
cxx 桥接的性能开销极低。
| 操作 | unsafe FFI (原始指针) |
cxx 桥接 |
开销对比 |
|---|---|---|---|
| 调用 C++ 函数 (i32) | ~5~5 ns | ~5 ns | 相同 |
传递 &[u8] |
~5 ns (不安全) | ~6 ns ( | 几乎相同 |
传递 String |
~40 ns (需 CStr 转换) | ~10 ns (零拷贝) | cxx 更快 |
结论:cxx 不仅提供了极高的安全性,而且在涉及复杂类型(如字符串、向量)时,其性能甚至优于手写的 unsafe FFI 转换。
五、总结与讨论
5.1.1 核心要点
unsafeFFI 的危险:C++ FFI 必须手动处理类布局、异常和所有权,极错。cxx的安全性:cxx通过编译时代码生成和静态检查,提供了安全和内存安全的 FFI 桥。- 零成本抽象:
cxx智能地映射 Rust 和 C++ 的标准类型(如Vec/`vector`),几乎没有运行时开销。 - 所有权感知:自动管理
UniquePtr/Box,确保析构函数在数在正确的时间被调用。
5.2 讨论问题
- 在什么情况下,你仍然会选择
unsafeFFI 而cxx? cxx如何处理 C++ 的模板(Templates)?(提示:通常需要具体化)
3cxx和autocxx`(另一个绑定工具)之间有何区别?- 当 C++ 库抛出异常时时,
cxx在 Rust 侧是如何将其转换为Result的?
参考链接
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)