Skip to content

Unsafe Rust

unsafe 稍稍放开了编译器赋予代码的枷锁,但不安全代码不等于危险代码,而是将安全责任转移给开发者,开发者需要自己负责unsafe代码的安全性

仅在以下情况考虑使用:

  • 与硬件/操作系统交互
  • 性能关键路径
  • 集成 C 代码库
  • 实现底层数据结构

基础使用方法:用 unsafe 块包裹

unsafe {
    // 不安全操作
}

Unsafe 解锁五大超能力

  • 解引用裸指针
  • 调用不安全的函数或方法
  • 访问或修改可变静态变量
  • 实现 unsafe trait
  • 访问 union 的字段

unsafe 只是关闭了上述五个编译器安全检查的功能,它并不会关闭借用检查器或禁用任何其他 Rust 安全检查,所以你仍然能在不安全块中获得某种程度的安全。

而且若程序出错,则可以快速定位在使用了 unsafe 的代码块。也正是如此,最好确保每一个 unsafe 代码块都足够小,以便程序在出错时可快速修复。

最好的unsafe使用方式是:把不安全代码打包进安全的函数之中,以便安全代码的调用(否则调用方也必须是unsafe代码)。

解引用裸指针

还记得那所谓的 "悬垂引用" 吗,编译器不允许变量有多个可变引用 或者 同时有可变和不可变引用多个可变引用

不安全 Rust 有两个被称为裸指针的新类型(作用类似于引用)。

  • 裸指针类型:*const T(不可变)和 *mut T(可变),* 不是解引用符合,而是裸指针类型名的一部分。

  • 与普通引用和智能指针的区别:

    • 可同时存在多个可变/不可变指针(忽略借用规则)

    • 可能指向无效内存

    • 允许为空

    • 没有自动清理功能(需要手动drop)

rust
let mut num = 5;

// 安全代码中创建裸指针
let r1 = &raw const num; // 不可变裸指针
let r2 = &raw mut num;   // 可变裸指针

// 必须在 unsafe 块中解引用
unsafe {
    println!("r1: {}", *r1); // 输出 5
    println!("r2: {}", *r2); // 输出 5
}

调用不安全的函数或方法

rust
// unsafe函数使用注释
// 由函数作者撰写的一些注意事项
unsafe fn dangerous() { /* 底层操作 */ }	// 函数被声明unsafe,只能在unsafe块中被使用

unsafe {
    dangerous(); // 需确保满足函数契约(调用条件,一般要看注释)
}

必须在一个单独的 unsafe 块中调用 dangerous 函数。

通过 unsafe 块,我们向 Rust 承诺我们已经阅读过函数的文档,理解如何正确使用它,并核实我们履行了该函数的契约(要求)。

安全抽象实践

——将不安全代码封装为安全接口。 示例:实现安全的 split_at_mut

rust
use std::slice;

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr(); // 获取裸指针
    
    assert!(mid <= len);
    
    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),          // 创建前半段切片
            slice::from_raw_parts_mut(ptr.add(mid), len - mid) // 创建后半段切片
        )
    }
}
// 安全调用:let (a, b) = v.split_at_mut(3);

FFI 调用(调用外部库)

使用关键字extern 使用 外部函数接口Foreign Function Interface,FFI)。外部函数接口是一个编程语言用以定义函数的方式,其允许不同编程语调用这些函数(同语言编译出来的库也是如此)。

因为其他语言不会强制执行 Rust 的规则,所以 extern 块中声明的函数在 Rust 代码中通常是不安全的,extern 块本身也必须标注 unsafe

——与其他语言交互,示例为 Rust 调用 C 标准库中的 abs 函数

rust
unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
  • 代码中的 "C" 定义了外部函数所使用的 应用二进制接口application binary interface,ABI)是 "C" ABI。

  • ABI 定义了如何在汇编语言层面调用此函数。"C" ABI 是最常见的,并遵循 C 编程语言的 ABI。

  • Rust 支持的所有 ABI 的信息参见 the Rust Reference

unsafe extern 默认是 unsafe 的,但可以使用 safe 关键字让一些 FFI 函数可以安全地调用。

例如,C 标准库中的 abs 函数可以使用任何 i32 参数调用,完全不必考虑内存安全:

rust
unsafe extern "C" {
    safe fn abs(input: i32) -> i32;
}

fn main() {
    // 在 safe 代码块中直接调用
    println!("Absolute value of -3 according to C: {}", abs(-3));
}

注意: 不是所有 unsafe 块中都能使用 safe 关键字,它只允许在unsafe extern 中使用。

暴露函数(导出函数)

如果说 FFI 是调用其他语言的函数,那暴露函数就是给其他程序(包括跨语言)调用。

不同于创建整个 extern 块来使用 FFI,导出函数需要:

  • fn 关键字之前增加 extern 关键字并指定 ABI
  • 增加 #[no_mangle] 注解来告诉 Rust 编译器不要 mangle 此函数的名称。

Mangling 指编译时会将函数更改为其他名称。

禁用 Rust 编译器的 name mangling 是不安全的,需要开发者自行确保所有导出的函数之间没有命名冲突

rust
#[unsafe(no_mangle)]	//在这里对no_mangle属性声明unsafe,并非将下面的代码变成unsafe
						//表示“我知道这个属性危险,但我知道我在干什么。”
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

访问或修改可变静态变量

静态变量即为全局变量,一般是不可变的。

rust
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("name is: {HELLO_WORLD}");
}

可变的静态变量是unsafe的:

rust
static mut COUNTER: u32 = 0;

/// SAFETY: 同时在多个线程调用这个方法是未定义的行为,所以你*必须*保证同一时间只
/// 有一个线程在调用它。
unsafe fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    unsafe {
        // SAFETY: 它只在 `main` 这一个线程被调用。
        add_to_count(3);
        println!("COUNTER: {}", *(&raw const COUNTER));
    }
}
  • 使用 mut 关键字来指定可变性。任何读写 COUNTER 的代码都必须位于 unsafe 块中。

实现 unsafe trait

当 trait 中的方法存在不变式(invariant)时该 trait 就是不安全的。

rust
unsafe trait Foo {	// 这里要unsafe关键字
    // 方法声明在这里
}

unsafe impl Foo for i32 {	// 这里也要unsafe关键字
    // 方法实现在这里
}

访问 union 的字段

unionstruct 类似,但是在一个实例中同时只能使用一个已声明的字段。

这类型的作用是与 C 代码中的联合体进行交互。

rust
#[repr(C)]
union MyUnion {
    i: i32,
    f: f32,
}

fn main() {
    let u = MyUnion { i: 42 };
    
    unsafe {
        // 访问联合体字段需要 unsafe
        println!("Integer: {}", u.i);
        // 危险:可能错误解释内存
        // println!("Float: {}", u.f); // 未定义行为!
    }
}

最佳实践

文档注释

每当编写一个不安全函数,惯常做法是编写一个以 SAFETY 开头的注释并解释调用者如何使用这个函数。

同理,当进行不安全操作时,编写一个以 SAFETY 解释如何根据安全性规则进行维护。

rust
static mut COUNTER: u32 = 0;

/// SAFETY: 同时在多个线程调用这个方法是未定义的行为,所以你*必须*保证同一时间只
/// 有一个线程在调用它。
unsafe fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    unsafe {
        // SAFETY: 它只在 `main` 这一个线程被调用。
        add_to_count(3);
        println!("COUNTER: {}", *(&raw const COUNTER));
    }
}
rust
unsafe {
    // SAFETY: 指针有效性由以下条件保证:
    // 1. 指针来自有效引用
    // 2. 索引在边界内
    *ptr.add(index) = value;
}

缩小 unsafe 范围

rust
fn safe_wrapper() {
    // 良好实践:将 unsafe 限制在最小范围
    let result = unsafe {
      // 仅包含必要的不安全操作
    };
    // 安全处理结果
}

用 miri 检查不安全代码

sh
rustup +nightly component add miri
cargo +nightly miri test