第十三篇:《unsafe Rust 与 FFI:与 C 和系统调用交互》

在之前的十二篇文章中,我们一直在安全 Rust(Safe Rust) 的世界里------所有权、借用检查、生命周期,编译器替我们挡住了所有内存安全问题。但系统编程的某些场景,安全 Rust 无法表达:调用 C 库、操作裸指针、实现底层数据结构、直接与操作系统交互。这时,就需要 unsafe Rust。unsafe 不是"关闭安全检查",而是开启五项编译器无法验证的能力,并将安全责任转移给开发者。本文从 unsafe 的五种能力讲起,深入讲解裸指针、FFI(外部函数接口)、bindgen/cbindgen 工具,并通过一个完整的 C 库封装实战,帮你掌握安全使用 unsafe 的方法论。

一、unsafe 的五种能力

unsafe 关键字解锁了五种编译器无法验证的操作:

💡 关键理解:unsafe 不会关闭借用检查或类型检查。它只允许你做上述五种操作。编译器仍然检查类型安全和借用规则。

二、裸指针(Raw Pointer)

裸指针 *const T 和 *mut T 与 C 的指针类似,但不受 Rust 借用规则约束。

2.1 创建和使用裸指针

rust 复制代码
fn main() {
    let mut num = 5;

    // 创建裸指针(安全操作)
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

    // 解引用裸指针(unsafe 操作)
    unsafe {
        println!("r1 = {}", *r1);
        println!("r2 = {}", *r2);

        *r2 = 10;   // 通过可变裸指针修改
        println!("num = {}", num);   // 10
    }
}

裸指针与引用的区别:

2.2 裸指针的典型用途

场景一:与 C 代码交互

rust 复制代码
extern "C" {
    fn malloc(size: usize) -> *mut u8;
    fn free(ptr: *mut u8);
}

fn main() {
    unsafe {
        let ptr = malloc(1024);
        if !ptr.is_null() {
            // 使用内存...
            free(ptr);
        }
    }
}

场景二:实现底层数据结构

rust 复制代码
struct LinkedList<T> {
    head: *mut Node<T>,
    len: usize,
}

struct Node<T> {
    value: T,
    next: *mut Node<T>,
}

impl<T> LinkedList<T> {
    fn new() -> Self {
        LinkedList {
            head: std::ptr::null_mut(),
            len: 0,
        }
    }

    fn push(&mut self, value: T) {
        let new_node = Box::into_raw(Box::new(Node {
            value,
            next: self.head,
        }));

        self.head = new_node;
        self.len += 1;
    }
}

三、unsafe 函数

unsafe fn 表示调用者必须满足某些前提条件:

rust 复制代码
/// # Safety
///
/// `ptr` 必须是指向有效 `i32` 的非空指针。
unsafe fn read_value(ptr: *const i32) -> i32 {
    *ptr
}

fn main() {
    let x = 42;
    let value = unsafe { read_value(&x) };
    println!("value = {}", value);
}

最佳实践:为 unsafe fn 添加 # Safety 文档注释,说明调用者必须满足的条件。

四、安全抽象:封装 unsafe 代码

unsafe 代码应该被封装在安全抽象中。这是 Rust 生态的核心原则:用最小范围的 unsafe 实现功能,对外暴露安全 API。

4.1 封装示例:安全的 split_at_mut

标准库的 split_at_mut 可以在一个可变切片中安全地创建两个不重叠的可变切片:

rust 复制代码
use std::slice;

fn split_at_mut_safe<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    assert!(mid <= len, "索引越界");

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut_safe(&mut v, 3);
    left[0] = 10;
    right[0] = 40;
    println!("{:?}", v);   // [10, 2, 3, 40, 5, 6]
}

封装要点:

外部 API 是安全的(无 unsafe)。

assert! 检查前置条件。

unsafe 块只包含必要的操作。

五、FFI:外部函数接口

FFI(Foreign Function Interface) 让 Rust 可以调用其他语言(主要是 C)的函数,也可以被其他语言调用。

5.1 调用 C 函数

rust 复制代码
use std::os::raw::{c_char, c_int};

// 声明外部 C 函数
extern "C" {
    fn strlen(s: *const c_char) -> usize;
    fn puts(s: *const c_char) -> c_int;
}

fn main() {
    let s = std::ffi::CString::new("Hello from Rust!").unwrap();
    unsafe {
        puts(s.as_ptr());
        let len = strlen(s.as_ptr());
        println!("字符串长度: {}", len);
    }
}

C 类型与 Rust 类型的对应:

C 类型 Rust 类型

5.2 导出 Rust 函数给 C

rust 复制代码
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn greet(name: *const std::os::raw::c_char) {
    let name = unsafe { std::ffi::CStr::from_ptr(name) };
    println!("Hello, {}!", name.to_str().unwrap());
}

关键属性:

#no_mangle:阻止 Rust 修改函数名。

extern "C":使用 C 调用约定。

生成 C 头文件:

toml

lib

crate-type = "cdylib" # 生成动态库

编译后生成 .so(Linux)或 .dll(Windows),C 程序可以链接使用。

5.3 处理字符串

Rust 字符串 → C 字符串:

rust 复制代码
use std::ffi::CString;

let rust_str = "hello";
let c_str = CString::new(rust_str).unwrap();
let ptr = c_str.as_ptr();   // *const c_char

C 字符串 → Rust 字符串:

rust 复制代码
use std::ffi::CStr;

let c_str: *const c_char = ...;
let rust_str = unsafe { CStr::from_ptr(c_str) }
    .to_str()
    .unwrap();

⚠️ 注意:CString 拥有所有权,CStr 是借用。CString::new 会检查字符串中是否包含空字节。

六、bindgen 与 cbindgen:自动生成绑定

手动编写 FFI 绑定容易出错。社区提供了两个工具:

6.1 bindgen:从 C 头文件生成 Rust 绑定

bash 复制代码
cargo install bindgen-cli
rust 复制代码
// wrapper.h
#include "mylib.h"
bash 复制代码
bindgen wrapper.h -o src/bindings.rs

生成的 bindings.rs 包含 C 函数和类型的 Rust 声明。

build.rs 中自动生成:

rust 复制代码
// build.rs
fn main() {
    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .generate()
        .expect("生成绑定失败");

    let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("写入绑定失败");
}

6.2 cbindgen:从 Rust 生成 C 头文件

bash 复制代码
cargo install cbindgen
bash 复制代码
cbindgen --config cbindgen.toml --crate my_crate --output my_crate.h

七、实战:封装一个 C 库

假设我们有一个 C 库实现了简单的计算器:

c 复制代码
// calculator.h
typedef struct {
    double result;
    int error_code;
} CalcResult;

CalcResult calc_add(double a, double b);
CalcResult calc_div(double a, double b);

Step 1:创建 Rust 项目

toml

package

name = "calculator"

version = "0.1.0"

edition = "2021"

dependencies

libc = "0.2"

build-dependencies

cc = "1"

Step 2:编写安全封装

rust 复制代码
use std::os::raw::c_double;

#[repr(C)]
struct CalcResult {
    result: c_double,
    error_code: i32,
}

extern "C" {
    fn calc_add(a: c_double, b: c_double) -> CalcResult;
    fn calc_div(a: c_double, b: c_double) -> CalcResult;
}

// ===== 安全抽象 =====

#[derive(Debug)]
pub enum CalcError {
    DivisionByZero,
    Unknown(i32),
}

pub fn add(a: f64, b: f64) -> Result<f64, CalcError> {
    let result = unsafe { calc_add(a, b) };
    check_result(result)
}

pub fn div(a: f64, b: f64) -> Result<f64, CalcError> {
    let result = unsafe { calc_div(a, b) };
    check_result(result)
}

fn check_result(result: CalcResult) -> Result<f64, CalcError> {
    match result.error_code {
        0 => Ok(result.result),
        1 => Err(CalcError::DivisionByZero),
        code => Err(CalcError::Unknown(code)),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2.0, 3.0).unwrap(), 5.0);
    }

    #[test]
    fn test_div_by_zero() {
        assert!(matches!(div(1.0, 0.0), Err(CalcError::DivisionByZero)));
    }
}

Step 3:编译 C 代码(build.rs

rust 复制代码
// build.rs
fn main() {
    cc::Build::new()
        .file("src/calculator.c")
        .compile("calculator");
}

核心设计原则:

unsafe 只出现在最小的范围内。

对外暴露的是安全的 Result<f64, CalcError>。

内部错误码被转换为 Rust 的 enum。

提供单元测试确保封装正确。

八、unsafe 的最佳实践

最小化 unsafe 范围:unsafe 块应尽可能小,只包含必须 unsafe 的操作。

封装为安全抽象:对外暴露安全 API,将 unsafe 隐藏在内部。

文档化 Safety 条件:为 unsafe fn 添加 # Safety 注释。

使用 debug_assert! 验证前置条件:在 debug 模式下检查不变量。

用 Miri 检测未定义行为:cargo +nightly miri test 可以检测 unsafe 代码中的 UB。

优先使用标准库的安全抽象:如 std::slice::from_raw_parts_mut 代替手写指针运算。

考虑使用 bytemuck、zerocopy 等库:它们提供了经过验证的安全类型转换。

8.1 使用 Miri 检测 UB

bash 复制代码
rustup +nightly component add miri
cargo +nightly miri test

Miri 是一个 MIR 解释器,可以检测未定义行为(如越界访问、使用未初始化内存、数据竞争)。

九、小结

unsafe 的五种能力:解引用裸指针、调用 unsafe 函数、访问可变静态变量、实现 unsafe trait、访问 union 字段。

unsafe 不关闭借用检查和类型检查------它只解锁编译器无法验证的操作。

裸指针:*const T / *mut T,可为 null,不受借用规则约束。

FFI:extern "C" 声明外部函数,#no_mangle 导出 Rust 函数。

bindgen / cbindgen:自动生成 C 与 Rust 的绑定代码。

安全抽象:unsafe 代码应封装在安全 API 内部,最小化 unsafe 范围。

Miri:检测 unsafe 代码中的未定义行为。

相关推荐
传奇开心果编程2 小时前
【Rust入门练中学】 第1课:从零开始
开发语言·学习·rust
对象存储与RustFS4 小时前
跨实现迁移 MinIO→RustFS:mc diff 静默返回才是最容易翻车的一步
后端·rust·开源
doiito(Do It Together)5 小时前
【Agent Harness】Gliding Horse 最新进化:从“能学习”到“可验证的自主进化”
人工智能·rust·系统架构·开源
传奇开心果编程15 小时前
【Rust入门练中学】第2课:变量、可变性与常量
开发语言·学习·rust
rustfs15 小时前
MinIO 国产开源平替正式 GA
分布式·docker·云原生·rust
doiito16 小时前
【Agent Harness】Gliding Horse 最新进化:从“能学习”到“可验证的自主进化”
ai·rust·架构设计·ai agent
星栈独行17 小时前
ADK-Rust 是什么?Rust 生态新一代 AI Agent 开发套件
人工智能·后端·rust
传奇开心果编程19 小时前
【Rust入门练中学】 第3课:数据类型
开发语言·学习·rust
孙启超19 小时前
【AI开发之Rust】第 7 课:错误处理 —— panic、Result 与 `?`
人工智能·分布式·后端·爬虫·spring cloud·架构·rust