目录

📝 文章摘要

一、背景介绍

二、原理详解

2.1 #[repr(C)]:内存布局

2.2 `CString vs CStr:C 字符串

三、代码实战

3.1 实战:Rust 调用 C (CStr / CString)

3.2 实战:C 调用 Rust 回调函数 (最危险的 FFI)

四、结果分析

4.1 FFI 安全性总结

五、总结与讨论

5.1 核心要点

5.2 讨论问题

参考链接


📝 文章摘要

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模块提供了CStrCString` 来安全地处理这些 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 闭包。

陷阱

  1. 闭包(Closure)不是一个简单的函数指针(fn())。
  2. 如果闭包捕获了环境(move),它是有状态的。
  3. 我们必须保证 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 块的责任):

  1. 数据有效性:传递给 C 的指针必须指向有效的内存。
  2. 生命周期:传递给 C 的指针(如 `CString.as_ptr()所指向的数据,其存活时间必须长于 C 库使用它的时间。
  3. Null 指针:C 库返回的指针可能是 NULL,Rust 必须(if p.is_null())检查。
  4. **线程安全:C 库可能不是线程安全的。
  5. 所有权:必须明确 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 讨论问题
  1. 为什么 CString::new() 在遇到 \0 字节时必须返回 Err(panic)?
  2. 为什么 CStr::from_ptr() 是 unsafe 的,而 CStr::to_str()(转为 &str)却返回 Result(安全)?
  3. 如果一个 C 库返回一个 char*,Rust(point_free)应该调用 free() 还是 C 库提供的 my_lib_free()?(提示:分配器)

参考链接

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐