10Rust程序设计语言

Unsafe Rust

突破安全边界

unsafe裸指针FFI

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第十章 Unsafe Rust (Unsafe Rust)

一、导读

1.1 本章学习目标

理解 Unsafe Rust 的设计哲学——它是安全抽象的底层基础,而非鼓励不安全的编程

掌握 Unsafe Rust 的五种"超级能力"及其使用场景

理解裸指针(Raw Pointers)的类型、创建和解引用规则

掌握 FFI(Foreign Function Interface)的调用约定,能够与 C 语言库互操作

理解 unsafe 块的作用域规则和安全边界的封装方法

掌握 UnsafeCell<T> 的工作原理及其作为内部可变性基础的作用

了解编写安全的 Unsafe 抽象的最佳实践和验证方法

能够阅读和理解标准库中的 unsafe 代码

1.2 为什么本章如此重要

Unsafe Rust 是 Rust 语言中最具争议也最被误解的特性。许多开发者将 unsafe 等同于"不安全"或"危险",从而完全回避它。然而事实恰恰相反——Rust 标准库中大量的核心抽象(Vec<T>、HashMap、Box<T>、Rc<T>、Cell<T> 等)都是在 unsafe 块中实现的。没有 unsafe,Rust 就无法与操作系统交互、无法实现高性能的数据结构、无法与其他语言的库互操作。

理解 Unsafe Rust 的关键在于区分两个层次:安全抽象(Safe Abstraction)和不安全实现(Unsafe Implementation)。Vec<T> 对外是安全的——你可以随意使用它而不会导致内存错误。但它的内部实现使用了裸指针和手动内存管理,这些操作在 unsafe 块中完成。Vec 的作者承担了验证安全性的责任,使得使用者不需要理解底层的 unsafe 细节。

本章将系统地讲解 Unsafe Rust 的五种超级能力:解引用裸指针、调用 unsafe 函数或方法、访问或修改可变静态变量、实现 unsafe trait、访问 union 的字段。我们还将深入探讨 FFI 的使用、UnsafeCell 的工作原理,以及编写安全 unsafe 抽象的最佳实践。

学习本章内容的目的不是鼓励你在日常编程中使用 unsafe,而是让你理解 Rust 安全抽象的底层机制,能够阅读和理解标准库和第三方库中的 unsafe 代码,并在确实需要时能够正确地编写 unsafe 代码。

1.3 核心问题引导

Unsafe Rust 的五种超级能力分别是什么?为什么需要它们?

裸指针(*const T 和 *mut T)与引用(&T 和 &mut T)有什么本质区别?

FFI 中如何保证 Rust 和 C 之间的数据类型兼容?repr(C) 的作用是什么?

UnsafeCell<T> 为什么是 Rust 内部可变性的基础?它与 Cell<T> 和 RefCell<T> 是什么关系?

如何编写一个"安全的 unsafe 抽象"?需要满足哪些条件?

Miri 工具如何帮助验证 unsafe 代码的正确性?


二、核心概念详解

10.1 Unsafe 的五种超级能力

在 unsafe 块中,Rust 允许以下五种在安全 Rust 中被禁止的操作:

解引用裸指针(Dereference a raw pointer)

调用 unsafe 函数或方法(Call an unsafe function or method)

访问或修改可变静态变量(Access or modify a mutable static variable)

实现 unsafe trait(Implement an unsafe trait)

访问 union 的字段(Access fields of a union)

rust
unsafe {
    // 1. 解引用裸指针
    let raw_ptr: *const i32 = &42;
    let value = *raw_ptr;
    
    // 2. 调用 unsafe 函数
    // dangerous();
    
    // 3. 修改可变静态变量
    // static mut COUNTER: u32 = 0;
    // COUNTER += 1;
}

unsafe 不意味着不安全:unsafe 块只是解除了编译器的某些检查。编写 unsafe 代码的开发者有责任确保代码实际上不会导致未定义行为(Undefined Behavior, UB)。如果 unsafe 代码违反了其文档中规定的安全条件(Safety Contract),则整个程序的行为都是未定义的——即使安全代码部分也可能出错。

10.2 裸指针

裸指针分为 *const T(不可变裸指针)和 *mut T(可变裸指针)。与引用不同,裸指针:

  • 可以指向空地址(null)
  • 不保证指向有效的内存
  • 不参与借用检查,编译器不追踪其生命周期
  • 可以任意读写(*mut T)
rust
let mut num = 5;

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

// 直接创建裸指针(可能指向无效内存)
let address = 0x7fff5d2a2c3c as *const i32;

unsafe {
    println!("r1: {}", *r1);
    println!("r2: {}", *r2);
    // println!("address: {}", *address); // 可能段错误
}

裸指针的使用场景:裸指针主要用于实现底层数据结构(如链表、B+树)、FFI 交互和与 C 库的互操作。在安全抽象的内部,裸指针是管理内存和实现高性能数据结构的基本工具。

10.3 FFI:与 C 语言互操作

FFI(Foreign Function Interface)允许 Rust 调用 C 函数,也允许 C 调用 Rust 函数。

调用 C 函数:

rust
// 声明外部 C 函数
extern "C" {
    fn abs(input: i32) -> i32;
    fn printf(format: *const i8, ...) -> i32;
}

fn main() {
    unsafe {
        println!("abs(-3) = {}", abs(-3));
    }
}

导出 Rust 函数供 C 调用:

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

`repr(C)` 属性:确保 Rust 结构体的内存布局与 C 兼容:

rust
#[repr(C)]
struct Point {
    x: f64,
    y: f64,
}

extern "C" {
    fn process_point(p: *const Point) -> f64;
}

bindgen 工具:bindgen 可以自动从 C 头文件生成 Rust FFI 绑定,大大减少了手动编写 FFI 声明的工作量。

10.4 可变静态变量

Rust 允许定义全局可变变量(static mut),但访问它们是 unsafe 的:

rust
static mut GLOBAL_COUNTER: u32 = 0;

fn increment() {
    unsafe {
        GLOBAL_COUNTER += 1;
    }
}

fn get_count() -> u32 {
    unsafe { GLOBAL_COUNTER }
}

线程安全问题:static mut 不是线程安全的。在多线程环境中访问可变静态变量需要额外的同步机制。通常推荐使用 AtomicU32 或 Mutex 替代 static mut。

rust
use std::sync::atomic::{AtomicU32, Ordering};

static COUNTER: AtomicU32 = AtomicU32::new(0);

fn increment() {
    COUNTER.fetch_add(1, Ordering::SeqCst);
}

10.5 UnsafeCell 与内部可变性

UnsafeCell<T> 是 Rust 中所有内部可变性(Interior Mutability)的基础。它是唯一一个被编译器特殊对待的类型——&UnsafeCell<T> 允许在共享引用的同时修改内部数据。

rust
use std::cell::UnsafeCell;

struct SharedCounter {
    value: UnsafeCell<i32>,
}

// 安全地封装 UnsafeCell
impl SharedCounter {
    fn new(value: i32) -> Self {
        SharedCounter {
            value: UnsafeCell::new(value),
        }
    }

    fn increment(&self) {
        unsafe {
            *self.value.get() += 1;
        }
    }

    fn get(&self) -> i32 {
        unsafe { *self.value.get() }
    }
}

// 注意:上面的实现不是线程安全的!
// 需要额外的同步机制

Cell<T>、RefCell<T> 和 OnceCell<T> 都是基于 UnsafeCell<T> 构建的安全抽象。它们在运行时检查借用规则,将编译期检查转为运行期检查。

10.6 编写安全的 Unsafe 抽象

编写 unsafe 代码的核心原则是:在安全边界内封装所有不安全操作。

rust
use std::alloc::{alloc, dealloc, Layout};
use std::ptr;

struct SimpleVec<T> {
    ptr: *mut T,
    len: usize,
    capacity: usize,
}

impl<T> SimpleVec<T> {
    fn new() -> Self {
        SimpleVec {
            ptr: ptr::null_mut(),
            len: 0,
            capacity: 0,
        }
    }

    fn push(&mut self, value: T) {
        if self.len == self.capacity {
            self.grow();
        }
        unsafe {
            ptr::write(self.ptr.add(self.len), value);
        }
        self.len += 1;
    }

    fn grow(&mut self) {
        let new_capacity = if self.capacity == 0 { 4 } else { self.capacity * 2 };
        let new_layout = Layout::array::<T>(new_capacity).unwrap();
        
        let new_ptr = unsafe {
            if self.capacity == 0 {
                alloc(new_layout) as *mut T
            } else {
                let old_layout = Layout::array::<T>(self.capacity).unwrap();
                std::alloc::realloc(self.ptr as *mut u8, old_layout, new_layout.size()) as *mut T
            }
        };
        
        if new_ptr.is_null() {
            panic!("内存分配失败");
        }
        
        self.ptr = new_ptr;
        self.capacity = new_capacity;
    }

    fn get(&self, index: usize) -> Option<&T> {
        if index < self.len {
            Some(unsafe { &*self.ptr.add(index) })
        } else {
            None
        }
    }
}

impl<T> Drop for SimpleVec<T> {
    fn drop(&mut self) {
        if self.capacity > 0 {
            unsafe {
                for i in 0..self.len {
                    ptr::drop_in_place(self.ptr.add(i));
                }
                let layout = Layout::array::<T>(self.capacity).unwrap();
                dealloc(self.ptr as *mut u8, layout);
            }
        }
    }
}

三、深入分析与讨论

3.1 Unsafe 的正确性验证

验证 unsafe 代码的正确性是 Rust 开发中最具挑战性的任务之一。Rust 定义了严格的"未定义行为"(UB)规则,unsafe 代码必须避免所有 UB:

  • 不能解引用空指针或悬垂指针
  • 不能创建无效的引用(如引用已释放的内存)
  • 不能修改不可变数据
  • 不能产生数据竞争
  • 不能使用无效的枚举值

Miri 是 Rust 的中间表示(MIR)解释器,可以在测试中检测许多 unsafe 代码的错误。通过 cargo +nightly miri test 运行测试,Miri 可以发现内存泄漏、悬垂指针、数据竞争等问题。

3.2 Unsafe 的替代方案

在许多场景下,可以使用安全代码替代 unsafe:

  • 使用 AtomicPtr 替代裸指针操作
  • 使用 Box 和 Vec 替代手动内存管理
  • 使用 RefCell 替代 UnsafeCell 的直接使用
  • 使用 bindgen 或 cxx 替代手写 FFI

只有在这些安全抽象无法满足需求时(如实现新的数据结构、与 C 库交互),才应使用 unsafe。

3.3 Unsafe 代码的文档规范

每个 unsafe 函数和 unsafe 块都应该有清晰的安全条件文档。安全条件描述了调用者必须保证的前提条件,以及函数保证的行为。


四、实践案例

4.1 实现简单的链表

使用裸指针实现一个单向链表。

rust
use std::ptr;

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

struct LinkedList<T> {
    head: *mut Node<T>,
    len: usize,
}

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

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

    fn pop_front(&mut self) -> Option<T> {
        if self.head.is_null() {
            return None;
        }
        unsafe {
            let old_head = Box::from_raw(self.head);
            self.head = old_head.next;
            self.len -= 1;
            Some(old_head.value)
        }
    }

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

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

impl<T> Drop for LinkedList<T> {
    fn drop(&mut self) {
        while self.pop_front().is_some() {}
    }
}

fn main() {
    let mut list = LinkedList::new();
    list.push_front(3);
    list.push_front(2);
    list.push_front(1);

    println!("长度: {}", list.len());
    println!("弹出: {:?}", list.pop_front());
    println!("弹出: {:?}", list.pop_front());
    println!("长度: {}", list.len());
}

4.2 调用 C 标准库

通过 FFI 调用 C 标准库的字符串处理函数。

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

extern "C" {
    fn strlen(s: *const c_char) -> usize;
    fn strcat(dest: *mut c_char, src: *const c_char) -> *mut c_char;
}

fn rust_strlen(s: &str) -> usize {
    let c_string = CString::new(s).unwrap();
    unsafe { strlen(c_string.as_ptr()) }
}

fn main() {
    let s = "Hello, Unsafe Rust!";
    println!("Rust len: {}", s.len());
    println!("C strlen: {}", rust_strlen(s));

    let c_str = unsafe {
        let ptr = b"Hello from C\0" as *const u8 as *const c_char;
        CStr::from_ptr(ptr)
    };
    println!("C string: {:?}", c_str);
}

五、常见误区与注意事项

认为 unsafe 就是"不检查":unsafe 不是关闭所有检查,只是解除了编译器的部分安全检查。开发者仍然需要确保代码的正确性,否则会导致未定义行为。

在 unsafe 块中编写过多代码:unsafe 块应该尽可能小,只包含真正需要 unsafe 的操作。将安全检查放在 unsafe 块之外,确保只有满足安全条件时才会进入 unsafe 块。

忽略对齐(Alignment)要求:裸指针操作必须考虑内存对齐。未对齐的访问在某些架构上会导致硬件异常,在 Rust 中是未定义行为。

忘记实现 Drop:手动管理的内存必须在 Drop 实现中正确释放,否则会导致内存泄漏。同时要注意"双重释放"问题。

混淆 `*const T` 与 `&T`:裸指针不保证指向有效内存,也不保证生命周期。从裸指针创建引用时必须确保这些条件满足。


六、本章小结

本章深入讲解了 Unsafe Rust 的设计理念和实践方法。Unsafe Rust 提供了五种超级能力:解引用裸指针、调用 unsafe 函数、访问可变静态变量、实现 unsafe trait 和访问 union 字段。这些能力是实现底层数据结构、FFI 交互和性能关键代码的基础。UnsafeCell<T> 是所有内部可变性抽象的底层基础。编写 unsafe 代码的核心原则是在安全边界内封装所有不安全操作,确保对外暴露的 API 是安全的。Miri 工具可以帮助验证 unsafe 代码的正确性。


七、思考题与练习

编写一个函数,使用裸指针交换两个变量的值。解释为什么这个操作需要 unsafe。

实现一个 MyBox<T> 智能指针,使用裸指针管理堆内存。实现 Deref、Drop 和 new 方法。

通过 FFI 调用 C 标准库的 qsort 函数对 Rust 数组排序。注意回调函数的定义方式。

解释 UnsafeCell<T> 为什么是 Cell、RefCell 和 Mutex 的基础。画出它们的关系图。

实现一个线程安全的 UnsafeCell 包装器,使用 AtomicBool 实现运行时借用检查。

研究 std::mem::transmute 的作用和危险性。给出一个合理使用 transmute 的场景。

使用 Miri 工具检测一个有 Bug 的 unsafe 程序。描述 Miri 发现了什么问题。

阅读 Vec<T> 的源码(alloc/src/vec/mod.rs),找出其中使用 unsafe 的部分,解释为什么这些操作必须是 unsafe 的。

10/10Unsa
0%