目录

📝 文章摘要

一、背景介绍

1.1 unsafe FFI 的痛点

1.2 cxx:安全的 FFI 桥

二、原理详解

2.1 cxx::bridge! 宏

2.2 类型映射

三、代码实战

3.1 项目结构

3.2 步骤 1:配置 build.rs

3.3 步骤 2:定义 C+++ 侧代码

3.4 步骤 3:定义 FFI 桥接

3.5 步骤 4:实现 Rust 侧逻辑

四、结果分析

4.1 运行

4.2 性能开销

五、总结与讨论

5.1 核心要点

5.2 讨论问题

参考链接


📝 文章摘要

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 风格的原始指针。

Rustst 类型 C++ 类型 转换开销
i32f64bool int32_tdoublebool`
&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 已销毁

分析

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

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

5.2 讨论问题

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

参考链接

Logo

新一代开源开发者平台 GitCode,通过集成代码托管服务、代码仓库以及可信赖的开源组件库,让开发者可以在云端进行代码托管和开发。旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐