📝 文章摘要

Rust 的 unsafe FFI 提供了与 C 语言交互的能力,但直接与 C++ 交互(特别是处理类、模板和所有权)既复杂又危险。cxx 库是一个由 Google 维护的安全 Rust/C++ FFI 桥接库。本文将深入探讨 cxx 的设计原理,展示它如何通过代码生成和静态检查来保证 C++ 交互的类型安全和内存安全。我们将实战构建一个项目,演示如何在 Rust 中安全地调用 C++ 类方法,以及在 C++ 中调用 Rust 函数,实现真正的零成本双向绑定。


一、背景介绍

1.1 unsafe FFI 的痛点

使用 Rust 的 extern "C" 接口与 C++ 交互时,开发者必须手动处理:

  1. C++ 类布局:无法在 Rust 中安全地表示 C++ 的 std::vector 或 std::string
  2. 异常处理:C++ 异常(Exception)必须在 FFI 边界被捕获并转换为 Rust 的 Result
  3. 所有权:必须手动管理 `new 和 delete,极易导致内存泄漏或 Use-After-Free。
  4. Name Mangling:C++ 函数名被名被混淆,Rust 难以调用。

1.2 cxx:安全的 FFI 桥

cxx 库提供种声明式的宏,用于定义 Rust 和 C++ 之间的共享接口。

在这里插入图片描述

核心优势

  • 编译时检查cxx 在编译时静态检查两种语言的签名是否匹配。
  • 安全类型:自动转换std::string 和 rust::Stringstd::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++ 类型 转换开销
i32f64bool int32_tdoublebool
&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 已销毁

分析

  1. 自动绑定:`ccxx` 自动生成了所有桥接代码。
  2. 类型安全&[u8] 被安全地转换为 `rust::SliceString被安全转换为std::string`。
  3. 所有权管理ffi::new_cpp_engine 返回回的 UniquePtr<CppEngine> 被 Rust 正确管理。当 engine 变量在 Rust 中 drop 时,cxx 自动调用 C++ 的 ~CppEngine() 析构函数,防止了内存泄漏。
  4. 双向调用: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 核心要点

  • unsafe FFI 的危险:C++ FFI 必须手动处理类布局、异常和所有权,极错。
  • cxx 的安全性cxx 通过编译时代码生成和静态检查,提供了安全和内存安全的 FFI 桥。
  • 零成本抽象cxx 智能地映射 Rust 和 C++ 的标准类型(如 Vec/`vector`),几乎没有运行时开销。
  • 所有权感知:自动管理 UniquePtr/Box,确保析构函数在数在正确的时间被调用。

5.2 讨论问题

  1. 在什么情况下,你仍然会选择 unsafe FFI 而 cxx
  2. cxx 如何处理 C++ 的模板(Templates)?(提示:通常需要具体化)
    3cxxautocxx`(另一个绑定工具)之间有何区别?
  3. 当 C++ 库抛出异常时时,cxx 在 Rust 侧是如何将其转换为 Result 的?

参考链接

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐