跳转至

unsafe Rust 与 FFI

unsafe与FFI

📋 章节信息

属性 详情
学习时间 6-8小时
难度等级 ⭐⭐⭐⭐⭐ 高级
前置知识 所有权与借用、智能指针、生命周期、类型系统
核心主题 unsafe代码、裸指针、FFI、安全抽象、UB检测

1. unsafe 的五种超能力

Rust 的安全保证在编译期检查,但有些操作编译器无法验证其安全性。unsafe 关键字提供了五种额外能力:

  1. 解引用裸指针(Raw Pointer Dereference)
  2. 调用 unsafe 函数或方法
  3. 访问或修改可变静态变量
  4. 实现 unsafe trait
  5. 访问 union 的字段(因为编译器无法确定当前存储的是哪个变体)

⚠️ unsafe 不意味着「这段代码是危险的」,而是告诉编译器:「我已经手动验证了安全性,由我来承担责任。」

Rust
// 第5种能力示例:访问 union 字段
union IntOrFloat {
    i: i32,
    f: f32,
}

fn main() {
    let u = IntOrFloat { i: 42 };
    // 读取 union 字段需要 unsafe,因为编译器不知道当前存储的是哪个变体
    // 不变量:调用者必须保证读取的字段与最后写入的字段类型匹配
    let val = unsafe { u.i };
    println!("值: {}", val);
}

1.1 unsafe 块的基本语法

Rust
fn main() {
    // 安全代码
    let x = 42;

    // unsafe 块:开启超能力
    unsafe {
        // 在这里可以使用四种额外能力
        // 其他安全操作在unsafe块中仍然受编译器检查
    }

    // 回到安全代码
    println!("x = {}", x);
}

1.2 unsafe 的边界

Rust
// unsafe 函数:调用者需要保证前置条件
unsafe fn dangerous() {
    // 函数体中的所有代码都处于 unsafe 上下文
    println!("这是一个 unsafe 函数");
}

fn main() {
    // 必须在 unsafe 块中调用 unsafe 函数
    unsafe {
        dangerous();
    }
}

2. 何时使用 unsafe(必要性分析)

2.1 合理使用 unsafe 的场景

Rust
// ✅ 场景1:与C库交互(FFI)
extern "C" {
    fn abs(input: i32) -> i32;
}

fn safe_abs(x: i32) -> i32 {
    unsafe { abs(x) } // 必须用 unsafe 调用外部函数
}

// ✅ 场景2:性能关键路径,跳过边界检查
fn get_unchecked_example(slice: &[i32], index: usize) -> i32 {
    assert!(index < slice.len()); // 手动验证安全性
    unsafe { *slice.get_unchecked(index) }
}

// ✅ 场景3:实现底层数据结构
struct MyVec<T> {
    ptr: *mut T,
    len: usize,
    cap: usize,
}

// ✅ 场景4:内联汇编或硬件访问
fn read_cpu_id() -> u64 {
    let result: u64;
    unsafe {
        std::arch::asm!(
            "mov {}, 0",
            out(reg) result,
        );
    }
    result
}

2.2 不应使用 unsafe 的场景

Rust
// ❌ 错误:仅为了绕过借用检查器
fn bad_example() {
    let mut data = vec![1, 2, 3];
    let ptr = data.as_mut_ptr();
    // 不应该通过裸指针绕过借用规则
    // unsafe { *ptr = 10; } // 这里有更好的安全替代方案
    data[0] = 10; // ✅ 正确做法
}

// ❌ 错误:仅为了避免 Option/Result 处理
fn bad_unwrap(val: Option<i32>) -> i32 {
    // unsafe { val.unwrap_unchecked() } // 不要这样做
    val.unwrap_or(0) // ✅ 正确做法
}

// ❌ 错误:用transmute转换不相关类型
fn bad_transmute() {
    // let x: f64 = unsafe { std::mem::transmute(42u64) }; // 避免这样做
}

2.3 unsafe 使用原则

Rust
/// 使用 unsafe 的黄金准则:
///
/// 1. 最小化 unsafe 代码范围
/// 2. 在 unsafe 块外提供安全 API
/// 3. 文档化所有安全性不变量(safety invariants)
/// 4. 使用 #[deny(unsafe_op_in_unsafe_fn)] 限制隐式 unsafe
/// 5. 为 unsafe 代码编写充分的测试

// 示例:使用属性强制在 unsafe 函数内显式标注 unsafe 操作
#[deny(unsafe_op_in_unsafe_fn)]
unsafe fn careful_function(ptr: *const i32) -> i32 {
    // 即使在 unsafe fn 内,也需要显式 unsafe 块
    // 这样能更精确地标识哪些操作是 unsafe 的
    unsafe { *ptr }
}

3. 裸指针操作

3.1 裸指针基础(*const T / *mut T)

Rust
fn main() {
    let x = 42;
    let y = &x as *const i32;       // 不可变裸指针
    let mut z = 10;
    let w = &mut z as *mut i32;      // 可变裸指针

    // 创建裸指针是安全的,解引用是 unsafe 的
    println!("y 指针地址: {:?}", y);
    println!("w 指针地址: {:?}", w);

    unsafe {
        println!("y 指向的值: {}", *y);
        println!("w 指向的值: {}", *w);
        *w = 20; // 通过可变裸指针修改值
        println!("修改后 z = {}", z);
    }
}

3.2 裸指针与引用的区别

Rust
fn main() {
    // 裸指针的特性:
    // 1. 允许忽略借用规则,可以同时有可变和不可变指针
    let mut num = 5;
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;
    // 安全代码中不允许同时有 &num 和 &mut num,但裸指针可以

    // 2. 不保证指向有效内存
    let address = 0x012345usize;
    let _r = address as *const i32; // 可能指向无效内存

    // 3. 允许为空
    let null_ptr: *const i32 = std::ptr::null();
    assert!(null_ptr.is_null());

    // 4. 没有自动清理
    unsafe {
        println!("r1 = {}", *r1);
        println!("r2 = {}", *r2);
    }
}

3.3 指针运算

Rust
fn main() {
    let arr = [10, 20, 30, 40, 50];
    let ptr = arr.as_ptr();

    unsafe {
        // offset: 按元素偏移
        for i in 0..5 {
            let val = *ptr.offset(i as isize);
            println!("arr[{}] = {}", i, val);
        }

        // add: 无符号偏移(常用)
        let third = *ptr.add(2);
        println!("第三个元素: {}", third);

        // sub: 反向偏移
        let end_ptr = ptr.add(4);
        let second_last = *end_ptr.sub(1);
        println!("倒数第二个: {}", second_last);
    }
}

3.4 指针类型转换

Rust
use std::mem;

fn main() {
    // 指针类型转换(cast)
    let x: i32 = 42;
    let ptr_i32: *const i32 = &x;
    let ptr_u8: *const u8 = ptr_i32 as *const u8;

    unsafe {
        // 读取 i32 的第一个字节
        println!("第一个字节: {}", *ptr_u8);
    }

    // 使用 cast 方法(更推荐)
    let ptr_u8_v2: *const u8 = ptr_i32.cast::<u8>();

    // 对齐指针
    let data: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
    let ptr = data.as_ptr();
    let aligned = unsafe { ptr.add(ptr.align_offset(mem::align_of::<u32>())) };
    println!("对齐后的指针: {:?}", aligned);
}

3.5 NonNull 智能裸指针

Rust
use std::ptr::NonNull;

struct MyBox<T> {
    ptr: NonNull<T>,
}

impl<T> MyBox<T> {
    fn new(val: T) -> Self {
        let boxed = Box::new(val);
        let ptr = Box::into_raw(boxed);
        // NonNull::new_unchecked 要求指针非空(Box保证了这一点)
        MyBox {
            ptr: unsafe { NonNull::new_unchecked(ptr) },
        }
    }

    fn get(&self) -> &T {
        unsafe { self.ptr.as_ref() }
    }

    fn get_mut(&mut self) -> &mut T {
        unsafe { self.ptr.as_mut() }
    }
}

impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        unsafe {
            // 从裸指针恢复 Box,让 Box 负责释放内存
            let _ = Box::from_raw(self.ptr.as_ptr());
        }
    }
}

fn main() {
    let mut my_box = MyBox::new(42);
    println!("值: {}", my_box.get());
    *my_box.get_mut() = 100;
    println!("修改后: {}", my_box.get());
}

4. 手动内存管理

4.1 alloc / dealloc / Layout

Rust
use std::alloc::{self, Layout};

fn main() {
    // 分配 10 个 i32 的内存
    let layout = Layout::array::<i32>(10).unwrap();

    unsafe {
        // 分配内存
        let ptr = alloc::alloc(layout) as *mut i32;
        if ptr.is_null() {
            alloc::handle_alloc_error(layout);
        }

        // 初始化内存
        for i in 0..10 {
            ptr.add(i).write(i as i32 * 10);
        }

        // 读取
        for i in 0..10 {
            println!("data[{}] = {}", i, *ptr.add(i));
        }

        // 释放内存
        alloc::dealloc(ptr as *mut u8, layout);
    }
}

4.2 alloc_zeroed 与 realloc

Rust
use std::alloc::{self, Layout};

fn main() {
    unsafe {
        // 分配零初始化内存
        let layout = Layout::array::<u8>(1024).unwrap();
        let ptr = alloc::alloc_zeroed(layout);

        // 验证内存已清零
        for i in 0..1024 {
            assert_eq!(*ptr.add(i), 0);
        }

        // 重新分配(扩容)
        let new_layout = Layout::array::<u8>(2048).unwrap();
        let new_ptr = alloc::realloc(ptr, layout, new_layout.size());
        if new_ptr.is_null() {
            alloc::handle_alloc_error(new_layout);
        }

        // 新区域未初始化,需要手动清零
        std::ptr::write_bytes(new_ptr.add(1024), 0, 1024);

        alloc::dealloc(new_ptr, new_layout);
    }
}

4.3 全局分配器

Rust
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};

/// 自定义分配器:追踪内存使用
struct TrackingAllocator {
    inner: System,
    allocated: AtomicUsize,
}

unsafe impl GlobalAlloc for TrackingAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let ptr = unsafe { self.inner.alloc(layout) };
        if !ptr.is_null() {
            self.allocated.fetch_add(layout.size(), Ordering::Relaxed);
        }
        ptr
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        unsafe { self.inner.dealloc(ptr, layout) };
        self.allocated.fetch_sub(layout.size(), Ordering::Relaxed);
    }
}

#[global_allocator]
static ALLOCATOR: TrackingAllocator = TrackingAllocator {
    inner: System,
    allocated: AtomicUsize::new(0),
};

fn main() {
    let before = ALLOCATOR.allocated.load(Ordering::Relaxed);
    let v: Vec<i32> = vec![1, 2, 3, 4, 5];
    let after = ALLOCATOR.allocated.load(Ordering::Relaxed);
    println!("Vec 分配了 {} 字节", after - before);
    drop(v);
    let final_count = ALLOCATOR.allocated.load(Ordering::Relaxed);
    println!("释放后剩余 {} 字节", final_count - before);
}

4.4 MaybeUninit

Rust
use std::mem::MaybeUninit;

fn main() {
    // 安全地处理未初始化内存
    let mut data: [MaybeUninit<i32>; 5] = [MaybeUninit::uninit(); 5];

    // 逐个初始化
    for (i, elem) in data.iter_mut().enumerate() {
        elem.write(i as i32 * 10);
    }

    // 转换为已初始化数组
    // 安全性保证:所有元素已被初始化
    let data: [i32; 5] = unsafe {
        // transmute 把 [MaybeUninit<i32>; 5] 转为 [i32; 5]
        std::mem::transmute(data)
    };

    println!("{:?}", data); // [0, 10, 20, 30, 40]
}

// 更安全的方式:使用 assume_init
fn create_array() -> [String; 3] {
    let mut arr: [MaybeUninit<String>; 3] = [
        MaybeUninit::uninit(),
        MaybeUninit::uninit(),
        MaybeUninit::uninit(),
    ];

    arr[0].write(String::from("hello"));
    arr[1].write(String::from("world"));
    arr[2].write(String::from("!"));

    // 使用 map 逐个 assume_init
    arr.map(|x| unsafe { x.assume_init() })
}

5. FFI 与 C 互操作

5.1 调用 C 函数

Rust
// 声明 C 函数签名
extern "C" {
    fn strlen(s: *const std::os::raw::c_char) -> usize;
    fn printf(format: *const std::os::raw::c_char, ...) -> i32;
}

fn main() {
    // CString 确保以 null 结尾
    let c_str = std::ffi::CString::new("Hello, FFI!").unwrap();

    unsafe {
        let len = strlen(c_str.as_ptr());
        println!("C strlen 返回: {}", len);
    }
}

5.2 C 类型映射

Rust
// Rust 与 C 类型对照表
// C 类型        -> Rust 类型
// int           -> std::os::raw::c_int (i32)
// unsigned int  -> std::os::raw::c_uint (u32)
// long          -> std::os::raw::c_long
// char          -> std::os::raw::c_char
// float         -> f32
// double        -> f64
// void*         -> *mut std::os::raw::c_void
// const char*   -> *const std::os::raw::c_char
// size_t        -> usize
// bool          -> bool (C99)

use std::os::raw::{c_char, c_int, c_void};
use std::ffi::{CStr, CString};

extern "C" {
    fn atoi(s: *const c_char) -> c_int;
    fn malloc(size: usize) -> *mut c_void;
    fn free(ptr: *mut c_void);
}

fn safe_atoi(s: &str) -> Option<i32> {
    let c_str = CString::new(s).ok()?;
    Some(unsafe { atoi(c_str.as_ptr()) })
}

fn main() {
    if let Some(num) = safe_atoi("42") {
        println!("解析结果: {}", num);
    }
}

5.3 CString 与 CStr

Rust
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

fn main() {
    // Rust -> C: 使用 CString
    let rust_string = String::from("Hello from Rust");
    let c_string = CString::new(rust_string).unwrap();
    let c_ptr: *const c_char = c_string.as_ptr();
    // c_ptr 可以传递给 C 函数

    // C -> Rust: 使用 CStr
    unsafe {
        let c_str: &CStr = CStr::from_ptr(c_ptr);
        let rust_str: &str = c_str.to_str().unwrap();
        println!("从 C 指针恢复: {}", rust_str);
    }

    // 注意:CString 拥有内存,CStr 只是借用
    // CString 在 drop 时释放内存
    // 如果 C 函数返回的指针需要由 C 释放,不要用 CString::from_raw
}

5.4 #[no_mangle] 与导出函数

Rust
// 供 C 代码调用的 Rust 函数
// #[no_mangle] 防止 Rust 编译器修改函数名
// extern "C" 使用 C ABI

/// 计算两个整数的和
///
/// # Safety
/// 此函数通过 FFI 调用,调用者需保证传入有效的 i32 值
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

/// 创建一个字符串并返回指针
/// 调用者需要使用 rust_free_string 释放内存
#[no_mangle]
pub extern "C" fn rust_greeting(name: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
    let c_str = unsafe {
        assert!(!name.is_null());
        std::ffi::CStr::from_ptr(name)
    };
    let name = c_str.to_str().unwrap_or("World");
    let greeting = format!("Hello, {}!", name);
    std::ffi::CString::new(greeting).unwrap().into_raw()
}

/// 释放 Rust 分配的字符串
///
/// # Safety
/// ptr 必须是由 rust_greeting 返回的有效指针
#[no_mangle]
pub unsafe extern "C" fn rust_free_string(ptr: *mut std::os::raw::c_char) {
    if !ptr.is_null() {
        unsafe {
            let _ = std::ffi::CString::from_raw(ptr);
            // CString 的 drop 会释放内存
        }
    }
}

5.5 使用 bindgen 自动生成绑定

TOML
# Cargo.toml
[build-dependencies]
bindgen = "0.71"
Rust
// build.rs
fn main() {
    // 生成 C 头文件的 Rust 绑定
    let bindings = bindgen::Builder::default()
        .header("wrapper.h")  // C 头文件
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .generate()
        .expect("无法生成绑定");

    // 输出到 $OUT_DIR/bindings.rs
    let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("无法写入绑定文件");
}
C
// wrapper.h - C 头文件示例
#ifndef MY_LIB_H
#define MY_LIB_H

typedef struct {
    int x;
    int y;
} Point;

Point point_new(int x, int y);
double point_distance(const Point* a, const Point* b);
void point_free(Point* p);

#endif
Rust
// src/lib.rs - 使用生成的绑定
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

// 引入 bindgen 生成的绑定
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

// 创建安全的 Rust 封装
pub struct SafePoint {
    inner: Point,
}

impl SafePoint {
    pub fn new(x: i32, y: i32) -> Self {
        let inner = unsafe { point_new(x, y) };
        SafePoint { inner }
    }

    pub fn distance(&self, other: &SafePoint) -> f64 {
        unsafe { point_distance(&self.inner, &other.inner) }
    }
}

5.6 使用 cbindgen 生成 C 头文件

TOML
# Cargo.toml
[lib]
crate-type = ["cdylib"]

[build-dependencies]
cbindgen = "0.27"
Rust
// build.rs
fn main() {
    let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
    cbindgen::Builder::new()
        .with_crate(crate_dir)
        .with_language(cbindgen::Language::C)
        .generate()
        .expect("无法生成 C 头文件")
        .write_to_file("include/mylib.h");
}
Rust
// src/lib.rs - 标注 #[repr(C)] 确保内存布局与 C 兼容

/// 二维向量
#[repr(C)]
pub struct Vec2 {
    pub x: f64,
    pub y: f64,
}

/// 创建新向量
#[no_mangle]
pub extern "C" fn vec2_new(x: f64, y: f64) -> Vec2 {
    Vec2 { x, y }
}

/// 计算向量长度
#[no_mangle]
pub extern "C" fn vec2_length(v: &Vec2) -> f64 {
    (v.x * v.x + v.y * v.y).sqrt()
}

/// 向量相加
#[no_mangle]
pub extern "C" fn vec2_add(a: &Vec2, b: &Vec2) -> Vec2 {
    Vec2 {
        x: a.x + b.x,
        y: a.y + b.y,
    }
}

6. 安全抽象:将 unsafe 封装为安全 API

6.1 安全抽象的设计原则

Rust
/// 安全抽象示例:自定义切片分割
///
/// 标准库的 split_at_mut 就是一个安全抽象的典范
fn split_at_mut<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    // 前置条件检查(在 safe 代码中完成)
    assert!(mid <= len, "mid ({}) > len ({})", mid, len);

    // unsafe 块:仅包含必需的 unsafe 操作
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

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

6.2 类型状态模式保证安全

Rust
use std::marker::PhantomData;

// 类型状态标记
struct Locked;
struct Unlocked;

/// 保险箱:使用类型状态确保只有解锁后才能读取内容
struct Safe<State> {
    content: String,
    _state: PhantomData<State>,
}

impl Safe<Locked> {
    fn new(content: String) -> Self {
        Safe {
            content,
            _state: PhantomData,
        }
    }

    fn unlock(self, password: &str) -> Result<Safe<Unlocked>, Safe<Locked>> {
        if password == "secret" {
            Ok(Safe {
                content: self.content,
                _state: PhantomData,
            })
        } else {
            Err(self)
        }
    }
}

impl Safe<Unlocked> {
    fn read(&self) -> &str {
        &self.content
    }

    fn lock(self) -> Safe<Locked> {
        Safe {
            content: self.content,
            _state: PhantomData,
        }
    }
}

fn main() {
    let safe = Safe::new("机密文件".to_string());
    // safe.read(); // ❌ 编译错误:Safe<Locked> 没有 read 方法

    match safe.unlock("secret") {
        Ok(unlocked) => {
            println!("内容: {}", unlocked.read()); // ✅ 可以读取
            let _locked = unlocked.lock();
        }
        Err(_) => println!("密码错误"),
    }
}

6.3 封装 unsafe 集合

Rust
use std::alloc::{self, Layout};
use std::ptr;

/// 简易的固定大小栈
///
/// # Safety Invariants (安全不变量)
/// - `data` 指向一块已分配的、对齐的、容量为 `capacity` 个 `T` 的内存
/// - `len <= capacity`
/// - `data[0..len]` 中的所有元素均已初始化且有效
/// - `data[len..capacity]` 中的内存未初始化
pub struct FixedStack<T> {
    data: *mut T,
    len: usize,
    capacity: usize,
}

impl<T> FixedStack<T> {
    /// 创建指定容量的栈
    pub fn new(capacity: usize) -> Self {
        assert!(capacity > 0, "容量必须大于0");
        let layout = Layout::array::<T>(capacity).unwrap();
        let data = unsafe { alloc::alloc(layout) as *mut T };
        if data.is_null() {
            alloc::handle_alloc_error(layout);
        }
        FixedStack {
            data,
            len: 0,
            capacity,
        }
    }

    /// 压栈(安全 API)
    pub fn push(&mut self, val: T) -> Result<(), T> {
        if self.len >= self.capacity {
            return Err(val); // 栈满,返回值
        }
        unsafe {
            self.data.add(self.len).write(val);
        }
        self.len += 1;
        Ok(())
    }

    /// 弹栈(安全 API)
    pub fn pop(&mut self) -> Option<T> {
        if self.len == 0 {
            return None;
        }
        self.len -= 1;
        Some(unsafe { self.data.add(self.len).read() })
    }

    /// 查看栈顶
    pub fn peek(&self) -> Option<&T> {
        if self.len == 0 {
            return None;
        }
        Some(unsafe { &*self.data.add(self.len - 1) })
    }

    pub fn len(&self) -> usize {
        self.len
    }

    pub fn is_empty(&self) -> bool {
        self.len == 0
    }
}

impl<T> Drop for FixedStack<T> {
    fn drop(&mut self) {
        // 逐个 drop 元素
        while self.pop().is_some() {}

        // 释放内存
        let layout = Layout::array::<T>(self.capacity).unwrap();
        unsafe {
            alloc::dealloc(self.data as *mut u8, layout);
        }
    }
}

// Safety: FixedStack 独占 data 指向的内存,与 Box<[T]> 的所有权语义一致。
// 如果 T: Send,则 FixedStack<T> 可以安全地跨线程传递。
unsafe impl<T: Send> Send for FixedStack<T> {}
// Safety: FixedStack 只通过 &self 提供只读访问(peek),
// 如果 T: Sync,则多线程只读访问是安全的。
unsafe impl<T: Sync> Sync for FixedStack<T> {}

fn main() {
    let mut stack = FixedStack::new(3);
    stack.push(1).unwrap();
    stack.push(2).unwrap();
    stack.push(3).unwrap();
    assert!(stack.push(4).is_err()); // 栈满

    while let Some(val) = stack.pop() {
        println!("popped: {}", val);
    }
}

7. Pin 与自引用结构

7.1 自引用结构的问题

Rust
/// 自引用结构:一个字段引用另一个字段
/// 这在 Rust 中非常棘手,因为移动会使引用失效

// ❌ 这个结构体无法安全工作
struct SelfRef {
    data: String,
    // ptr 指向 data 的内容,但如果 SelfRef 被移动,ptr 就会悬空
    ptr: *const String,
}

impl SelfRef {
    fn new(data: String) -> Self {
        let mut s = SelfRef {
            data,
            ptr: std::ptr::null(),
        };
        s.ptr = &s.data; // 指向自身
        s
        // ⚠️ 返回时 s 被移动,ptr 变成悬空指针!
    }
}

7.2 Pin 的作用

Rust
use std::pin::Pin;
use std::marker::PhantomPinned;

/// 使用 Pin 的自引用结构
struct PinnedSelfRef {
    data: String,
    slice: *const str,
    _pin: PhantomPinned, // 标记为 !Unpin
}

impl PinnedSelfRef {
    fn new(data: String) -> Pin<Box<Self>> {
        let res = PinnedSelfRef {
            data,
            slice: std::ptr::null(),
            _pin: PhantomPinned,
        };
        let mut boxed = Box::pin(res);

        // 初始化自引用
        let slice: *const str = &boxed.data;
        unsafe {
            let mut_ref = Pin::as_mut(&mut boxed);
            Pin::get_unchecked_mut(mut_ref).slice = slice;
        }

        boxed
    }

    fn data(&self) -> &str {
        &self.data
    }

    fn slice(&self) -> &str {
        unsafe { &*self.slice }
    }
}

fn main() {
    let pinned = PinnedSelfRef::new("Hello, Pin!".to_string());
    println!("data: {}", pinned.data());
    println!("slice: {}", pinned.slice());
    // pinned 不能被移动,自引用始终有效
}

7.3 Pin 与 Future

Rust
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

/// 自定义 Future 示例
struct Delay {
    duration: std::time::Duration,
    started: bool,
}

impl Delay {
    fn new(duration: std::time::Duration) -> Self {
        Delay {
            duration,
            started: false,
        }
    }
}

impl Future for Delay {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // Pin<&mut Self> 保证 self 不会被移动
        let this = self.get_mut();
        if !this.started {
            this.started = true;
            // 实际实现中会注册定时器
            let waker = cx.waker().clone();
            let duration = this.duration;
            std::thread::spawn(move || {
                std::thread::sleep(duration);
                waker.wake();
            });
            Poll::Pending
        } else {
            Poll::Ready(())
        }
    }
}

7.4 pin! 宏的使用

Rust
use std::pin::pin;

async fn example() {
    let future = async { 42 };

    // pin! 宏将值固定在栈上
    let pinned = pin!(future);

    // pinned 的类型是 Pin<&mut impl Future<Output = i32>>
    // 可以安全地 poll
    println!("Future 已被 pin 到栈上");
}

// 在容器中使用 Pin
fn collect_futures() -> Vec<Pin<Box<dyn Future<Output = i32>>>> {
    vec![
        Box::pin(async { 1 }),
        Box::pin(async { 2 }),
        Box::pin(async { 3 }),
    ]
}

8. 常见 unsafe 陷阱和 UB(未定义行为)

8.1 什么是未定义行为(UB)

Rust
/// Rust 中的未定义行为列表:
/// 1. 解引用悬空指针或空指针
/// 2. 读取未初始化内存
/// 3. 违反别名规则(同时存在可变和不可变引用)
/// 4. 产生无效的基本类型值(如 bool 不是 0 或 1)
/// 5. 数据竞争
/// 6. 解引用未对齐的指针
/// 7. 调用 ABI 不兼容的函数
/// 8. 违反类型的不变量(如 &T 必须对齐且非空)

// 示例:各种 UB
fn ub_examples() {
    // ❌ UB 1: 悬空指针
    let ptr: *const i32 = {
        let x = 42;
        &x as *const i32
    }; // x 已被销毁,ptr 悬空
    // unsafe { println!("{}", *ptr); } // UB!

    // ❌ UB 2: 读取未初始化内存
    // let x: i32;
    // println!("{}", x); // 编译器会阻止,但 unsafe 中可能发生

    // ❌ UB 3: 违反别名规则
    let mut data = vec![1, 2, 3];
    let ptr = data.as_mut_ptr();
    // 同时通过指针和引用访问
    // unsafe { *ptr = 10; }
    // println!("{}", data[0]); // 可能 UB(取决于具体使用方式)

    // ❌ UB 4: 无效的 bool 值
    // let b: bool = unsafe { std::mem::transmute(2u8) }; // UB! bool 只能是 0 或 1
}

8.2 use-after-free

Rust
fn use_after_free_example() {
    let ptr: *const String;

    {
        let s = String::from("hello");
        ptr = &s as *const String;
        // s 在这里被 drop
    }

    // ❌ ptr 现在是悬空指针
    // unsafe { println!("{}", *ptr); } // use-after-free, UB!
}

// ✅ 安全替代方案
fn safe_alternative() {
    let s = String::from("hello");
    let r: &String = &s; // 借用检查器保证 r 不会比 s 活得久
    println!("{}", r);
}

8.3 数据竞争

Rust
use std::thread;

// ❌ 数据竞争 UB 示例(概念性代码,不要运行)
fn data_race_example() {
    let mut data = 0u32;
    let ptr = &mut data as *mut u32;

    // 多线程通过裸指针访问同一数据 = 数据竞争
    // let ptr_clone = ptr as usize;
    // thread::spawn(move || {
    //     let p = ptr_clone as *mut u32;
    //     unsafe { *p += 1; } // UB: 数据竞争
    // });
    // unsafe { *ptr += 1; } // UB: 数据竞争
}

// ✅ 安全替代方案:使用原子操作或 Mutex
use std::sync::{Arc, Mutex};

fn safe_concurrent() {
    let data = Arc::new(Mutex::new(0u32));

    let handles: Vec<_> = (0..10)
        .map(|_| {
            let data = Arc::clone(&data);
            thread::spawn(move || {
                let mut guard = data.lock().unwrap();
                *guard += 1;
            })
        })
        .collect();

    for h in handles {
        h.join().unwrap();
    }

    println!("结果: {}", *data.lock().unwrap());
}

8.4 transmute 的危险

Rust
use std::mem;

fn transmute_dangers() {
    // ❌ 危险:不同大小的类型
    // let x: u64 = unsafe { mem::transmute(0u32) }; // 编译错误,大小不同

    // ❌ 危险:创建无效的枚举值
    #[repr(u8)]
    enum Color {
        Red = 0,
        Green = 1,
        Blue = 2,
    }
    // let c: Color = unsafe { mem::transmute(42u8) }; // UB! 42 不是有效的 Color

    // ✅ 使用 transmute_copy 和验证
    fn from_u8(val: u8) -> Option<Color> {
        match val {
            0 => Some(Color::Red),
            1 => Some(Color::Green),
            2 => Some(Color::Blue),
            _ => None,
        }
    }

    // ✅ 安全的类型转换
    let bytes: [u8; 4] = [0x78, 0x56, 0x34, 0x12];
    let num = u32::from_le_bytes(bytes); // 使用专用函数代替 transmute
    println!("0x{:08X}", num);
}

8.5 未对齐访问

Rust
fn unaligned_access() {
    let data: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8];

    // ❌ 可能未对齐
    // let ptr = &data[1] as *const u8 as *const u32;
    // unsafe { println!("{}", *ptr); } // UB: 可能未对齐

    // ✅ 使用 read_unaligned
    let ptr = &data[1] as *const u8 as *const u32;
    let val = unsafe { ptr.read_unaligned() };
    println!("未对齐读取: {}", val);

    // ✅ 使用 from_ne_bytes
    let bytes = &data[1..5];
    let val = u32::from_ne_bytes(bytes.try_into().unwrap());
    println!("安全读取: {}", val);
}

9. Miri 工具检测 UB

9.1 安装和使用 Miri

Bash
# 安装 Miri
rustup +nightly component add miri

# 运行 Miri 检测 UB
cargo +nightly miri test

# 运行特定测试
cargo +nightly miri test test_name

# 运行程序
cargo +nightly miri run

# 设置 Miri 标志
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test

9.2 Miri 能检测的 UB

Rust
#[cfg(test)]
mod tests {
    // Miri 可以检测的问题:
    // ✓ use-after-free
    // ✓ 越界访问
    // ✓ 使用未初始化数据
    // ✓ 违反别名规则(Stacked Borrows / Tree Borrows)
    // ✓ 内存泄漏
    // ✓ 数据竞争
    // ✓ 未对齐的指针访问
    // ✓ 无效的枚举/bool值

    #[test]
    fn test_valid_unsafe() {
        // 有效的 unsafe 代码,Miri 应通过
        let mut data = vec![1, 2, 3, 4, 5];
        let ptr = data.as_mut_ptr();

        unsafe {
            for i in 0..5 {
                *ptr.add(i) *= 2;
            }
        }

        assert_eq!(data, vec![2, 4, 6, 8, 10]);
    }

    #[test]
    fn test_out_of_bounds() {
        // Miri 会检测到越界访问
        let data = vec![1, 2, 3];
        let ptr = data.as_ptr();

        unsafe {
            let _valid = *ptr.add(2); // OK
            // let _invalid = *ptr.add(3); // Miri 会报错!
        }
    }

    #[test]
    fn test_aliasing() {
        // 测试别名规则
        let mut data = 42i32;
        let ptr1 = &mut data as *mut i32;

        unsafe {
            *ptr1 = 100;
            assert_eq!(*ptr1, 100);
        }
    }
}

9.3 Miri 配置

TOML
# .cargo/config.toml
[target.'cfg(miri)']
runner = "cargo miri"

# 环境变量配置
# MIRIFLAGS="-Zmiri-tag-gc=1000"          # GC频率
# MIRIFLAGS="-Zmiri-tree-borrows"         # 使用 Tree Borrows 模型
# MIRIFLAGS="-Zmiri-disable-stacked-borrows" # 禁用 Stacked Borrows
Rust
// 在代码中检测是否在 Miri 下运行
#[cfg(miri)]
fn running_under_miri() {
    println!("当前在 Miri 下运行");
    // Miri 下跳过某些测试(如网络/文件IO)
}

#[cfg(not(miri))]
fn running_under_miri() {
    println!("正常运行");
}

// 条件编译测试
#[cfg(test)]
mod tests {
    #[test]
    #[cfg_attr(miri, ignore)] // Miri 下忽略(例如需要网络的测试)
    fn test_network() {
        // 网络测试...
    }

    #[test]
    fn test_memory_safety() {
        // 这个测试在 Miri 下运行
        let v = vec![1, 2, 3];
        assert_eq!(v[1], 2);
    }
}

10. 面试题

Q1: 为什么 Rust 需要 unsafe?它提供了哪些能力?

A1:

Rust 的安全保证依赖编译期静态分析,但有些操作的安全性无法在编译期验证:

Rust
// unsafe 提供五种"超能力":

// 1. 解引用裸指针
let x = 42;
let ptr = &x as *const i32;
let val = unsafe { *ptr };

// 2. 调用 unsafe 函数
unsafe fn dangerous_fn() {}
unsafe { dangerous_fn(); }

// 3. 访问或修改可变静态变量
static mut COUNTER: i32 = 0;
unsafe { COUNTER += 1; }

// 4. 实现 unsafe trait
unsafe trait Scary {}
unsafe impl Scary for i32 {}

// 5. 访问 union 的字段
union MyUnion { i: i32, f: f32 }
let u = MyUnion { i: 1 };
let val = unsafe { u.i };

需要 unsafe 的根本原因: - FFI 调用(与 C/C++ 库交互) - 实现底层数据结构(如 VecLinkedList) - 直接硬件访问(嵌入式、OS开发) - 性能关键路径(跳过边界检查)

核心理念: unsafe 不是"不安全的代码",而是"编译器无法验证安全性、由程序员负责保证"的代码。


Q2: 如何安全地封装 unsafe 代码?

A2:

安全封装的核心原则:对外暴露安全 API,内部用 unsafe 实现。

Rust
/// 安全封装示例:从两个不可变切片创建可变切片
/// 前提:两个范围不重叠
pub fn split_at_mut<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    // 1. 在安全代码中验证前置条件
    assert!(mid <= len);

    // 2. unsafe 块最小化
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
    // 3. 返回安全类型(&mut [T]),调用者无需 unsafe
}

封装检查清单: 1. 前置条件在 safe 代码中验证(assert! / 返回 Result) 2. unsafe 块尽可能小 3. 文档化 safety invariants(/// # Safety 注释) 4. 返回安全类型 5. 编写测试 + Miri 检测


Q3: 什么是未定义行为(UB)?列举常见的 UB

A3:

Rust
// UB = 编译器不保证程序行为的代码,可能导致任何结果

// 常见 UB 列表:

// 1. 解引用空指针或悬空指针
let ptr: *const i32 = std::ptr::null();
// unsafe { *ptr }; // UB

// 2. 读取未初始化内存
// let x: i32 = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; // UB

// 3. 创建无效值
// let b: bool = unsafe { std::mem::transmute(2u8) }; // UB (bool 只能是 0 或 1)

// 4. 违反别名规则
// 同时存在 &T 和 &mut T 指向同一数据

// 5. 数据竞争
// 多线程无同步地同时读写同一内存

// 6. 未对齐的引用
// let p: &u32 = unsafe { &*(addr as *const u32) }; // 如果 addr 未对齐则 UB

// 7. 违反函数前置条件
// unsafe { "hello".get_unchecked(10..) }; // 越界 UB

检测工具: Miri(cargo +nightly miri test)可以检测大部分 UB。


Q4: 裸指针(*const T / *mut T)与引用(&T / &mut T)有什么区别?

A4:

Rust
fn main() {
    let mut x = 42;

    // 引用的约束:
    let r: &i32 = &x;       // 1. 必须有效(非空、非悬空)
    // let r2: &mut i32 = &mut x; // 2. 借用规则(不能同时存在&和&mut)
                              // 3. 自动 deref
                              // 4. 有生命周期

    // 裸指针的自由:
    let p1: *const i32 = &x; // 1. 可以为空
    let p2: *mut i32 = &mut x; // 2. 可以同时有 *const 和 *mut
                              // 3. 不安全 deref(需要 unsafe)
                              // 4. 没有生命周期
                              // 5. 可以进行指针运算

    // 创建裸指针是安全的,使用裸指针才需要 unsafe
    println!("指针地址: {:?}", p1);
    unsafe {
        println!("值: {}", *p1);
    }
}
特性 引用 &T / &mut T 裸指针 *const T / *mut T
空值 不允许 允许
对齐 保证对齐 不保证
有效性 保证有效 不保证
别名规则 编译器检查 程序员负责
生命周期
解引用 安全 unsafe

Q5: FFI 中如何正确处理字符串?

A5:

Rust
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

// Rust -> C: 使用 CString
fn send_to_c(s: &str) {
    // CString::new 会追加 \0,并检查字符串中没有内部 \0
    let c_string = CString::new(s).expect("字符串包含内部 null 字节");
    let ptr: *const c_char = c_string.as_ptr();
    // ptr 在 c_string 存活期间有效
    unsafe { some_c_function(ptr); }
}

// C -> Rust: 使用 CStr
unsafe fn receive_from_c(ptr: *const c_char) -> String {
    // CStr::from_ptr 会找到 \0 终止符
    let c_str = unsafe { CStr::from_ptr(ptr) };
    // to_string_lossy 处理非 UTF-8 数据
    c_str.to_string_lossy().into_owned()
}

extern "C" { fn some_c_function(s: *const c_char); }

要点: - CString(拥有所有权)vs CStr(借用) - Rust 字符串是 UTF-8、无 null 终止;C 字符串是 null 终止、可能非 UTF-8 - 注意 CString 的生命周期,不要返回其内部指针后立即 drop


Q6: Pin 解决了什么问题?async/await 为什么需要 Pin?

A6:

Rust
use std::pin::Pin;
use std::marker::PhantomPinned;

// 问题:自引用结构移动后,内部指针悬空
struct SelfRef {
    data: String,
    ptr: *const String, // 指向 self.data
}
// 如果 SelfRef 被移动(如 Vec 扩容),ptr 变成悬空指针

// async/await 编译后生成的 Future 是自引用结构:
async fn example() {
    let data = String::from("hello");
    some_async_op(&data).await; // await 点跨越了 data 的借用
    println!("{}", data);       // data 在 await 后仍被使用
}
// 编译器生成的状态机结构体中,同时保存了 data 和对 data 的引用
// → 这就是自引用结构

// Pin 的解决方案:
// Pin<P> 保证 P 指向的值不会被移动(除非实现 Unpin)
// Future::poll 签名使用 Pin<&mut Self>

async fn some_async_op(_: &str) {}

// 核心规则:
// - Unpin 类型可以自由移动(大部分标准类型都是 Unpin)
// - !Unpin 类型被 Pin 后不能移动(自引用结构标记为 !Unpin)
// - PhantomPinned 是标记 !Unpin 的零大小标记类型

✅ 学习检查清单

基础概念

  • 理解 unsafe 的五种超能力(裸指针、unsafe函数、可变静态变量、unsafe trait、union字段访问)
  • 知道何时应该和不应该使用 unsafe
  • 理解 unsafe 块的作用域和边界

裸指针

  • 掌握 *const T*mut T 的创建和使用
  • 理解裸指针与引用的区别
  • 会进行指针运算(offset/add/sub)
  • 了解 NonNull<T> 的用途

内存管理

  • 掌握 alloc / dealloc / Layout
  • 理解 MaybeUninit 的作用
  • 了解全局分配器 GlobalAlloc

FFI

  • 掌握 extern "C" 声明和调用 C 函数
  • 熟练使用 CString / CStr 处理字符串
  • 了解 #[no_mangle]#[repr(C)]
  • 知道 bindgen / cbindgen 工具的用途

安全抽象

  • 能将 unsafe 代码封装为安全 API
  • 理解 safety invariants 的概念
  • 会文档化 unsafe 代码的安全性要求

Pin 与自引用

  • 理解自引用结构的问题
  • 掌握 Pin 的作用和使用方式
  • 理解 Unpin trait
  • 知道 async/await 为何需要 Pin

UB 检测

  • 能列举常见的 UB 类型
  • 会使用 Miri 检测 UB
  • 了解 Miri 的配置选项

📌 核心要点: unsafe 不是逃生门,而是一种契约。编写 unsafe 代码意味着你在向编译器保证"我已经验证了安全性"。最小化 unsafe 范围、充分文档化、使用 Miri 测试,是写好 unsafe Rust 的三大原则。