文章目录

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++ 类、模板、异常和标准库容器时,通常还需要额外的包装层。

本文将依次讲清楚:

  1. Rust 与 C++ 交互的底层原理;
  2. 手动 C ABI 的双向调用;
  3. bindgencbindgen 的职责;
  4. CXX 的双向类型安全桥接;
  5. Crubit 当前适合什么场景;
  6. Python 如何调用 Rust;
  7. Rust 如何嵌入并调用 Python;
  8. 字符串、容器、对象、异常、线程和内存如何跨越语言边界;
  9. 不同项目应该如何选型。

一、先建立整体认识:到底是谁调用谁

“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”,而要先回答三个问题:

  1. 谁是主程序?
  2. 谁调用谁?
  3. 边界上需要传递哪些类型?

二、跨语言调用的底层: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
bindgenC/C++ → Rust较低根据 C/C++ 头文件生成 Rust 声明
cbindgenRust → C/C++较低根据 Rust 导出接口生成 C/C++ 头文件
CXX双向较高新项目中的 Rust/C++ 类型安全桥接
Crubit双向较高Bazel 和大型 C++/Rust 混合代码库
PyO3 + maturinPython → RustRust 编写 Python 扩展模块
PyO3 嵌入模式Rust → PythonRust 程序调用 Python
ctypes / CFFIPython → 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++ 不透明类型、UniquePtrSharedPtr、Rust Vec、C++ std::vector、切片、字符串和显式错误传播等机制。

Chromium 的 Rust FFI 文档也曾明确推荐使用 CXX,并支持在 Rust 和 C++ 之间双向调用。


九、完整 CXX 双向调用示例

这个示例同时演示:

  1. Rust 调用 C++ 类;
  2. C++ 回调 Rust 函数;
  3. Rust 字符串与 C++ 字符串交互;
  4. 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 是否完全安全、完全零开销

不能这样绝对描述。

更准确的说法是:

  1. CXX 会静态检查 bridge 中的很多类型和签名;
  2. 它能避免大量手写胶水代码;
  3. 它不需要 RPC、JSON 或通用序列化层;
  4. 切片和引用等场景可以直接借用数据;
  5. 部分字符串转换仍然可能进行复制、分配或 UTF-8 校验;
  6. C++ 实现依然可能违反生命周期、空指针、线程安全等契约;
  7. 原始指针接口仍然需要声明为 unsafe

因此,CXX 提供的是:

比手动 C ABI 更强的静态约束和类型映射,而不是把任意 C++ 代码自动变成安全 Rust。


十二、方案三:Crubit 当前处于什么状态

Crubit 是 Google 开发的 C++/Rust 双向绑定生成器。

它的目标是根据已有 C++ 或 Rust API 自动生成高保真绑定,减少像 CXX bridge 这样的重复声明。

Crubit 当前已经不适合简单概括为“整个项目仍然完全处于实验阶段”。官方文档区分了多个能力等级:

supported
  └── 面向明确支持 FFI 的库,提供一般可用能力

wrapper
  └── 不稳定、需要 allowlist,主要用于高接触核心库

experimental
  └── 内部实验能力,可能频繁变化

官方说明 supported 特性用于一般可用的互操作能力,而 wrapperexperimental 仍然具有明显的不稳定性和使用限制。

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 官方教程同样使用 cdylibabi3 配置构建扩展模块。

不需要兼容多个 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 时的类型映射

常见类型映射大致如下:

RustPython
i32i64u64int
f32f64float
boolbool
String&strstr
Vec<T>list
HashMap<K, V>dict
Option<T>TNone
PyResult<T>返回值或 Python 异常
#[pyclass] structPython 类

但类型可转换不代表没有开销。

例如:

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 的区别

对比项ctypesPyO3
接口层级C ABIPython 原生扩展
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 {}

SendSync 是 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 的高性能底层模块,而不必一次性重写整个工程。

更多推荐