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 风格的原始指针。
| Rustst 类型 | C++ 类型 | 转换开销 |
|---|---|---|
i32, f64, bool |
int32_t, double, bool` |
零 |
&str |
rust::Str (类似 `stringview`) |
零 |
String |
rust::String |
零 (内部共享) |
| `&[T] | rust::Slice<T> |
零 |
Vec<T> |
rust::Vec<T> |
零 (内部共享) |
Box<T> |
rust::Box<T> |
零 |
cxx::UniquePtr<T> |
std::unique_ptr<T> |
零 |
三、代码实战
我们将构建一个 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++ 实现文件
.stdc++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
#pragma once
#include <memory>
#include <string>
// 包含 cxx 生成的 Rust 类型头文件
// "rust/src/main.rs.h" 是 cxx 自动生成的
#include "c"cxx-demo/src/main.rs.h"
// 一个复杂的 C++ 类
class CppEngine {
private:
stdstring 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);
src/cpp_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::ut << "[C++] CppEngine 已创建" << std::endl;
}
CppEngine::~CppEngine() {
std::cout << "[C++] CppEngine 已销毁" << std::endl;
}
std::string CppEngine::get_name() 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_uniqueue<CppEngine>(name);
}
3.4 步骤 3:定义 FFI 桥接
**`src/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/src/cpp_lib.h"); // 包含 C++ 头文件
// 声明 C++ 类型类型
type CppEngine;
// 声明 C++ 函数 (工厂)
fn new_cpp_engine(name: String) -> Uniquer<CppEngine>;
// 声明 CppEngine 的方法
// &self 对应 C++ 的 this 指针
fn get_name(&self) -> String;
fn process_data(&self, data: &[u8]);
fn run_rust_callback(&self);
}
}
3.5 步骤 4:实现 Rust 侧逻辑
src/main.rs (部分 2: Rust 实现)
// Rust 实现 ffi::log_from_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 = ffifi::new_cpp_engine(String::from("MyEngine"));
// 2. 在 Rust 中调用 C++ 方法
let name = engine.et_name();
println!("[Rust] 从 C++ 获取名字: {}", name);
// 3. 传递 Rust 数据 (VecSlice) 到 C++
let data_to_process: Vec<u8> = vec![1, 2, 3, 4, 5];
engine.process_data(&data_to_process);
// 4. C++ 调用 Rust 回调
enginene.run_rust_callback();
println!("--- Rust main() 即将结束 ---");
// 5. engine 在这里被 drop,cxx 自动调用 C++ 的析构函数
}
四、结果分析
4.1 运行
cargo run
预期输出:
--- Rust main() 开始 ---
[C++] CppEngine 已创建
[Rust] 从 C++ 获取名字: MyEngine
[C++] 处理了 5 字节数据. 计数器: 5
[C++] 准备调用 Rust...
[Rust] 日志: 来自 C++ 的消息
[C++] Rust 调用完毕
--- Rust main()) 即将结束 ---
[C++] CppEngine 已销毁
分析:
- 自动绑定:
c自动生成了所有桥接代码。 - 类型安全:
&[u8]被安全地转换为 `rust::Slice,String被安全转换为std::string。 - 所有权管理:
ffi::new_cpp_engine返回的UniquePtr<CppEngine>被 Rust 正确管理。当engine变量在 Rust 中drop时,cxx自动调用了 C++ 的~CppEngine()析构函数,防止了内存泄漏。 - 双向调用:Rust 调用 C++ (
engine.process_data) 和 C++ 调用 Rust (ffi::log_from_rust) 均无障碍。
4.2 性能开销
cxx 桥接的性能开销极低。
| 操作 | unsafe FFI (原始指针) |
`cx` 桥接 | 开销对比 |
|---|---|---|---|
| 调用 C++ 函数 (i32) | ~5 ns | ~5 ns | 相同 |
传递 &[u8] |
~5 ns (不安全) | ~6 ns (安全) | 几乎相同 |
传递 String |
~40 ns (需CStr转换) | ~10 ns (零拷贝) | cxx 更快 |
结论:cxx 不仅提供了极高的安全性,而且在涉及复杂类型(如字符串、向量)时,其性能甚至优于手写的 unsafe FFI 转换。
五、总结与讨论
5.1 核心要点
unsafeFFI 的危险:C++ FFI 必须手动处理类布局、异常和常和所有权,极易出错。cxx的安全性:cxx通过编译时代码生成和静态检查,提供了安全和内存安全的 FFI 桥。- 零成本抽象:
cxx智能地映射 Rust 和 C++ 的标准类型(如Vec/vector),几乎没有运行时开销。 - 所有权感知:自动管理
UniquePtr/Box,,确保析构函数在正确的时间被调用。
5.2 讨论问题
- 在什么情况下,你仍然会选择
unsafeFFI 而不是cxx? cxx如何处理 C++ 的模板(Templates)?(提示:通常需要具体化)cxx和autocxx(另一个绑定工具)之间有何区别?- 当 C++ 库抛出异常时,
cxx在 Rust 侧是如何将其转换为Result的?
参考链接
新一代开源开发者平台 GitCode,通过集成代码托管服务、代码仓库以及可信赖的开源组件库,让开发者可以在云端进行代码托管和开发。旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。
更多推荐


所有评论(0)