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,
}
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 兼容的形式,通常是指针和长度的组合。
- 基本类型:Rust 的
# 2. 在 C++ 中定义匹配的结构体
C++ 端的结构体定义必须与 Rust 中 #[repr(C)]
的结构体在字段顺序和类型上完全一致。
#include <cstdint>
struct MyData {
int32_t value;
const char* message;
};
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
}
}
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);
}
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;
}
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) };
}
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
)。
- Rust to C++: 使用
向量/数组 (
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);
}
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;
}
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++ 代码的项目;追求安全和开发效率。 |
核心注意事项清单:
- 始终使用
#[repr(C)]
:这是保证内存布局一致性的基础。 - 明确内存所有权:谁分配,谁释放。提供配对的
create
和free
函数是最佳实践。 - 避免直接传递 Rust/C++ 特有类型:将
String
,Vec
,std::string
,std::vector
等转换为 C 兼容的表示(指针 + 长度)。 - 注意字符串编码和结尾符:Rust 字符串是 UTF-8 且不以 null 结尾,而 C 字符串通常是 ASCII/UTF-8 且以 null 结尾。使用
CString
和CStr
进行正确转换。 - 错误处理:FFI 边界不会自动传递 Rust 的
Result
或 C++ 的异常。通常的做法是返回一个错误码或特殊值(如空指针)来表示失败。 - 考虑使用
cxx
:对于大多数 Rust/C++ 混合项目,cxx
提供了更安全、更高效的开发体验,是首选方案。
通过遵循这些原则和注意事项,你可以安全、有效地在 Rust 和 C++ 程序之间通过结构体传递数据,充分利用两种语言的优势。