Rust 与 C++、Python 互操作完全指南:从 C ABI、CXX 到 PyO3
文章目录
- Rust 与 C++、Python 互操作完全指南:从 C ABI、CXX 到 PyO3
- 一、先建立整体认识:到底是谁调用谁
- 二、跨语言调用的底层:ABI 而不是源代码
- 三、方案总览
- 四、方案一:基于 C ABI 的手动 FFI
- 五、完整示例一:Rust 调用 C++ 类
- 六、完整示例二:C++ 调用 Rust
- 七、bindgen 与 cbindgen 到底做了什么
- 八、方案二:使用 CXX 实现现代 Rust/C++ 互操作
- 九、完整 CXX 双向调用示例
- 十、CXX 如何处理所有权
- 十一、CXX 是否完全安全、完全零开销
- 十二、方案三:Crubit 当前处于什么状态
- 十三、Rust 与 Python 交互的两种基本方向
- 十四、方案四:使用 PyO3 让 Python 调用 Rust
- 十五、完整示例三:Python 调用 Rust
- 十六、Python 调用 Rust 时的类型映射
- 十七、PyO3 与 Python GIL
- 十八、方案五:Python 使用 ctypes 调用 Rust C ABI
- 十九、完整示例四:Rust 调用 Python
- 二十、Rust/C++/Python 三语言混合架构
- 二十一、字符串应该如何传递
- 二十二、结构体布局
- 二十三、对象所有权规则
- 二十四、错误和异常边界
- 二十五、回调函数设计
- 二十六、线程安全问题
- 二十七、构建系统如何组织
- 二十八、不同方案的准确对比
- 二十九、工程选型建议
- 三十、推荐的分层架构
- 三十一、常见错误清单
- 三十二、最终结论
Rust 与 C++、Python 互操作完全指南:从 C ABI、CXX 到 PyO3
前言
在实际工程中,很少有大型系统能够一次性切换到某一种语言。
常见情况包括:
- 已有大量 C++ 基础设施,希望用 Rust 重写部分安全敏感模块;
- 核心算法使用 Rust 实现,但上层仍然是 C++、Qt 或 Chromium;
- Python 负责业务编排,Rust 负责图像处理、压缩、解析或高性能计算;
- Rust 程序需要调用 NumPy、PyTorch 或现有 Python 脚本;
- C++、Rust、Python 三种语言同时存在于一个工程中。
这些需求的本质并不是“让一种语言看懂另一种语言的语法”,而是建立一个稳定的跨语言边界:
┌───────────────────────────────────────────────────────────┐
│ 上层业务代码 │
├─────────────────┬─────────────────┬───────────────────────┤
│ C++ │ Rust │ Python │
├─────────────────┴─────────────────┴───────────────────────┤
│ ABI、类型转换、所有权、错误模型、线程模型、构建与链接 │
├───────────────────────────────────────────────────────────┤
│ 操作系统与机器代码 │
└───────────────────────────────────────────────────────────┘
Rust 官方将这种跨语言调用称为 FFI,即 Foreign Function Interface。Rust 可以直接声明和调用兼容 ABI 的外部函数,但面对 C++ 类、模板、异常和标准库容器时,通常还需要额外的包装层。
本文将依次讲清楚:
- Rust 与 C++ 交互的底层原理;
- 手动 C ABI 的双向调用;
bindgen与cbindgen的职责;- CXX 的双向类型安全桥接;
- Crubit 当前适合什么场景;
- Python 如何调用 Rust;
- Rust 如何嵌入并调用 Python;
- 字符串、容器、对象、异常、线程和内存如何跨越语言边界;
- 不同项目应该如何选型。
一、先建立整体认识:到底是谁调用谁
“Rust 与 C++ 交互”至少包含两个不同方向:
方向一:Rust 调用 C++
Rust
│
│ FFI 声明或自动生成绑定
▼
C/C++ 接口
│
▼
已有 C++ 实现
方向二:C++ 调用 Rust
C++
│
│ Rust 导出的 C ABI / CXX 接口
▼
Rust 动态库或静态库
│
▼
Rust 核心实现
Rust 与 Python 同样包含两个方向:
方向三:Python 调用 Rust
Python
│
│ import 扩展模块
▼
PyO3 / CPython C API
│
▼
Rust 高性能实现
方向四:Rust 调用 Python
Rust 可执行程序
│
│ 嵌入 Python 解释器
▼
PyO3 / CPython C API
│
▼
Python 模块、函数和对象
因此,不能只说“使用 bindgen”或者“使用 PyO3”,而要先回答三个问题:
- 谁是主程序?
- 谁调用谁?
- 边界上需要传递哪些类型?
二、跨语言调用的底层:ABI 而不是源代码
2.1 什么是 ABI
API 描述的是源代码层面的调用方式,例如:
int add(int a, int b);
ABI 描述的是编译后机器代码层面的约定,包括:
- 函数参数放在什么寄存器或栈位置;
- 返回值如何传递;
- 函数符号叫什么;
- 结构体字段如何排列;
- 栈由谁清理;
- 异常如何展开;
- 虚函数表和 RTTI 如何组织;
- 标准库对象如何布局。
同样一个 C++ 函数:
int add(int a, int b);
编译器可能把它修饰为类似:
_Z3addii
这就是名称修饰,即 name mangling。
使用:
extern "C" int add(int a, int b);
可以要求 C++ 编译器按照 C linkage 导出符号,使符号名称和调用约定更容易被其他语言使用。
但必须明确:
extern "C"只解决函数链接名称和 C 调用约定问题,不会把 C++ 类、模板、异常或std::string自动变成 C 类型。
下面这些接口依然不适合作为通用 C ABI:
extern "C" std::string get_name(); // 不合适
extern "C" std::vector<int> get_values(); // 不合适
extern "C" MyCppClass create_object(); // 不合适
因为这些类型的布局和生命周期仍然受 C++ ABI、编译器和标准库实现控制。
2.2 为什么 C ABI 是最常见的跨语言基线
C ABI 的优势不是“功能强大”,而是足够简单和稳定。
通常可以直接跨边界传递:
固定宽度整数:
int8_t / uint8_t
int16_t / uint16_t
int32_t / uint32_t
int64_t / uint64_t
浮点数:
float
double
原始指针:
T*
const T*
数组:
pointer + length
简单结构体:
字段固定,并确保两侧布局一致
不透明对象:
void* 或 opaque handle
通常不应直接通过原始 C ABI 传递:
Rust String
Rust Vec<T>
Rust Result<T, E>
Rust trait object
std::string
std::vector<T>
std::unique_ptr<T>
C++ class
C++ exception
Python object
这些复杂类型必须被转换、包装或者交给专门的桥接库处理。
三、方案总览
| 工具或方案 | 方向 | 抽象层级 | 主要用途 |
|---|---|---|---|
| 手动 C ABI | 双向 | 最低 | 小型接口、稳定 SDK、完全控制 ABI |
bindgen | C/C++ → Rust | 较低 | 根据 C/C++ 头文件生成 Rust 声明 |
cbindgen | Rust → C/C++ | 较低 | 根据 Rust 导出接口生成 C/C++ 头文件 |
| CXX | 双向 | 较高 | 新项目中的 Rust/C++ 类型安全桥接 |
| Crubit | 双向 | 较高 | Bazel 和大型 C++/Rust 混合代码库 |
| PyO3 + maturin | Python → Rust | 高 | Rust 编写 Python 扩展模块 |
| PyO3 嵌入模式 | Rust → Python | 高 | Rust 程序调用 Python |
ctypes / CFFI | Python → Rust | 较低 | Python 调用 Rust 导出的 C ABI |
四、方案一:基于 C ABI 的手动 FFI
手动 C ABI 是最基础的方案,也是理解其他工具的前提。
它的核心思想是:
Rust 不直接理解 C++ 类
│
▼
C++ 提供一层 C 风格包装接口
│
▼
Rust 只调用这层稳定接口
五、完整示例一:Rust 调用 C++ 类
下面实现一个 C++ Calculator 类,但不直接把这个类暴露给 Rust,而是通过不透明句柄进行访问。
5.1 工程结构
rust_calls_cpp_cabi/
├── Cargo.toml
├── build.rs
├── cpp
│ ├── include
│ │ └── calculator_c.h
│ └── src
│ └── calculator.cpp
└── src
└── main.rs
5.2 C ABI 头文件
文件:cpp/include/calculator_c.h
#ifndef CALCULATOR_C_H
#define CALCULATOR_C_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// Rust 不需要知道 CalculatorHandle 的内部结构。
// Rust 只能持有并传回这个指针。
typedef struct CalculatorHandle CalculatorHandle;
typedef int32_t CalculatorStatus;
#define CALCULATOR_OK ((CalculatorStatus)0)
#define CALCULATOR_NULL_POINTER ((CalculatorStatus)1)
#define CALCULATOR_CPP_EXCEPTION ((CalculatorStatus)2)
// 创建一个 C++ Calculator 对象。
CalculatorHandle* calculator_create(void);
// 释放对象。
void calculator_destroy(CalculatorHandle* handle);
// 对数组求和。
// 计算结果通过 out_sum 返回,函数返回状态码。
CalculatorStatus calculator_sum(
const CalculatorHandle* handle,
const int32_t* values,
size_t length,
int64_t* out_sum
);
#ifdef __cplusplus
}
#endif
#endif
这里没有向 Rust 暴露:
class Calculator;
而是只暴露:
typedef struct CalculatorHandle CalculatorHandle;
这叫作 opaque handle,即不透明句柄。
Rust 只知道它是一个类型,但不知道对象大小、字段、虚函数表或继承关系。
5.3 C++ 实现
文件:cpp/src/calculator.cpp
#include "calculator_c.h"
#include <cstdint>
#include <new>
namespace {
class Calculator {
public:
std::int64_t sum(const std::int32_t* values, std::size_t length) const {
std::int64_t result = 0;
for (std::size_t i = 0; i < length; ++i) {
result += values[i];
}
return result;
}
};
} // namespace
// C ABI 中暴露的句柄,内部真正持有 C++ 对象。
struct CalculatorHandle {
Calculator calculator;
};
extern "C" CalculatorHandle* calculator_create(void) {
try {
return new CalculatorHandle{};
} catch (...) {
// 绝不能让 C++ 异常直接穿过 C ABI。
return nullptr;
}
}
extern "C" void calculator_destroy(CalculatorHandle* handle) {
delete handle;
}
extern "C" CalculatorStatus calculator_sum(
const CalculatorHandle* handle,
const std::int32_t* values,
std::size_t length,
std::int64_t* out_sum
) {
if (handle == nullptr || out_sum == nullptr) {
return CALCULATOR_NULL_POINTER;
}
if (values == nullptr && length != 0) {
return CALCULATOR_NULL_POINTER;
}
try {
*out_sum = handle->calculator.sum(values, length);
return CALCULATOR_OK;
} catch (...) {
// 将 C++ 异常转换为错误码。
return CALCULATOR_CPP_EXCEPTION;
}
}
这里有一个关键原则:
C++ 异常
│
│ catch (...)
▼
错误码
│
▼
Rust Result
不要让 C++ 异常直接进入普通的 Rust extern "C" 调用边界。Rust 官方文档指出,跨越不允许展开的 ABI 边界可能导致进程中止或未定义行为;原始 FFI 应当在边界处拦截异常和 panic。
5.4 使用 build.rs 编译 C++
文件:build.rs
fn main() {
println!("cargo:rerun-if-changed=cpp/include/calculator_c.h");
println!("cargo:rerun-if-changed=cpp/src/calculator.cpp");
cc::Build::new()
.cpp(true)
.file("cpp/src/calculator.cpp")
.include("cpp/include")
.flag_if_supported("-std=c++17")
.flag_if_supported("/std:c++17")
.compile("calculator_cpp");
}
5.5 Cargo.toml
[package]
name = "rust_calls_cpp_cabi"
version = "0.1.0"
edition = "2024"
[build-dependencies]
cc = "1"
Rust 2024 Edition 要求外部函数声明块写成:
unsafe extern "C" {
}
同时,no_mangle 等影响全局符号的属性需要写成 #[unsafe(no_mangle)]。
5.6 Rust 声明及安全封装
文件:src/main.rs
use std::ptr::NonNull;
// 这个类型只用于表示 C++ 侧的不透明对象。
// Rust 不会创建它,也不会读取它的字段。
#[repr(C)]
struct CalculatorHandle {
_private: [u8; 0],
}
type CalculatorStatus = i32;
const CALCULATOR_OK: CalculatorStatus = 0;
const CALCULATOR_NULL_POINTER: CalculatorStatus = 1;
const CALCULATOR_CPP_EXCEPTION: CalculatorStatus = 2;
unsafe extern "C" {
fn calculator_create() -> *mut CalculatorHandle;
fn calculator_destroy(handle: *mut CalculatorHandle);
fn calculator_sum(
handle: *const CalculatorHandle,
values: *const i32,
length: usize,
out_sum: *mut i64,
) -> CalculatorStatus;
}
#[derive(Debug)]
enum CalculatorError {
CreateFailed,
NullPointer,
CppException,
UnknownStatus(i32),
}
// 对原始指针进行 RAII 封装。
struct Calculator {
raw: NonNull<CalculatorHandle>,
}
impl Calculator {
fn new() -> Result<Self, CalculatorError> {
// SAFETY:
// calculator_create 没有参数,并且返回一个对象指针或 nullptr。
let raw = unsafe { calculator_create() };
let raw = NonNull::new(raw).ok_or(CalculatorError::CreateFailed)?;
Ok(Self { raw })
}
fn sum(&self, values: &[i32]) -> Result<i64, CalculatorError> {
let mut result = 0_i64;
let values_ptr = if values.is_empty() {
std::ptr::null()
} else {
values.as_ptr()
};
// SAFETY:
// 1. self.raw 在 Calculator 生命周期内有效;
// 2. values_ptr 在调用期间有效;
// 3. length 与切片长度一致;
// 4. result 是有效的可写地址。
let status = unsafe {
calculator_sum(
self.raw.as_ptr(),
values_ptr,
values.len(),
&mut result,
)
};
match status {
CALCULATOR_OK => Ok(result),
CALCULATOR_NULL_POINTER => Err(CalculatorError::NullPointer),
CALCULATOR_CPP_EXCEPTION => Err(CalculatorError::CppException),
value => Err(CalculatorError::UnknownStatus(value)),
}
}
}
impl Drop for Calculator {
fn drop(&mut self) {
// SAFETY:
// raw 由 calculator_create 创建,并且只在这里释放一次。
unsafe {
calculator_destroy(self.raw.as_ptr());
}
}
}
fn main() -> Result<(), CalculatorError> {
let calculator = Calculator::new()?;
let values = [1, 2, 3, 4, 5];
let result = calculator.sum(&values)?;
println!("sum = {result}");
Ok(())
}
预期输出:
sum = 15
5.7 unsafe 被限制在哪里
整个程序并没有到处使用 unsafe。
真正的不安全操作只有:
┌────────────────────────────┐
│ 原始 extern 函数声明 │
├────────────────────────────┤
│ 创建对象时调用一次 │
├────────────────────────────┤
│ 调用求和函数 │
├────────────────────────────┤
│ Drop 中释放对象 │
└────────────────────────────┘
外部使用者只调用:
let calculator = Calculator::new()?;
let result = calculator.sum(&values)?;
这就是 Rust FFI 的正确设计模式:
使用少量经过审计的
unsafe代码,构造一个普通的安全 Rust API。
unsafe 不会关闭借用检查器,而是表示这段代码包含编译器无法自动证明的安全条件,需要开发者自行保证。
六、完整示例二:C++ 调用 Rust
现在反过来,把 Rust 编译成动态库,供 C++ 调用。
6.1 工程结构
cpp_calls_rust/
├── Cargo.toml
├── include
│ └── rust_math.h
├── cpp
│ └── main.cpp
└── src
└── lib.rs
6.2 Cargo.toml
[package]
name = "rust_math"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "staticlib"]
两种库类型的区别:
cdylib
└── 生成供 C/C++、Python 等加载的动态库
staticlib
└── 生成包含 Rust 运行时和依赖的静态库
在不同平台上,动态库名称通常为:
Linux: librust_math.so
macOS: librust_math.dylib
Windows: rust_math.dll
6.3 Rust 导出函数
文件:src/lib.rs
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::slice;
const RUST_MATH_OK: i32 = 0;
const RUST_MATH_NULL_POINTER: i32 = 1;
const RUST_MATH_PANIC: i32 = 2;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_sum_i32(
values: *const i32,
length: usize,
out_sum: *mut i64,
) -> i32 {
if out_sum.is_null() {
return RUST_MATH_NULL_POINTER;
}
if values.is_null() && length != 0 {
return RUST_MATH_NULL_POINTER;
}
let execution = catch_unwind(AssertUnwindSafe(|| {
let values: &[i32] = if length == 0 {
&[]
} else {
// SAFETY:
// 调用者必须保证 values 指向至少 length 个有效 i32。
unsafe { slice::from_raw_parts(values, length) }
};
let sum = values
.iter()
.map(|value| i64::from(*value))
.sum::<i64>();
// SAFETY:
// 已检查 out_sum 非空,调用者还必须保证其可写。
unsafe {
*out_sum = sum;
}
}));
match execution {
Ok(()) => RUST_MATH_OK,
Err(_) => RUST_MATH_PANIC,
}
}
这里使用:
catch_unwind(...)
将可能发生的 Rust panic 限制在 Rust 内部。
边界模型为:
Rust panic
│
│ catch_unwind
▼
错误码 2
│
▼
C++ 正常处理错误
即使普通 extern "C" 函数在 panic 穿越边界时通常会中止进程,也不应把“自动中止”当作库接口的错误处理策略。更合理的做法仍然是在导出边界主动捕获 panic,或者将生产库配置为 panic = "abort" 并明确其行为。
6.4 C/C++ 头文件
文件:include/rust_math.h
#ifndef RUST_MATH_H
#define RUST_MATH_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define RUST_MATH_OK 0
#define RUST_MATH_NULL_POINTER 1
#define RUST_MATH_PANIC 2
int32_t rust_sum_i32(
const int32_t* values,
size_t length,
int64_t* out_sum
);
#ifdef __cplusplus
}
#endif
#endif
6.5 C++ 调用代码
文件:cpp/main.cpp
#include "rust_math.h"
#include <cstdint>
#include <iostream>
#include <vector>
int main() {
const std::vector<std::int32_t> values{10, 20, 30, 40};
std::int64_t result = 0;
const std::int32_t status = rust_sum_i32(
values.data(),
values.size(),
&result
);
if (status != RUST_MATH_OK) {
std::cerr << "rust_sum_i32 failed, status = "
<< status
<< '\n';
return 1;
}
std::cout << "sum = " << result << '\n';
return 0;
}
6.6 Linux 编译流程
首先编译 Rust 动态库:
cargo build --release
然后编译 C++:
g++ cpp/main.cpp \
-std=c++17 \
-Iinclude \
-Ltarget/release \
-lrust_math \
-Wl,-rpath,'$ORIGIN/target/release' \
-o cpp_demo
运行:
./cpp_demo
预期输出:
sum = 100
七、bindgen 与 cbindgen 到底做了什么
很多初学者容易把这两个工具理解为“自动完成所有互操作”。
实际上,它们的职责都更有限。
7.1 bindgen:把 C/C++ 声明翻译成 Rust 声明
调用方向:
Rust
│
▼
C/C++ 库
bindgen 会读取:
// library.h
int add(int a, int b);
生成类似:
unsafe extern "C" {
pub fn add(a: i32, b: i32) -> i32;
}
官方文档说明,bindgen 使用 Clang 解析 C/C++ 头文件,并生成 Rust FFI 类型和声明。它能够处理一部分 C++ 特性,但复杂类、构造函数、析构函数、重载运算符和模板通常无法得到符合 Rust 习惯的安全接口。
7.2 build.rs 中使用 bindgen
添加依赖:
cargo add --build bindgen
cargo add --build cc
示例 build.rs:
use std::env;
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-changed=cpp/include/calculator_c.h");
cc::Build::new()
.cpp(true)
.file("cpp/src/calculator.cpp")
.include("cpp/include")
.flag_if_supported("-std=c++17")
.flag_if_supported("/std:c++17")
.compile("calculator_cpp");
let bindings = bindgen::Builder::default()
.header("cpp/include/calculator_c.h")
.allowlist_function("calculator_.*")
.allowlist_type("CalculatorHandle")
.allowlist_var("CALCULATOR_.*")
.generate()
.expect("failed to generate bindings");
let out_dir = PathBuf::from(
env::var("OUT_DIR").expect("OUT_DIR is not set"),
);
bindings
.write_to_file(out_dir.join("bindings.rs"))
.expect("failed to write bindings");
}
然后在 Rust 中引入:
mod ffi {
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
工程结构就变成:
C/C++ 头文件
│
▼
bindgen
│
▼
自动生成的原始 Rust FFI
│
▼
手写安全 Rust 包装层
关键点是:
bindgen 生成的是底层声明,不是完整的安全 Rust API。
7.3 为什么复杂 C++ 库经常仍然需要包装层
假设原始 C++ API 是:
class Document {
public:
explicit Document(const std::string& path);
std::vector<std::string> pages() const;
template<typename T>
T get_property(const std::string& key) const;
};
这里包含:
- 构造函数;
std::string;std::vector<std::string>;- 成员函数;
- 模板;
- C++ 对象生命周期;
- 潜在异常。
即使工具能够解析一部分声明,Rust 侧也很难直接得到自然、安全、稳定的接口。
更合理的方式是先提供包装:
extern "C" {
DocumentHandle* document_open(
const char* path,
size_t path_length
);
void document_close(DocumentHandle* document);
int32_t document_page_count(
const DocumentHandle* document,
size_t* out_count
);
}
然后再让 bindgen 处理这个干净的 C API。
7.4 cbindgen:根据 Rust 接口生成 C/C++ 头文件
调用方向:
C/C++
│
▼
Rust 库
cbindgen 会读取 Rust 中公开的 FFI 导出声明,并生成 C 或 C++ 头文件。它生成的是接口声明,不会替你设计对象所有权、错误码或线程安全模型。
安装:
cargo install cbindgen
生成头文件:
cbindgen \
--crate rust_math \
--lang c \
--output include/rust_math.h
典型工作流:
Rust extern "C" 导出函数
│
▼
cbindgen
│
▼
生成 .h 头文件
│
▼
CMake / Visual Studio / C++ 调用方
因此:
bindgen = 头文件 → Rust 声明
cbindgen = Rust 导出声明 → 头文件
它们并不是互相替代,而是方向相反。
八、方案二:使用 CXX 实现现代 Rust/C++ 互操作
CXX 是专门为 Rust 和 C++ 双向调用设计的桥接库。
它的核心是:
#[cxx::bridge]
mod ffi {
// 跨语言接口定义
}
CXX 根据这个 bridge 同时生成:
- Rust 侧桥接代码;
- C++ 侧桥接代码;
- C++ 头文件;
- 类型转换和调用胶水。
CXX 支持共享结构体、枚举、Rust 类型、C++ 不透明类型、UniquePtr、SharedPtr、Rust Vec、C++ std::vector、切片、字符串和显式错误传播等机制。
Chromium 的 Rust FFI 文档也曾明确推荐使用 CXX,并支持在 Rust 和 C++ 之间双向调用。
九、完整 CXX 双向调用示例
这个示例同时演示:
- Rust 调用 C++ 类;
- C++ 回调 Rust 函数;
- Rust 字符串与 C++ 字符串交互;
- Rust 切片与 C++
rust::Slice交互。
9.1 工程结构
rust_cpp_cxx/
├── Cargo.toml
├── build.rs
├── cpp
│ ├── include
│ │ └── greeter.h
│ └── src
│ └── greeter.cpp
└── src
└── main.rs
9.2 Cargo.toml
[package]
name = "rust_cpp_cxx"
version = "0.1.0"
edition = "2024"
[dependencies]
cxx = "1"
[build-dependencies]
cxx-build = "1"
9.3 bridge 定义
文件:src/main.rs
#[cxx::bridge(namespace = "demo")]
mod ffi {
unsafe extern "C++" {
include!("cpp/include/greeter.h");
type Greeter;
// C++ 工厂函数。
fn new_greeter(prefix: &str) -> UniquePtr<Greeter>;
// C++ 成员函数。
fn greet(self: &Greeter, name: &str) -> String;
// C++ 函数,它会回调 Rust 中的 rust_sum。
fn call_rust_sum(values: &[i32]) -> i64;
}
extern "Rust" {
fn rust_sum(values: &[i32]) -> i64;
}
}
fn rust_sum(values: &[i32]) -> i64 {
values
.iter()
.map(|value| i64::from(*value))
.sum()
}
fn main() {
let greeter = ffi::new_greeter("Hello");
let greeter = greeter
.as_ref()
.expect("C++ returned a null Greeter");
let message = greeter.greet("Rust");
println!("{message}");
let values = [10, 20, 30];
let result = ffi::call_rust_sum(&values);
println!("sum from Rust callback = {result}");
}
9.4 C++ 头文件
文件:cpp/include/greeter.h
#pragma once
#include "rust/cxx.h"
#include <cstdint>
#include <memory>
#include <string>
namespace demo {
class Greeter {
public:
explicit Greeter(std::string prefix);
rust::String greet(rust::Str name) const;
private:
std::string prefix_;
};
std::unique_ptr<Greeter> new_greeter(rust::Str prefix);
std::int64_t call_rust_sum(
rust::Slice<const std::int32_t> values
);
} // namespace demo
这里使用了 CXX 提供的 C++ 类型:
rust::Str
rust::String
rust::Slice<const T>
注意:
rust::Str
不保证以 \0 结尾,因此不能直接当作 C 字符串使用。
必须使用:
name.data()
name.size()
9.5 C++ 实现
文件:cpp/src/greeter.cpp
#include "cpp/include/greeter.h"
// CXX 根据 src/main.rs 生成的头文件。
// 包名和源文件路径会参与生成该路径。
#include "rust_cpp_cxx/src/main.rs.h"
#include <cstdint>
#include <memory>
#include <string>
namespace demo {
Greeter::Greeter(std::string prefix)
: prefix_(std::move(prefix)) {
}
rust::String Greeter::greet(rust::Str name) const {
std::string result = prefix_;
result += ", ";
result.append(name.data(), name.size());
result += "!";
return rust::String(result);
}
std::unique_ptr<Greeter> new_greeter(rust::Str prefix) {
return std::make_unique<Greeter>(
std::string(prefix.data(), prefix.size())
);
}
std::int64_t call_rust_sum(
rust::Slice<const std::int32_t> values
) {
// rust_sum 是 extern "Rust" 中声明的 Rust 函数。
// CXX 会为 C++ 生成对应声明和调用胶水。
return rust_sum(values);
}
} // namespace demo
调用链如下:
Rust main
│
├── new_greeter()
│ │
│ ▼
│ C++ Greeter
│
├── Greeter::greet()
│ │
│ ▼
│ C++ 拼接字符串
│
└── call_rust_sum()
│
▼
C++ 桥接函数
│
▼
Rust rust_sum()
9.6 build.rs
fn main() {
println!("cargo:rerun-if-changed=src/main.rs");
println!("cargo:rerun-if-changed=cpp/include/greeter.h");
println!("cargo:rerun-if-changed=cpp/src/greeter.cpp");
cxx_build::bridge("src/main.rs")
.file("cpp/src/greeter.cpp")
.include(".")
.flag_if_supported("-std=c++17")
.flag_if_supported("/std:c++17")
.compile("rust_cpp_cxx_bridge");
}
运行:
cargo run
预期输出:
Hello, Rust!
sum from Rust callback = 60
十、CXX 如何处理所有权
CXX 并不是简单地把 C++ 类型按字节复制到 Rust。
它会根据类型语义提供不同表示。
10.1 C++ unique_ptr
C++:
std::unique_ptr<Greeter>
Rust:
UniquePtr<Greeter>
表示对象由 std::unique_ptr 管理,最终仍由 C++ 删除器释放。
CXX 当前只支持使用默认删除器的 std::unique_ptr<T>。对于 Rust 不透明对象,通常应使用 Rust Box<T> 而不是 C++ unique_ptr。
10.2 C++ string
C++:
std::string
Rust:
CxxString
Rust 一般不能直接按值持有 CxxString,而是通过:
&CxxString
Pin<&mut CxxString>
UniquePtr<CxxString>
访问。
这是因为 std::string 可能包含指向自身内部存储的指针,而 Rust 普通按值移动无法自动满足 C++ 对象的地址稳定性要求。
10.3 Rust String
Rust:
String
C++:
rust::String
其内存由 Rust 分配器和 Rust 类型规则管理。
C++ 不应该使用:
delete
free
释放其中的数据,而应该让 rust::String 自身析构。
10.4 数组和切片
Rust:
&[i32]
C++:
rust::Slice<const std::int32_t>
它们本质上都包含:
数据指针 + 元素数量
切片通常不需要额外序列化,但调用期间必须保证原始数据依然有效。
十一、CXX 是否完全安全、完全零开销
不能这样绝对描述。
更准确的说法是:
- CXX 会静态检查 bridge 中的很多类型和签名;
- 它能避免大量手写胶水代码;
- 它不需要 RPC、JSON 或通用序列化层;
- 切片和引用等场景可以直接借用数据;
- 部分字符串转换仍然可能进行复制、分配或 UTF-8 校验;
- C++ 实现依然可能违反生命周期、空指针、线程安全等契约;
- 原始指针接口仍然需要声明为
unsafe。
因此,CXX 提供的是:
比手动 C ABI 更强的静态约束和类型映射,而不是把任意 C++ 代码自动变成安全 Rust。
十二、方案三:Crubit 当前处于什么状态
Crubit 是 Google 开发的 C++/Rust 双向绑定生成器。
它的目标是根据已有 C++ 或 Rust API 自动生成高保真绑定,减少像 CXX bridge 这样的重复声明。
Crubit 当前已经不适合简单概括为“整个项目仍然完全处于实验阶段”。官方文档区分了多个能力等级:
supported
└── 面向明确支持 FFI 的库,提供一般可用能力
wrapper
└── 不稳定、需要 allowlist,主要用于高接触核心库
experimental
└── 内部实验能力,可能频繁变化
官方说明 supported 特性用于一般可用的互操作能力,而 wrapper 和 experimental 仍然具有明显的不稳定性和使用限制。
Crubit 更适合:
- Bazel 工程;
- 大型 Google 风格 C++ 代码库;
- 需要自动为大量目标生成绑定;
- 可以控制构建图和目标注解;
- 团队愿意跟进工具特性变化。
对于普通 Cargo + CMake 项目,CXX 或稳定 C ABI 通常仍然更容易落地。
十三、Rust 与 Python 交互的两种基本方向
Python 调用 Rust
Python 业务代码
│
│ import rust_module
▼
Python 扩展模块
│
▼
Rust 算法实现
适合:
- 图像处理;
- PDF 解析;
- 压缩算法;
- 加密;
- 文本解析;
- 数值计算;
- Python 性能热点重写。
Rust 调用 Python
Rust 主程序
│
│ 嵌入 Python 解释器
▼
Python 模块
│
▼
Python 函数、对象和生态库
适合:
- Rust 应用内置脚本系统;
- Rust 调用 NumPy 或机器学习代码;
- 复用已有 Python 业务逻辑;
- 使用 Python 作为插件语言;
- Rust 负责系统层,Python 负责动态扩展。
CPython 官方 C API 本身同时支持“编写扩展模块”和“把 Python 嵌入其他应用”两种模式。
十四、方案四:使用 PyO3 让 Python 调用 Rust
PyO3 是 Rust 与 Python 交互的主流高层库。
它支持:
- Rust 函数暴露为 Python 函数;
- Rust 结构体暴露为 Python 类;
- Rust 错误转换为 Python 异常;
- Python 对象转换为 Rust 类型;
- Rust 调用 Python 模块和函数;
- 嵌入 Python 解释器。
PyO3 官方同时支持编写 Python 原生扩展模块和从 Rust 程序中运行 Python。当前稳定文档为 0.29 系列,并推荐结合 maturin 构建 Python 包。
十五、完整示例三:Python 调用 Rust
我们实现一个 Python 模块:
import rust_py_math
rust_py_math.checked_div(10, 2)
counter = rust_py_math.Counter(10)
counter.add(5)
15.1 工程结构
rust_py_math/
├── Cargo.toml
├── pyproject.toml
├── src
│ └── lib.rs
└── test.py
15.2 Cargo.toml
[package]
name = "rust-py-math"
version = "0.1.0"
edition = "2024"
[lib]
name = "rust_py_math"
crate-type = ["cdylib"]
[dependencies]
pyo3 = {
version = "0.29",
features = ["abi3-py38"]
}
cdylib 表示生成可供 Python 加载的动态库。
abi3-py38 表示使用 CPython Stable ABI,并将最低 Python 版本设为 3.8。这样通常可以减少不同 Python 小版本所需的 wheel 数量。maturin 官方教程同样使用 cdylib 和 abi3 配置构建扩展模块。
不需要兼容多个 Python 小版本,或者需要使用某个 Python 版本特有的 API 时,可以不启用 abi3-py38。
15.3 pyproject.toml
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
15.4 Rust 实现
文件:src/lib.rs
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
#[pyfunction]
fn checked_div(left: f64, right: f64) -> PyResult<f64> {
if right == 0.0 {
return Err(PyValueError::new_err(
"right operand must not be zero",
));
}
Ok(left / right)
}
#[pyfunction]
fn dot(left: Vec<f64>, right: Vec<f64>) -> PyResult<f64> {
if left.len() != right.len() {
return Err(PyValueError::new_err(format!(
"vector length mismatch: {} != {}",
left.len(),
right.len(),
)));
}
let result = left
.iter()
.zip(right.iter())
.map(|(lhs, rhs)| lhs * rhs)
.sum();
Ok(result)
}
#[pyclass]
struct Counter {
value: i64,
}
#[pymethods]
impl Counter {
#[new]
#[pyo3(signature = (initial = 0))]
fn new(initial: i64) -> Self {
Self { value: initial }
}
fn add(&mut self, delta: i64) -> i64 {
self.value += delta;
self.value
}
fn reset(&mut self) {
self.value = 0;
}
#[getter]
fn value(&self) -> i64 {
self.value
}
fn __repr__(&self) -> String {
format!("Counter(value={})", self.value)
}
}
#[pymodule]
fn rust_py_math(module: &Bound<'_, PyModule>) -> PyResult<()> {
module.add_function(wrap_pyfunction!(checked_div, module)?)?;
module.add_function(wrap_pyfunction!(dot, module)?)?;
module.add_class::<Counter>()?;
Ok(())
}
PyO3 通过:
#[pyfunction]
将 Rust 函数注册为 Python 函数,通过:
#[pyclass]
#[pymethods]
将 Rust 类型注册为 Python 类。
15.5 构建环境
使用标准虚拟环境:
python -m venv .venv
Linux 或 macOS:
source .venv/bin/activate
Windows PowerShell:
.venv\Scripts\Activate.ps1
安装 maturin:
python -m pip install -U maturin
也可以使用 uv:
uv venv
uv pip install maturin
15.6 本地开发安装
maturin develop --release
maturin develop 会编译 Rust 扩展,并把它安装到当前 Python 虚拟环境。maturin build 则会生成可分发的 wheel 文件。
15.7 Python 测试代码
文件:test.py
import rust_py_math
def main() -> None:
result = rust_py_math.checked_div(10.0, 4.0)
print("division:", result)
dot_result = rust_py_math.dot(
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
)
print("dot:", dot_result)
counter = rust_py_math.Counter(10)
print(counter)
counter.add(5)
print("counter value:", counter.value)
try:
rust_py_math.checked_div(10.0, 0.0)
except ValueError as error:
print("caught:", error)
if __name__ == "__main__":
main()
运行:
python test.py
预期输出:
division: 2.5
dot: 32.0
Counter(value=10)
counter value: 15
caught: right operand must not be zero
错误转换流程:
Rust
Err(PyValueError::new_err(...))
│
▼
PyO3
│
▼
Python ValueError
与原始 C ABI 不同,PyO3 可以直接把 Rust PyResult<T> 转换成 Python 异常模型。
十六、Python 调用 Rust 时的类型映射
常见类型映射大致如下:
| Rust | Python |
|---|---|
i32、i64、u64 | int |
f32、f64 | float |
bool | bool |
String、&str | str |
Vec<T> | list |
HashMap<K, V> | dict |
Option<T> | T 或 None |
PyResult<T> | 返回值或 Python 异常 |
#[pyclass] struct | Python 类 |
但类型可转换不代表没有开销。
例如:
fn process(values: Vec<f64>)
意味着 Python 列表需要被遍历并转换成新的 Rust Vec<f64>。
对于大型二进制数据,应考虑:
- Python buffer protocol;
- NumPy 数组;
memoryview;bytes;- 共享内存;
- 专用 NumPy/PyO3 集成库。
否则,跨边界复制可能比算法本身更耗时。
十七、PyO3 与 Python GIL
传统 CPython 使用全局解释器锁约束 Python 对象访问。
如果 Rust 函数执行长时间的纯 Rust 计算,同时不需要访问 Python 对象,应当临时与 Python 解释器分离:
use pyo3::prelude::*;
#[pyfunction]
fn expensive_sum(py: Python<'_>, upper: u64) -> u64 {
py.detach(|| {
(0..upper).sum()
})
}
含义是:
进入 Rust 函数
│
▼
暂时 detach Python 线程状态
│
▼
执行纯 Rust 计算
│
▼
重新 attach
│
▼
返回 Python
PyO3 当前使用 Python::attach 表示线程已连接到解释器,使用 Python::detach 让不操作 Python 对象的原生代码释放解释器执行权。这既适用于传统 GIL 构建,也有助于当前 free-threaded Python 的吞吐。
不能在 detach 闭包中继续使用依赖当前 Python<'py> 生命周期的 Python 对象。
十八、方案五:Python 使用 ctypes 调用 Rust C ABI
PyO3 不是唯一方案。
Rust 也可以导出普通 C ABI 动态库,然后由 Python ctypes 加载。
调用结构:
Python ctypes
│
▼
Rust extern "C" 动态库
适合:
- 接口数量很少;
- 不希望依赖 PyO3;
- 已经存在稳定 C ABI;
- 同一个动态库还要提供给 C++、C#、Java 等语言;
- 不需要把 Rust 类型包装成 Python 类。
18.1 Rust 动态库
Cargo.toml:
[package]
name = "rust_ctypes_math"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
src/lib.rs:
use std::slice;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_dot_f64(
left: *const f64,
right: *const f64,
length: usize,
out_result: *mut f64,
) -> i32 {
if out_result.is_null() {
return 1;
}
if length != 0 && (left.is_null() || right.is_null()) {
return 1;
}
let left_values = if length == 0 {
&[]
} else {
// SAFETY:
// Python 调用方必须传入至少 length 个 f64。
unsafe { slice::from_raw_parts(left, length) }
};
let right_values = if length == 0 {
&[]
} else {
// SAFETY:
// Python 调用方必须传入至少 length 个 f64。
unsafe { slice::from_raw_parts(right, length) }
};
let result = left_values
.iter()
.zip(right_values.iter())
.map(|(lhs, rhs)| lhs * rhs)
.sum();
// SAFETY:
// 已检查 out_result 非空。
unsafe {
*out_result = result;
}
0
}
编译:
cargo build --release
18.2 Python ctypes 调用
import ctypes
import platform
from pathlib import Path
def library_path() -> Path:
root = Path(__file__).parent / "target" / "release"
system = platform.system()
if system == "Windows":
return root / "rust_ctypes_math.dll"
if system == "Darwin":
return root / "librust_ctypes_math.dylib"
return root / "librust_ctypes_math.so"
library = ctypes.CDLL(str(library_path()))
library.rust_dot_f64.argtypes = [
ctypes.POINTER(ctypes.c_double),
ctypes.POINTER(ctypes.c_double),
ctypes.c_size_t,
ctypes.POINTER(ctypes.c_double),
]
library.rust_dot_f64.restype = ctypes.c_int32
def dot(left: list[float], right: list[float]) -> float:
if len(left) != len(right):
raise ValueError("vector length mismatch")
array_type = ctypes.c_double * len(left)
left_array = array_type(*left)
right_array = array_type(*right)
result = ctypes.c_double()
status = library.rust_dot_f64(
left_array,
right_array,
len(left),
ctypes.byref(result),
)
if status != 0:
raise RuntimeError(f"rust_dot_f64 failed: {status}")
return result.value
print(dot([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]))
预期输出:
32.0
18.3 ctypes 与 PyO3 的区别
| 对比项 | ctypes | PyO3 |
|---|---|---|
| 接口层级 | C ABI | Python 原生扩展 |
| Python 类 | 需要手动封装 | #[pyclass] |
| 异常 | 手动状态码 | PyResult |
| 类型转换 | 手动配置 | 自动转换较多 |
| 包分发 | 手动处理动态库 | maturin 构建 wheel |
| 其他语言复用 | 容易 | 主要面向 Python |
| 适合接口规模 | 小 | 中大型 |
十九、完整示例四:Rust 调用 Python
现在让 Rust 成为主程序,并在进程内启动 Python 解释器。
19.1 Cargo.toml
[package]
name = "rust_calls_python"
version = "0.1.0"
edition = "2024"
[dependencies]
pyo3 = {
version = "0.29",
features = ["auto-initialize"]
}
在部分 Linux 环境中,需要安装 Python 开发库:
sudo apt install python3-dev
PyO3 官方说明,嵌入模式需要能够链接到 Python 共享库,auto-initialize 可以自动初始化解释器。
19.2 调用 Python 标准库
文件:src/main.rs
use pyo3::prelude::*;
use pyo3::types::PyModule;
fn main() -> PyResult<()> {
Python::attach(|py| {
let math = PyModule::import(py, "math")?;
let sqrt: f64 = math
.getattr("sqrt")?
.call1((81.0,))?
.extract()?;
println!("sqrt(81) = {sqrt}");
Ok(())
})
}
调用链:
Rust
│
├── import("math")
│
├── getattr("sqrt")
│
├── call1((81.0,))
│
└── extract::<f64>()
PyO3 使用带 'py 生命周期的 Python<'py> token,证明当前线程已经附着到 Python 解释器,并将 Python 对象的有效期绑定到该解释器上下文。
19.3 执行自定义 Python 代码
use pyo3::prelude::*;
use pyo3::types::PyModule;
fn main() -> PyResult<()> {
Python::attach(|py| {
let module = PyModule::from_code(
py,
c"
def transform(values):
return [value * value + 1 for value in values]
",
c"embedded_script.py",
c"embedded_script",
)?;
let result: Vec<i64> = module
.getattr("transform")?
.call1((vec![1_i64, 2, 3, 4],))?
.extract()?;
println!("result = {result:?}");
Ok(())
})
}
预期输出:
result = [2, 5, 10, 17]
PyModule::import 可用于调用已安装模块,PyModule::from_code 可以从代码字符串创建模块。后者会编译并执行传入代码,因此不能用于不可信输入。
二十、Rust/C++/Python 三语言混合架构
在大型项目中,可以同时使用三种语言。
例如:
┌────────────────────────────────────────────┐
│ Python 业务层 │
│ 流程编排、模型调用、脚本、数据分析 │
└──────────────────────┬─────────────────────┘
│ PyO3
▼
┌────────────────────────────────────────────┐
│ Rust 核心层 │
│ 安全解析、压缩、并发、内存管理、算法 │
└──────────────────────┬─────────────────────┘
│ CXX / C ABI
▼
┌────────────────────────────────────────────┐
│ C++ 基础设施层 │
│ PDFium、OpenCV、Qt、Chromium、旧有 SDK │
└────────────────────────────────────────────┘
一个具体场景可能是:
Python
│
│ 调用批处理接口
▼
Rust
│
├── 并发调度
├── 文件校验
├── 错误隔离
│
│ CXX
▼
C++
│
├── PDFium 页面渲染
├── FreeType 字体处理
└── 现有图像算法
此时应尽量减少跨边界次数。
不推荐:
Python 每处理一个像素调用一次 Rust
Rust 每处理一个字符调用一次 C++
推荐:
Python 一次提交整批任务
Rust 一次提交整页或整个文档
C++ 一次返回批量结果
跨语言调用本身可能很快,但频繁的类型转换、解释器状态切换、错误检查和小对象分配会迅速放大成本。
二十一、字符串应该如何传递
21.1 C ABI 字符串
传统方式:
const char* text
但必须明确:
- 是否以
\0结尾; - 是否允许包含内部
\0; - 使用 UTF-8、GBK 还是本地编码;
- 内存由谁拥有;
- 指针有效到什么时候。
更稳妥的二进制接口:
const uint8_t* data,
size_t length
它明确表示:
一段字节 + 字节数量
Rust 侧再根据约定处理:
let bytes = slice::from_raw_parts(data, length);
let text = std::str::from_utf8(bytes)
.map_err(...)?;
21.2 返回字符串
不要让一侧分配、另一侧随意释放。
错误设计:
extern "C" char* get_message();
因为调用者不知道:
应该 free?
应该 delete[]?
应该调用专用释放函数?
内存来自 Rust allocator?
更合理的设计有三种。
调用方提供缓冲区
int get_message(
char* buffer,
size_t capacity,
size_t* required_size
);
库提供专用释放函数
char* create_message();
void destroy_message(char* message);
返回借用视图
int get_message_view(
const char** data,
size_t* length
);
但必须明确返回数据的有效期。
二十二、结构体布局
Rust 默认结构体布局不是稳定 C ABI。
不要直接假设:
struct Point {
x: i32,
y: i32,
}
一定与 C++ 对应。
应该使用:
#[repr(C)]
pub struct Point {
pub x: i32,
pub y: i32,
}
C++:
struct Point {
std::int32_t x;
std::int32_t y;
};
#[repr(C)] 要求 Rust 按照与 C 兼容的字段排列和对齐策略表示结构体。
但即使使用 repr(C),仍要避免随意放入:
String
Vec<T>
Option<String>
Box<T>
&str
trait object
因为这些字段本身不是通用 C ABI 类型。
二十三、对象所有权规则
跨语言对象必须明确回答:
谁创建?
谁拥有?
谁可以借用?
谁负责释放?
可以在哪个线程释放?
一个合理规则是:
C++ 创建的对象
└── C++ 释放
Rust 创建的对象
└── Rust 释放
Python 创建的对象
└── Python 引用计数管理
不要这样做:
Rust malloc/new 一个对象
C++ 使用另一套运行时 free/delete
C++ new 一个对象
Rust 把它误认为 Box<T>
Python PyObject*
Rust 忽略引用计数长期保存
23.1 推荐的 opaque handle 模式
create()
│
▼
Handle*
│
├── operation_1(handle)
├── operation_2(handle)
└── operation_3(handle)
│
▼
destroy(handle)
Rust 侧使用 Drop:
impl Drop for Wrapper {
fn drop(&mut self) {
unsafe {
native_destroy(self.raw.as_ptr());
}
}
}
C++ 侧使用 RAII:
struct RustObjectDeleter {
void operator()(RustObject* object) const noexcept {
rust_object_destroy(object);
}
};
using RustObjectPtr =
std::unique_ptr<RustObject, RustObjectDeleter>;
二十四、错误和异常边界
三种语言的错误模型不同:
C
└── 错误码、errno、输出参数
C++
└── 返回值、std::error_code、异常
Rust
└── Result<T, E>、panic
Python
└── exception
因此必须进行显式转换。
24.1 C++ 到 Rust
C++ exception
│
▼
catch (...)
│
▼
status code / CXX Result
│
▼
Rust Result
24.2 Rust 到 C++
Rust Result
│
▼
错误码 + out 参数
│
▼
C++ std::error_code / expected / exception
24.3 Rust 到 Python
Rust PyResult<T>
│
▼
PyO3
│
▼
Python exception
24.4 Python 到 Rust
Python exception
│
▼
PyErr
│
▼
Rust PyResult<T>
CXX 也提供跨语言 fallible function 支持,可以将 Rust 错误信息暴露给 C++,并将受支持的 C++ 异常转换为 Rust 错误。
二十五、回调函数设计
C ABI 可以通过函数指针实现回调。
C 头文件:
typedef void (*ProgressCallback)(
int32_t progress,
void* user_data
);
void process_document(
ProgressCallback callback,
void* user_data
);
Rust:
type ProgressCallback = unsafe extern "C" fn(
progress: i32,
user_data: *mut std::ffi::c_void,
);
这里的 user_data 用来携带调用方上下文。
结构如下:
Rust closure / C++ object
│
▼
user_data 指针
│
▼
C ABI callback(progress, user_data)
需要特别处理:
- 回调对象生命周期;
- 回调可能在哪个线程执行;
- 回调期间能否重入库;
- panic 或异常不能跨边界;
- 销毁时不得仍有异步回调运行。
二十六、线程安全问题
一个对象能否跨线程使用,必须由边界契约明确规定。
C++ 类型可能:
- 不是线程安全的;
- 只能在创建线程销毁;
- 内部依赖线程局部状态;
- 依赖 GUI 线程;
- 依赖 COM apartment;
- 依赖 Python 解释器状态。
不要因为持有的是一个裸指针,就在 Rust 中直接:
unsafe impl Send for ForeignObject {}
unsafe impl Sync for ForeignObject {}
Send 和 Sync 是 unsafe trait。错误实现后,其他 Rust 代码会默认相信该类型满足线程安全契约,最终可能产生数据竞争或未定义行为。
只有在明确确认外部库线程模型后,才能实现对应 trait。
二十七、构建系统如何组织
27.1 Cargo 驱动 C++ 构建
适合 Rust 是主工程:
Cargo
│
├── build.rs
├── cc
├── cxx-build
└── bindgen
优点:
cargo build一条命令完成;- 依赖管理简单;
- 适合 Rust 主程序。
27.2 CMake 驱动 Rust 构建
适合已有大型 C++ 工程:
CMake
│
├── add_custom_command(cargo build ...)
├── imported library
└── target_link_libraries(...)
或者使用专门的 Cargo/CMake 集成方案。
27.3 双层构建
大型项目通常采用:
顶层构建系统
│
├── 构建 C++
├── 调用 Cargo
├── 收集 Rust 库
├── 生成绑定
├── 配置 rpath
└── 打包动态库
必须统一:
- Debug / Release;
- 目标架构;
- C/C++ runtime;
- 静态或动态链接;
- Sanitizer;
- LTO;
- Windows CRT;
- 异常和 RTTI 配置;
- Rust panic 策略。
二十八、不同方案的准确对比
| 方案 | 安全性 | 自动化 | 类型体验 | 构建难度 | 典型场景 |
|---|---|---|---|---|---|
| 手动 C ABI | 取决于封装质量 | 低 | 低 | 中 | 稳定 SDK、小接口 |
| bindgen | 原始层较低 | 高 | 较低 | 中 | Rust 调用现有 C API |
| cbindgen | 原始层较低 | 高 | 较低 | 中 | C++ 调用 Rust C API |
| CXX | 较高 | 中 | 高 | 中 | 新建 Rust/C++ 混合模块 |
| Crubit | 较高 | 高 | 高 | 较高 | Bazel、大型代码库 |
| PyO3 + maturin | 高 | 高 | 高 | 低到中 | Python 调用 Rust |
| PyO3 嵌入 | 高 | 中 | 高 | 中 | Rust 调用 Python |
| ctypes | 较低 | 低 | 低 | 低 | Python 调少量 C ABI |
二十九、工程选型建议
场景一:只有几个简单 C++ 函数
例如:
int compress_file(...);
int verify_document(...);
选择:
手动 C ABI
没有必要引入复杂桥接框架。
场景二:Rust 调用成熟 C 库
例如:
- SQLite;
- libcurl;
- FFmpeg C API;
- zlib;
- OpenSSL C API。
选择:
bindgen + 安全 Rust 包装层
如果社区已经存在维护良好的 -sys crate 和安全包装 crate,应优先使用现有实现。
场景三:Rust 调用复杂 C++ 类库
例如:
- PDFium;
- Qt;
- Chromium;
- 自有大型 C++ SDK。
选择顺序通常为:
CXX
│
├── 无法表达的部分增加 C++ wrapper
│
└── 必要时局部使用 bindgen
不建议让 bindgen 直接承担全部复杂 C++ API。
场景四:现有 C++ 项目引入 Rust 核心模块
选择:
稳定公共 SDK:
C ABI + cbindgen
内部紧密集成:
CXX
Bazel 大型工程:
评估 Crubit
场景五:Python 性能热点使用 Rust 重写
选择:
PyO3 + maturin
这是最自然的 Python 扩展开发方式。
场景六:同一个 Rust 库同时供 C++ 和 Python 使用
推荐把业务逻辑与语言绑定分层:
┌─────────────────────────────────────┐
│ rust_core:纯 Rust 核心 │
├─────────────────────────────────────┤
│ rust_c_api:C ABI / cbindgen │
├─────────────────────────────────────┤
│ rust_python:PyO3 │
└─────────────────────────────────────┘
目录结构:
workspace/
├── Cargo.toml
├── rust_core
│ └── src/lib.rs
├── rust_c_api
│ └── src/lib.rs
└── rust_python
└── src/lib.rs
不要在核心算法中直接混入大量:
#[pyfunction]
#[unsafe(no_mangle)]
#[cxx::bridge]
否则核心逻辑会被绑定层污染,难以测试和复用。
三十、推荐的分层架构
┌──────────────────────────────────────┐
│ C++ / Python API │
├──────────────────────────────────────┤
│ Language Adapter │
│ C ABI / CXX / PyO3 / type conversion │
├──────────────────────────────────────┤
│ Safe Rust Facade │
│ 参数校验、错误转换、所有权封装 │
├──────────────────────────────────────┤
│ Core Business Logic │
│ 纯 Rust 或纯 C++,不依赖语言绑定 │
└──────────────────────────────────────┘
例如:
// 纯 Rust 核心逻辑。
pub fn calculate_dot(
left: &[f64],
right: &[f64],
) -> Result<f64, CoreError> {
// ...
}
PyO3 适配层:
#[pyfunction]
fn dot(
left: Vec<f64>,
right: Vec<f64>,
) -> PyResult<f64> {
calculate_dot(&left, &right)
.map_err(convert_core_error_to_python)
}
C ABI 适配层:
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dot_f64(
left: *const f64,
right: *const f64,
length: usize,
out_result: *mut f64,
) -> i32 {
// 指针校验
// 构造切片
// 调用 calculate_dot
// 转换状态码
}
CXX 适配层:
#[cxx::bridge]
mod ffi {
extern "Rust" {
fn dot(values1: &[f64], values2: &[f64]) -> Result<f64>;
}
}
这样核心算法只维护一份。
三十一、常见错误清单
错误一:直接返回 Rust String 给 C++
#[unsafe(no_mangle)]
pub extern "C" fn get_name() -> String {
String::from("Rust")
}
错误原因:
String不是稳定 C ABI 类型;- C++ 不知道如何析构;
- 布局不属于接口契约。
错误二:返回局部字符串指针
#[unsafe(no_mangle)]
pub extern "C" fn get_name() -> *const u8 {
let value = String::from("Rust");
value.as_ptr()
}
函数返回后:
value 被释放
│
▼
返回指针悬空
错误三:C++ 异常穿过 extern “C”
extern "C" int process() {
throw std::runtime_error("failed");
}
必须在边界内部捕获。
错误四:Rust panic 穿过 FFI
#[unsafe(no_mangle)]
pub extern "C" fn process() {
panic!("failed");
}
库接口应当捕获、转换或采用明确的 abort 策略。
错误五:一侧创建,另一侧错误释放
Rust CString::into_raw()
│
▼
C++ delete[]
这是错误的分配器配对。
正确方式是提供:
rust_string_free(...)
由 Rust 自己重建并释放对象。
错误六:没有设置 ctypes argtypes
如果不设置:
library.function.argtypes
library.function.restype
ctypes 可能使用错误的参数和返回值解释方式,尤其在 64 位指针、浮点数和结构体场景中会产生严重问题。
错误七:把所有调用都标成安全
外部函数签名本身无法证明:
- 指针有效;
- 长度正确;
- 对象未释放;
- 调用线程正确;
- 外部实现遵守契约。
因此原始绑定一般应保持 unsafe,再由安全包装层提供普通方法。
三十二、最终结论
Rust 与 C++、Python 的交互可以归纳成三层。
第一层:稳定 ABI
C ABI
优势:
- 最通用;
- 最稳定;
- 可以服务多种语言;
- 容易形成长期 SDK。
代价:
- 类型表达能力弱;
- 需要手动管理指针、长度、状态码和生命周期。
第二层:自动绑定生成
bindgen
cbindgen
Crubit
优势:
- 减少重复声明;
- 适合接口数量较多的项目;
- 能与构建系统集成。
但自动生成不等于自动获得安全接口。
第三层:语言专用高级桥接
CXX —— Rust 与 C++
PyO3 —— Rust 与 Python
优势:
- 类型体验更自然;
- 错误模型更容易映射;
- 大幅减少手写胶水;
- 更适合现代内部工程。
最终选型可以简化为:
Rust ↔ C++:
少量、稳定、需要长期兼容
└── C ABI
Rust 调用现成 C 接口
└── bindgen
C++ 调用 Rust C API
└── cbindgen
新建双向 Rust/C++ 模块
└── CXX
Bazel 大型代码库
└── 评估 Crubit
Rust ↔ Python:
Python 调用 Rust
└── PyO3 + maturin
Rust 调用 Python
└── PyO3 embedding
少量函数且已有 C ABI
└── ctypes / CFFI
最重要的设计原则不是选择哪个工具,而是:
1. 边界类型必须明确;
2. 对象所有权必须唯一;
3. 分配和释放必须配对;
4. 异常和 panic 必须在边界内转换;
5. unsafe 必须被限制在很小的适配层;
6. 跨语言调用应当批量化;
7. 核心业务逻辑不要依赖具体绑定框架;
8. 公共 ABI 一旦发布,就要按长期接口维护。
只要这些规则得到满足,Rust 就可以安全地嵌入现有 C++ 系统,也可以作为 Python 的高性能底层模块,而不必一次性重写整个工程。
更多推荐
所有评论(0)