Rust std::ffi 手动 FFI 绑定:CStr、CString 与回调回调函数
目录
3.1 实战:Rust 调用 C (CStr / CString)
3.2 实战:C 调用 Rust 回调函数 (最危险的 FFI)
📝 文章摘要
Rust 必须与 C 语言库(如 libc)进行底层交互。虽然 bindgen(篇已介绍)可以自动化这个过程,但理解手动 FFI(Foreign Function Interface,外部函数接口) 绑定是掌握 Rust 系统编程能力的关键。本文将深入 std::ffi 模块,探讨 #[repr(C)](C 内存布局)、`CStr(C 借用字符串)、CString(C 所有权字符串)的正确用法,并实战演练 FFI 中最危险的模式:如何安全地将 Rust 闭包(Closure)作为回调函数(Callback)传递给 C。
一、背景介绍
FFI(外部函数接口)是 Rust unsafe 块的核心用例之一。它允许 Rust 调用 C 函数,反之亦然。
// Rust 调用 C 的 `puts`
extern "C" {
fn puts(s: *const i8) -> i32;
}
fn main() {
let s = "Hello C!\0"; // 必须是 C 风格的 null 结尾
unsafe {
puts(s.as_ptr() as *const i8);
}
}
这个例子看起来简单,但它充满了陷阱:
1. Rust &str 不是 null 结尾的!
2. *const u8 (Rust) 和 `*const i8(C char) 必须手动转换。
3. 如果 s 包含内部 \0 怎么办?
`std:ffi模块提供了CStr和CString` 来安全地处理这些 C 语言的字符串约定。
二、原理详解
2.1 #[repr(C)]:内存布局
Rust 编译器不保证 struct 的内存布局。
struct RustStruct {
a: i32, // 4 字节
b: i8, // 1 字节
c: i64, // 8 字节
}
// Rust 编译器为了优化,可能会重排为:
// { c: i64 (8B), a: i32 (4B), b: i8 (1B), padding(3B) }
C 语言保证 `struct 的字段顺序。为了让 Rust struct 与 C struct 兼容,我们必须使用 #[repr(C)]。
// ✓ C 兼容
#[repr(C)]
struct MyStructC {
a: i32,
b: i8,
c: i64,
}
// 编译器保证其布局与 C struct 相同
2.2 `CString vs CStr:C 字符串
C 字符串(char*)是以 \0(Null 终止符)结尾的字节数组。Rust String({ptr, len, cap})不是 Null 终止的,并且可以包含内部 Null。
**td::ffi::CString** (对应 String`)
- 所有权(Owned)的、C 兼容的字符串。串。
- 保证:1. 内部没有
\0。 2. 结尾有\0。 - 用于**Rust 向 C 传递**(Rust 构造,C 接收所有权或借用)。
std::ffi::CStr (对应&str`)
- 借用(Borrowed)的、C 兼容的字符串。
- 封装一个
*const i8,并,并提供安全的方法(如to_str())。 - 用于从 C 向 Rust 传递(C 构造,Rust 借用)。
graph TD
A[Rust 域]
B[C 域]
A -- "CString::new().as_ptr()" --> B;
B -- "CStr::from_ptr()" --> A;
subgraph A
C(String: "Hello")
D(CString: "Hello\0");
E(&str: "Rust")
F(CStr:: "Rust\0");
end
subgraph B
G(char*: "Hello\0");
H(const char*: "ust\0");
end
style D fill:#e8f5e9
style F fill:#e1f5fe
三、代码实战
3.1 实战:Rust 调用 C (CStr / CString)
我们将安全地调用 libc 的 strlen(计算 C 字符串长度)和 puts。
use std::ffi::{CString, CStr};;
use std::os::raw::{c_char, c_int};
// 1. 手动声明 C 函数签名
extern "C" { fn strlen(s: *const c_char) -> usize;
fn puts(s: *const c_char) -> c_int;
}
fn main() {
// --- 1. Rust -> C (CString) ---
// (不能用 "Hello\0",如果 "Hello" 包含 \0 会 panic)
let rust_str = "Hello C from CString!";
let c_string = CString::new(rust_str)
.expect("CString::new failed"); // 确保没有内部 \0
unsafe {
// CString.as_ptr() 返回 *const c_char
println!("C (strlen) 认为 '{}' 的长度是: {}",
rust_str,
strlen(c_string.as_ptr()));
puts(c_string.as_ptr());
}
// --- 2. C -> Rust (CStr) ---
// (假设 c_string.as_ptr() 是 C 库返回给我们的)
let c_ptr: *const c_char = c_string.as_ptr();
unsafe {
/// CStr::from_ptr() 是 unsafe 的,
// 我们必须保证 c_ptr 是有效的、null 结尾的。
let cstr = CStr::from_ptr(c_ptr);
// CStr::to_str() 是安全的,
// 它检查 UTF-8 有效性
match c_str.to_str() {
Ok(rust_slice) => {
println!("("Rust (to_str) 转换回: '{}'", rust_slice);
}
Err(_) => {
eprintln!("C符串不是有效的 UTF-8");
}
}
}
}
3.2 实战:C 调用 Rust 回调函数 (最危险的 FFI)
场景:C 库(如 libfoo)需要一个回调函数 `void (*llback)(int status)`。我们想传递一个 Rust 闭包。
陷阱:
- 闭包(Closure)不是一个简单的函数指针(
fn())。 - 如果闭包捕获了环境(
move),它是有状态的。 - 我们必须保证 Rust 闭包的存活时间(Lifetime)比 C 库调用它的时间长。
**解决方案:`Box::into_rawextern "C" fn 包装器**
src/lib.rs (C 调用 Rust)
use std::os:::raw::c_int;
// 1. 定义 C 库期望的函数指针类型
type CCallback = unsafe extern "C" fn(status c_int);
// 2. 模拟 C 库 (它接受一个回调)
extern "C" {
fn run_c_task(cb: CCallback);
}
// 3. 包装器:将 (void*) 转换为 Rust 闭包
// (这是 C 库实际际调用的)
unsafe extern "C" fn rust_callback_wrapper(status: c_int) {
// (这只是一个示例, // 实际中 C 库会提供一个 `user_data: *mut c_void`
// 让我们能安全地取回闭包)
println!("[Rust Wrapper] C 库已调用回调,状态: {}", status);
// 假设 C 库允许我们传递一个 "user_data" 指针
// let closure_ptr = user_data as *mut Box<dyn Fn(i32)>;
// let closure = &*closure_ptr;
// closure(status);
}
fn main() {
// (省略... 这是一个复杂的模式,
// 通常涉及将 `Box<Box<dyn Fn()>>`
// 两次 `Box::into_raw`
// 来获取一个 `*mut c_void` 的 "user_data"
// 并传递给 C)
// (为简化,我们只传递一个静态包装器)
unsafe {
println!("[Rust] 准备调用 C 任务...");
run_c_task(rust_callback_wrapper);
}
}
**`src/c_lib(模拟 C 库)**
#include <stdio.h>
// 1. 定义函数指针类型
typedef void (*Callback)(int status);
// 2. C 函数,接受回调
void run_c_task(Callback cb) {
printf("[C Lib] C 任务正在执行...\n");
// 3. 执行回调
cb(99); // 状态码 99
printf("[C Lib] C 任务完成。\n");
}
(注:需要配置 build.rs 来编译 c_lib.c)
四、结果分析
4.1 FFI 安全性总结
FFI 永远是 unsafe 的,因为 Rust 编译器无法信任 C 代码。
Rust 必须保证的(unsafe 块的责任):
- 数据有效性:传递给 C 的指针必须指向有效的内存。
- 生命周期:传递给 C 的指针(如 `CString.as_ptr()所指向的数据,其存活时间必须长于 C 库使用它的时间。
- Null 指针:C 库返回的指针可能是
NULL,Rust 必须(if p.is_null())检查。 - **线程安全:C 库可能不是线程安全的。
- 所有权:必须明确 C 库是“借用”指针还是“拥有”有”指针(如果是拥有,C 负责
free,或 Rust 负责point_free)。
CStr 和 CString 是ust 提供的、用于安全处理 C 字符串(UTF-8 + Null 终止)的最佳工具。
五、总结与讨论
5.1 核心要点
extern "C":指示rustc使用 C ABI(Application Binary Interface,应用二进制接口)来编译函数。#[repr(C)]:指示rustc使用 C 内存布局(保证字段顺序)来编译struct或enum。CString:Rust 侧的“所有权” C 字符串。用于(安全地)构建一个带\0结尾、无内部\0的字节数组,以传递给 C。
-----CStr**:Rust 侧的“借用” C 字符串。用于(安全地)包装一个从 C 获得的 `*const char`,并提供 UTF-8 转换。
- 回调函数:是 FFI 中最复杂的模式,通常需要将 Rust 闭包
Box两次,将其指针(*mut c_void)作为user_data传递给 C。
5.2 讨论问题
- 为什么
CString::new()在遇到\0字节时必须返回Err(panic)? - 为什么
CStr::from_ptr()是unsafe的,而CStr::to_str()(转为&str)却返回Result(安全)? - 如果一个 C 库返回一个
char*,Rust(point_free)应该调用free()还是 C 库提供的my_lib_free()?(提示:分配器)
参考链接
- [The Rust Book - Ch 19-01: Unsafe Rust (FFI)](https://doc.rustst FFI Omnibus (FFI 实践大全)](https://www.google.com/search?q=https://rust-ffi.github.io/ffi-omnibus/)
- The Rustonomicon - Foreign Function Interface (FFI)
- std::ffi::CString (官方文档)
- [std::ffi::CStr (官方文档)](https://doc.rust
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)