Xz's blog Xz's blog
首页
时间序列
多模态
合成生物学
其他方向
生活
工具相关
PyTorch
导航站

Xu Zhen

首页
时间序列
多模态
合成生物学
其他方向
生活
工具相关
PyTorch
导航站
  • Rust

    • Rust 工具链
    • Cargo
    • 变量
    • 函数和控制流
    • 所有权
    • 引用与借用
    • Slice切片
    • 属性 Attribute
    • 闭包
    • Trait—关联类型(Associated Types)
    • 关联类型与泛型
    • 多线程 mpsc::channel
    • Rust 调用 C++ 之静态链接
    • Rust 调用 C++ 之动态链接
    • Rust与C++之间传递数据
      • Rust 与 C++ 通过结构体传递数据:深入解析与注意事项
        • 核心原则:C ABI 作为桥梁
        • 方法一:手动管理 FFI 与 #[repr(C)]
        • 1\. 在 Rust 中定义 C 兼容的结构体
        • 2\. 在 C++ 中定义匹配的结构体
        • 3\. 内存管理:明确所有权
        • 4\. 处理复杂数据类型
        • 方法二:使用 cxx Crate
        • 1\. 定义共享接口
        • 2\. C++ 端实现
        • 3\. cxx 的优势和注意事项
        • 总结与选择建议
  • Rust-Windows 窗口自动化

  • Tauri

  • C++

  • Claude Code

  • Liunx相关

  • Windows相关

  • IDE

  • Conda

  • Docker

  • VMware虚拟机

  • Python常用代码片段

  • 工具相关
  • Rust
xuzhen
2025-08-25
目录

Rust与C++之间传递数据

# Rust 与 C++ 通过结构体传递数据:深入解析与注意事项

在 Rust 作为主程序,并需要与 C++ 模块进行交互的场景中,通过结构体(struct)传递数据是一种常见且高效的方式。然而,由于两种语言在内存布局、所有权模型和生命周期管理上的根本差异,直接传递并非一蹴而就,需要遵循特定的规则和约定以确保安全和正确性。本文将深入探讨 Rust 与 C++ 之间通过结构体传递数据的两种主要方法:手动 FFI(Foreign Function Interface) 和使用 cxx 库,并详细阐述各自的注意事项。

# 核心原则:C ABI 作为桥梁

Rust 和 C++ 都有其独特的、不稳定的应用二进制接口(ABI)。为了实现互操作,必须依赖一个共同且稳定的标准,即 C ABI。这意味着所有跨语言边界传递的数据结构和函数调用都必须遵循 C 语言的规范。


# 方法一:手动管理 FFI 与 #[repr(C)]

这是一种更底层、更灵活但需要开发者承担更多安全责任的方式。核心在于将 Rust 的结构体标记为 C 兼容的内存布局,并手动管理内存。

# 1. 在 Rust 中定义 C 兼容的结构体

为了保证 Rust 的 struct 和 C++ 的 struct 具有完全相同的内存布局(字段顺序、大小和对齐),必须在 Rust 的结构体定义上使用 #[repr(C)] 属性。

#[repr(C)]
pub struct MyData {
    pub value: i32,
    pub message: *const libc::c_char,
}
1
2
3
4
5

注意事项:

  • #[repr(C)] 是必需的:没有它,Rust 编译器可能会为了优化而重排字段,导致 C++ 端无法正确解析。
  • 使用 C 兼容的类型:结构体字段的类型也必须是 C ABI 兼容的。
    • 基本类型:Rust 的 i32, u64, f32 等通常直接对应 C++ 的 int32_t, uint64_t, float。为了跨平台的确定性,推荐使用 libc crate 中定义的类型,如 libc::c_int, libc::c_char。
    • 指针:裸指针 *const T 和 *mut T 是 FFI 的基础,用于传递复杂数据或实现所有权转移。
    • 复杂类型(字符串、向量等):不能直接在 #[repr(C)] 结构体中使用 Rust 特有的类型,如 String 或 Vec,因为它们的内存布局不确定且包含非 repr(C) 的元数据。必须将它们转换为 C 兼容的形式,通常是指针和长度的组合。

# 2. 在 C++ 中定义匹配的结构体

C++ 端的结构体定义必须与 Rust 中 #[repr(C)] 的结构体在字段顺序和类型上完全一致。

#include <cstdint>

struct MyData {
    int32_t value;
    const char* message;
};
1
2
3
4
5
6

# 3. 内存管理:明确所有权

这是手动 FFI 中最关键也最容易出错的部分。内存必须由分配它的一方来释放。

场景一:Rust 创建数据,C++ 使用

在 Rust 中创建结构体,并将其指针传递给 C++。C++ 只能读取或修改其内容,但不能释放它。Rust 必须提供一个独立的函数供 C++ 调用以释放内存。

Rust 代码:

use std::ffi::CString;
use std::os::raw::c_char;

#[repr(C)]
pub struct MyData {
    pub value: i32,
    pub message: *const c_char,
}

#[no_mangle]
pub extern "C" fn create_my_data() -> *mut MyData {
    let message = CString::new("Hello from Rust").unwrap();
    let data = Box::new(MyData {
        value: 42,
        message: message.into_raw(), // 转移所有权给 C
    });
    Box::into_raw(data) // 再次转移所有权
}

#[no_mangle]
pub extern "C" fn free_my_data(ptr: *mut MyData) {
    if ptr.is_null() {
        return;
    }
    unsafe {
        // 首先,从 MyData 中取回 CString 的所有权并释放
        let data = Box::from_raw(ptr);
        let _ = CString::from_raw(data.message as *mut c_char);
        // Box 的 Drop 会自动释放 MyData
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

C++ 代码:

extern "C" {
    struct MyData* create_my_data();
    void free_my_data(struct MyData* ptr);
}

void process_data_from_rust() {
    struct MyData* data = create_my_data();
    // 使用 data...
    std::cout << "Value: " << data->value << ", Message: " << data->message << std::endl;
    // 使用完毕后,调用 Rust 提供的函数释放内存
    free_my_data(data);
}
1
2
3
4
5
6
7
8
9
10
11
12

场景二:C++ 创建数据,Rust 使用

C++ 分配内存并创建结构体,然后将其指针传递给 Rust。Rust 使用完毕后,需要调用 C++ 提供的函数来释放内存。

C++ 代码:

struct MyData* create_data_in_cpp() {
    MyData* data = new MyData;
    data->value = 123;
    data->message = "Hello from C++";
    return data;
}

void free_data_in_cpp(MyData* ptr) {
    delete ptr;
}
1
2
3
4
5
6
7
8
9
10

Rust 代码:

extern "C" {
    fn create_data_in_cpp() -> *mut MyData;
    fn free_data_in_cpp(ptr: *mut MyData);
}

fn use_data_from_cpp() {
    let data_ptr = unsafe { create_data_in_cpp() };
    if !data_ptr.is_null() {
        let data = unsafe { &*data_ptr };
        let message = unsafe { std::ffi::CStr::from_ptr(data.message).to_str().unwrap_or("Invalid UTF-8") };
        println!("Value: {}, Message: {}", data.value, message);
    }
    unsafe { free_data_in_cpp(data_ptr) };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4. 处理复杂数据类型

  • 字符串:

    • Rust to C++: 使用 std::ffi::CString 创建一个以 null 结尾的 C 字符串,并通过 as_ptr() 或 into_raw() 获取 *const c_char。注意 into_raw 会转移所有权。
    • C++ to Rust: 使用 std::ffi::CStr 从 *const c_char 创建一个安全的 Rust 字符串切片 (&str)。
  • 向量/数组 (Vec):

    • 不能直接传递 Vec。需要将其分解为三部分:指向数据的指针、长度和容量。
    • 在 #[repr(C)] 结构体中定义 *mut T, usize (长度), usize (容量)。
    • 在接收端,根据这三个部分重新构建相应语言的数据结构。
    • 关键:同样需要明确内存所有权和释放规则。通常的做法是,分配 Vec 的一方提供一个函数来释放它。

# 方法二:使用 cxx Crate

cxx 是一个用于在 Rust 和 C++ 之间实现安全、零成本互操作的库。它通过一个宏来定义共享的 API,并自动生成底层的 FFI 绑定代码,极大地简化了流程并提高了安全性。

# 1. 定义共享接口

在一个 #[cxx::bridge] 宏块中,你可以定义共享的结构体、在 Rust 中实现的函数(extern "Rust")和在 C++ 中实现的函数(unsafe extern "C++")。

// src/lib.rs
#[cxx::bridge]
mod ffi {
    struct SharedData {
        value: i32,
        message: String,
    }

    extern "Rust" {
        fn process_data_from_cpp(data: &SharedData);
    }

    unsafe extern "C++" {
        include!("path/to/your/cpp/header.h");

        fn get_data_from_cpp() -> SharedData;
    }
}

fn process_data_from_cpp(data: &ffi::SharedData) {
    println!("Received in Rust: value = {}, message = '{}'", data.value, data.message);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2. C++ 端实现

cxx 会根据你的 bridge 定义生成一个 C++ 头文件。你需要在你的 C++ 代码中包含它,并实现相应的函数。

// src/main.cpp
#include "path/to/your/rust/project/target/cxxbridge/lib/src/lib.rs.h" // 自动生成的头文件
#include <iostream>
#include <string>

SharedData get_data_from_cpp() {
    return SharedData{100, "Hello from C++ via cxx"};
}

int main() {
    SharedData data_to_rust;
    data_to_rust.value = 99;
    data_to_rust.message = "Sending to Rust";

    process_data_from_cpp(data_to_rust);

    SharedData data_from_rust = get_data_from_cpp();
    std::cout << "Received in C++: value = " << data_from_rust.value
              << ", message = '" << data_from_rust.message << "'" << std::endl;

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 3. cxx 的优势和注意事项

  • 安全性:cxx 在编译时进行静态检查,确保 Rust 和 C++ 端的类型和签名匹配,极大地减少了 unsafe 代码和潜在的错误。
  • 易用性:自动处理了许多复杂类型的转换,如 String 到 std::string,Vec 到 std::vector。
  • 零成本抽象:生成的代码非常高效,通常没有额外的运行时开销。
  • 构建集成:需要配置 build.rs 来编译 C++ 代码并链接到 Rust 程序。
  • 类型支持:cxx 支持一系列核心类型,但对于不支持的复杂类型,仍可能需要手动处理指针。

# 总结与选择建议

特性 手动 FFI (#[repr(C)]) cxx Crate
控制力 非常高,可以精确控制每一个细节。 较高,但受限于 cxx 的框架。
安全性 完全依赖开发者,容易出错。需要大量 unsafe 代码。 非常高,编译时静态检查,大部分代码是安全的。
开发效率 低,需要手动编写大量样板代码和内存管理逻辑。 高,自动生成绑定代码,简化了复杂类型处理。
适用场景 与已有的纯 C 库交互;需要极高灵活性的底层操作。 新项目或可以修改 C++ 代码的项目;追求安全和开发效率。

核心注意事项清单:

  1. 始终使用 #[repr(C)]:这是保证内存布局一致性的基础。
  2. 明确内存所有权:谁分配,谁释放。提供配对的 create 和 free 函数是最佳实践。
  3. 避免直接传递 Rust/C++ 特有类型:将 String, Vec, std::string, std::vector 等转换为 C 兼容的表示(指针 + 长度)。
  4. 注意字符串编码和结尾符:Rust 字符串是 UTF-8 且不以 null 结尾,而 C 字符串通常是 ASCII/UTF-8 且以 null 结尾。使用 CString 和 CStr 进行正确转换。
  5. 错误处理:FFI 边界不会自动传递 Rust 的 Result 或 C++ 的异常。通常的做法是返回一个错误码或特殊值(如空指针)来表示失败。
  6. 考虑使用 cxx:对于大多数 Rust/C++ 混合项目,cxx 提供了更安全、更高效的开发体验,是首选方案。

通过遵循这些原则和注意事项,你可以安全、有效地在 Rust 和 C++ 程序之间通过结构体传递数据,充分利用两种语言的优势。

#Rust
上次更新: 2025/08/25, 15:57:17

← Rust 调用 C++ 之动态链接 打印所有用户界面元素→

最近更新
01
Linux 通过Windows代理上网
09-18
02
vscode远程使用copilot和codex(内网环境)
09-18
03
跨机器克隆环境
09-18
更多文章>
Theme by Vdoing | Copyright © 2025-2025 Xu Zhen | 鲁ICP备2025169719号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式