Rust 与 C/C++ 互操作:FFI 的实践与技巧
一句话总结: 详细介绍 Rust 如何通过外部函数接口(FFI)与 C/C++ 代码进行高效、安全的互操作,实现现有代码库的集成。
引言:FFI 的重要性(利用现有 C/C++ 库、性能关键部分)
Rust 语言以其卓越的内存安全、并发安全和高性能而备受赞誉。然而,在现实世界的软件开发中,很少有项目能够完全从零开始。许多大型、成熟的系统和库都是用 C 或 C++ 编写的,它们积累了数十年的经验和优化。在这种背景下,**外部函数接口(Foreign Function Interface, FFI)**成为了 Rust 融入现有生态系统的关键桥梁。
FFI 允许 Rust 代码调用其他语言编写的函数,反之亦然。对于 Rust 而言,这意味着:
- 利用现有 C/C++ 库: 开发者可以无需重写,直接在 Rust 项目中复用大量成熟、经过验证的 C/C++ 库,例如图形库、网络库、加密库或操作系统 API。这极大地加速了开发进程,并降低了风险。
- 性能关键部分: 尽管 Rust 自身性能优异,但在某些极端性能敏感的场景下,可能需要利用 C/C++ 中高度优化的汇编代码或特定硬件接口。FFI 提供了这种直接访问的能力。
- 逐步迁移: 对于大型 C/C++ 项目,FFI 使得团队能够逐步将部分模块用 Rust 重写,享受 Rust 带来的安全性和开发效率提升,而无需一次性重构整个系统。
虽然 FFI 带来了巨大的便利,但它也引入了复杂性。由于 C/C++ 代码不遵循 Rust 的所有权和借用规则,通过 FFI 交互时,Rust 的编译时安全保证会被部分绕过,需要开发者手动确保内存安全。因此,理解 FFI 的工作原理和最佳实践至关重要。

extern "C" 块:声明外部函数
在 Rust 中,与 C/C++ 函数进行互操作的第一步是使用 extern "C" 块来声明外部函数。这个块告诉 Rust 编译器,这些函数是外部定义的,并且它们遵循 C 语言的调用约定(ABI,Application Binary Interface)。C ABI 是一个通用的标准,确保不同语言编译的代码能够正确地相互调用。
- 声明外部函数: 在
extern "C"块内部,你声明外部函数的签名,包括函数名、参数类型和返回值类型。这些类型需要与 C 语言中的对应类型兼容。 - 调用 C 函数: 声明后的外部函数可以在 Rust 代码中像普通函数一样调用。然而,由于 Rust 编译器无法验证外部 C 代码的安全性,调用这些函数通常被认为是
unsafe的操作,需要放在unsafe块中。这是因为 C 函数可能返回裸指针、修改不属于 Rust 管理的内存,或者有其他 Rust 编译器无法检查的前置条件。
以下是一个调用 C 语言标准库中 abs 函数的简单示例:
// 声明一个外部 C 函数,它接受一个 i32 并返回一个 i32
// extern "C" 块告诉 Rust 编译器,这个函数遵循 C 语言的调用约定
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
let num = -5;
let result;
// 调用外部 C 函数是 unsafe 的,因为 Rust 无法保证 C 代码的安全性
unsafe {
result = abs(num);
}
println!("C 语言的 abs({}) 是: {}", num, result); // 输出: C 语言的 abs(-5) 是: 5
}
在这个例子中,我们声明了 abs 函数,并在 unsafe 块中调用了它。Rust 编译器信任开发者会确保 abs 函数的调用是安全的。
裸指针(Raw Pointers):*const T 和 *mut T
在 FFI 中,裸指针是与 C/C++ 代码交互的核心。C/C++ 广泛使用指针来直接操作内存,而 Rust 的引用(& 和 &mut)则受到借用检查器的严格管理。为了与 C/C++ 的指针模型兼容,Rust 提供了裸指针类型:
*const T: 表示一个指向T类型数据的不可变裸指针。它类似于 C 语言中的const T*。*mut T: 表示一个指向T类型数据的可变裸指针。它类似于 C 语言中的T*。
与 Rust 引用不同,裸指针:
- 没有生命周期,编译器不会检查它们的有效性。
- 可以为空(
std::ptr::null()或std::ptr::null_mut())。 - 可以指向任意内存地址,甚至是非法的地址。
- 可以有多个可变裸指针指向同一数据,或者同时存在可变和不可变裸指针。
因此,解引用裸指针(即通过 *ptr 访问其指向的值)是一个 unsafe 操作。开发者必须手动确保裸指针是有效的、非空的,并且指向的内存是已分配且未被释放的。
fn main() {
let mut value = 42;
// 从 Rust 引用创建裸指针
let raw_ptr_const: *const i32 = &value;
let raw_ptr_mut: *mut i32 = &mut value;
unsafe {
// 解引用不可变裸指针
println!("通过 *const i32 解引用的值: {}", *raw_ptr_const);
// 通过 *mut i32 修改值
*raw_ptr_mut = 100;
println!("通过 *mut i32 修改后的值: {}", *raw_ptr_mut);
}
println!("原始变量 value 的值: {}", value); // 输出: 原始变量 value 的值: 100
}
在这个例子中,我们展示了如何从 Rust 引用创建裸指针,并在 unsafe 块中解引用和修改它们。
传递复杂数据结构:#[repr(C)]、Vec、String
当需要在 Rust 和 C/C++ 之间传递结构体、数组或字符串等复杂数据时,需要特别注意内存布局和数据表示。
-
#[repr(C)]属性:- Rust 编译器为了优化性能,可能会重新排列结构体字段的内存布局。然而,C 语言有其自己的内存布局规则。
#[repr(C)]属性强制 Rust 结构体使用与 C 语言兼容的内存布局。这意味着字段的顺序和填充将与 C 编译器生成的一致,从而确保 Rust 和 C 之间的数据结构能够正确地相互解释。#[repr(C)] // 确保内存布局与 C 兼容 struct Point { x: i32, y: i32, } extern "C" { fn process_point(p: Point); // 假设 C 端有一个函数接收 Point 结构体 } fn main() { let p = Point { x: 10, y: 20 }; unsafe { process_point(p); // 将 Rust 结构体传递给 C 函数 } }2.Vec与 C 数组的转换:
Rust 的 Vec<T> 是一个动态数组,其内部数据是连续存储的。这使得它非常适合与 C 语言的数组进行互操作。
你可以使用 vec.as_mut_ptr() 获取 Vec 内部数据的裸指针,并使用 vec.len() 获取其长度。将这两个信息传递给 C 函数,C 函数就可以像操作普通 C 数组一样操作 Vec 的数据。
extern "C" {
// 假设 C 端有一个函数,接收一个 i32 数组的指针和长度
fn sum_array(arr: *const i32, len: usize) -> i32;
}
fn main() {
let my_vec = vec![1, 2, 3, 4, 5];
let total_sum;
unsafe {
// 获取 Vec 内部数据的裸指针和长度
total_sum = sum_array(my_vec.as_ptr(), my_vec.len());
}
println!("C 函数计算的数组和: {}", total_sum); // 输出: C 函数计算的数组和: 15
}
String 与 C 字符串的转换:
- Rust 的
String和&str是 UTF-8 编码的,并且不以 null 终止。而 C 字符串(char*或const char*)通常是 ASCII 或其他编码,并且以 null 字节 (\0) 终止。 - 为了将 Rust 字符串传递给 C,你需要将其转换为一个以 null 终止的 C 字符串。
std::ffi::CString类型就是为此目的而设计的。它会在 Rust 字符串的末尾添加一个 null 字节,并提供一个裸指针。
从 C 接收字符串时,可以使用 std::ffi::CStr 来安全地处理 C 字符串,并将其转换为 Rust 的 &str
use std::ffi::{CStr, CString};
use std::os::raw::c_char; // C 语言的 char 类型
extern "C" {
// 假设 C 端有一个函数,接收一个 C 字符串并打印
fn print_c_string(s: *const c_char);
// 假设 C 端有一个函数,返回一个 C 字符串
fn get_c_string() -> *const c_char;
}
fn main() {
// Rust String -> CString -> *const c_char
let rust_string = String::from("Hello from Rust!");
let c_string = CString::new(rust_string).expect("CString::new failed");
let c_ptr = c_string.as_ptr();
unsafe {
print_c_string(c_ptr); // 将 C 字符串指针传递给 C 函数
// 从 C 函数获取 C 字符串指针 -> CStr -> Rust &str
let received_c_ptr = get_c_string();
let received_c_str = CStr::from_ptr(received_c_ptr);
let received_rust_str = received_c_str.to_str().expect("无效的 UTF-8 序列");
println!("从 C 函数接收到的字符串: {}", received_rust_str);
}
}
从 Rust 调用 C:build.rs 和 bindgen
当 C 代码比较复杂,或者需要编译 C 库时,手动管理 FFI 可能会变得繁琐。Rust 提供了强大的工具来简化这个过程。
build.rs:构建 C 库
build.rs 是一个特殊的 Rust 脚本,它在编译你的 Rust crate 之前运行。它通常用于编译 C/C++ 库,并将其链接到你的 Rust 项目中。
你可以使用 cc crate(一个 Rust 库)来方便地在 build.rs 中编译 C/C++ 源代码。
build.rs 示例:
// build.rs
fn main() {
// 告诉 cargo 链接到名为 "my_c_lib" 的静态库
println!("cargo:rustc-link-lib=static=my_c_lib");
// 告诉 cargo 在当前目录查找库
println!("cargo:rustc-link-search=native=.");
// 使用 cc crate 编译 C 源代码
cc::Build::new()
.file("src/my_c_lib.c") // C 源文件路径
.compile("my_c_lib"); // 编译成名为 my_c_lib 的静态库
}
src/my_c_lib.c 示例:
// src/my_c_lib.c
#include <stdio.h>
void greet_from_c(const char* name) {
printf("Hello, %s from C!\n", name);
}
int add_numbers(int a, int b) {
return a + b;
}
src/main.rs 示例:
// src/main.rs
use std::ffi::CString;
use std::os::raw::c_char;
extern "C" {
fn greet_from_c(name: *const c_char);
fn add_numbers(a: i32, b: i32) -> i32;
}
fn main() {
let name = CString::new("Rust").unwrap();
unsafe {
greet_from_c(name.as_ptr());
let sum = add_numbers(10, 20);
println!("10 + 20 = {}", sum);
}
}
bindgen 工具:自动生成 Rust 绑定
- 手动为复杂的 C 头文件编写
extern "C"声明是乏味且容易出错的。bindgen是一个强大的工具,它可以自动从 C/C++ 头文件生成 Rust FFI 绑定。 bindgen可以解析 C 头文件,并生成对应的 Rustextern "C"块、结构体定义(带有#[repr(C)])、枚举等。这大大减少了手动编写 FFI 代码的工作量,并降低了出错的可能性。- 通常,
bindgen也在build.rs脚本中运行。
build.rs 中使用 bindgen 的概念示例:
// build.rs
extern crate bindgen;
use std::env;
use std::path::PathBuf;
fn main() {
// 告诉 cargo 重新运行此脚本,如果 my_c_lib.h 发生变化
println!("cargo:rerun-if-changed=src/my_c_lib.h");
// 生成 Rust 绑定
let bindings = bindgen::Builder::default()
.header("src/my_c_lib.h") // C 头文件路径
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("无法生成绑定");
// 将绑定写入到输出目录
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("无法写入绑定文件");
// 编译 C 库 (同上)
cc::Build::new()
.file("src/my_c_lib.c")
.compile("my_c_lib");
}
然后,在 src/main.rs 中,你可以通过 include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 导入生成的绑定。
从 C 调用 Rust:#[no_mangle] 和 extern "C" 函数
FFI 不仅仅是 Rust 调用 C,C/C++ 代码也可以调用 Rust 函数。这对于将 Rust 模块集成到现有 C/C++ 项目中非常有用。
#[no_mangle]属性:- Rust 编译器默认会对函数名进行“名称重整”(name mangling),以支持函数重载、模块路径等特性。这意味着 Rust 编译后的函数名与源代码中的函数名可能不同。
#[no_mangle]属性告诉 Rust 编译器不要对该函数进行名称重整,使其在编译后的二进制文件中保持原始名称。这样,C/C++ 代码就可以通过其声明的名称找到并调用它。
extern "C"函数:- 与从 Rust 调用 C 函数类似,当 C 调用 Rust 函数时,Rust 函数也需要遵循 C ABI。因此,Rust 函数需要用
extern "C"关键字标记。 - 这些函数通常还需要是
pub的,以便在库外部可见。
- 与从 Rust 调用 C 函数类似,当 C 调用 Rust 函数时,Rust 函数也需要遵循 C ABI。因此,Rust 函数需要用
src/lib.rs 示例(编译为静态库或动态库):
// src/lib.rs
use std::os::raw::c_char;
use std::ffi::{CStr, CString};
// 告诉 Rust 编译器不要重整此函数名,并使用 C ABI
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
println!("Rust 函数 rust_add 被调用!");
a + b
}
#[no_mangle]
pub extern "C" fn greet_rust(name_ptr: *const c_char) {
// 从 C 字符串指针安全地转换为 Rust &str
let name = unsafe {
CStr::from_ptr(name_ptr).to_str().expect("无效的 UTF-8 序列")
};
println!("Hello, {} from Rust!", name);
}
// 为了让 C 能够释放 Rust 分配的字符串,Rust 需要提供一个释放函数
#[no_mangle]
pub extern "C" fn rust_free_string(ptr: *mut c_char) {
if ptr.is_null() { return; }
unsafe {
// 将裸指针重新构造成 CString,然后让 CString 在 drop 时释放内存
let _ = CString::from_raw(ptr);
}
}
要从 C 调用这些 Rust 函数,你需要将 Rust 项目编译成一个静态库(.a 或 .lib)或动态库(.so 或 .dll)。例如,在 Cargo.toml 中添加:
[lib]
crate-type = ["cdylib"] # 编译为动态库
# 或者 crate-type = ["staticlib"] # 编译为静态库
然后,C 代码可以链接到这个库并调用 rust_add 和 greet_rust 函数。
结论:FFI 使得 Rust 能够无缝集成到现有生态中,同时保持其安全性和性能优势
Rust 的 FFI 机制是其强大功能集的重要组成部分。它使得 Rust 不仅仅是一个独立的现代语言,更是一个能够与庞大且成熟的 C/C++ 生态系统无缝协作的工具。通过 FFI,开发者可以:
- 高效利用现有资源: 避免重复造轮子,直接集成和利用数十年积累的 C/C++ 库。
- 实现极致性能: 在必要时,通过 FFI 调用 C/C++ 中高度优化的代码,以满足最严苛的性能要求。
- 平滑过渡: 为大型遗留项目提供了一条逐步引入 Rust 的可行路径,降低了迁移风险。
然而,FFI 并非没有代价。它要求开发者深入理解 C/C++ 的内存模型和调用约定,并在 unsafe 块中手动维护 Rust 的安全不变量。这意味着在使用 FFI 时,必须格外小心,遵循最佳实践,如使用 #[repr(C)]、CString、Vec 的裸指针,并考虑使用 bindgen 等工具来自动化和简化绑定过程。
最终,FFI 使得 Rust 能够在一个充满挑战的领域——系统编程和底层互操作——中,既保持其核心的安全性和性能优势,又能展现出卓越的实用性和适应性。
更多推荐



所有评论(0)