Rust 学习笔记(39)-unsafe

参考章节《Rust程序设计语言》第19.1章 不安全 Rust
参考章节《Rust语言圣经(Rust Course)》第3.9.1章 五种兵器

目前为止讨论过的代码都有 Rust 在编译时会强制执行的内存安全检查。然而,Rust 还隐藏有第二种语言,它不会强制执行这类内存安全检查

说白了,我们可以让Rust不执行内存安全检查,这被称为 不安全 Rust(unsafe Rust)
此时,你就必须对代码的正确性负责,如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。

我们来看看它都有哪些魔法吧

  • 解引用裸指针
  • 调用不安全的函数或方法
  • 访问或修改可变静态变量
  • 实现不安全 trait
  • 访问 union 的字段
1
2
有一点很重要,unsafe 并不会关闭借用检查器或禁用任何其他 Rust 安全检查,例如,如果在不安全代码中使用引用,它仍会被检查。  
unsafe 关键字只是提供了上面五个不会被编译器检查内存安全的功能。换句话说,你仍然能在不安全代码块中获得某种程度的安全。

可以通过 unsafe 关键字来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块。

  1. 解引用裸指针

不安全 Rust 有两个被称为 裸指针(raw pointers) 的类似于引用的新类型。
和引用一样,裸指针是不可变或可变的,分别写作 *const T*mut T。这里的 * 不是解引用运算符;它是类型名称的一部分。
在裸指针的上下文中,不可变意味着指针解引用之后不能直接赋值。

下面的代码基于引用创建了一个不可变裸指针和可变裸指针

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    let mut num = 5;

    let r1 = &num as *const i32; // 裸指针 r1, 它是不可变的
    let r2 = &mut num as *mut i32; // 裸指针 r2,它是可变的

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

细心的同学可能会发现,在创建裸指针时,并没有 unsafe 的身影,原因在于:创建裸指针是安全的行为,而解引用裸指针是不安全的行为

至此,我们已经学过三类指针,分别是引用,智能指针,和裸指针,而裸指针和它们的区别在于

  • 裸指针允许忽略借用规则,可以同时拥有一个数据的可变、不可变指针,甚至还能拥有多个可变的指针
  • 不保证指向有效的内存
  • 允许为空(null)
  • 没有实现任何自动的回收 (drop)

你还可以基于任意内存地址创建裸指针

不过这么做是非常危险的,因为该内存地址有可能存在值,也有可能没有,就算有值,也大概率不是你需要的值,总之,不要这么做,虽然它是可行的。

1
2
let address = 0x012345usize;
let r = address as *const i32;
  1. 调用不安全的函数或方法

unsafe 函数从外表上来看跟普通函数并无区别,唯一的区别就是它需要使用 unsafe fn 来进行定义。
这种定义方式是为了告诉调用者,当调用此函数时,你需要注意它的相关需求,因为 Rust 无法担保调用者在使用该函数时能满足它所需的一切需求。

1
2
3
4
5
unsafe fn dangerous() {} // 定义一个不安全的函数

fn main() {
    dangerous(); // 调用这个函数
}

尝试运行上面这段代码

1
error[E0133]: call to unsafe function is unsafe and requires unsafe function or block

要调用不安全的函数,必须使用 unsafe 块包起来
这么做是为了让你清楚的认识到,正在调用的是一个不安全的函数,需要小心看看文档,看看函数有哪些特别的要求需要被满足。

1
2
3
4
5
6
7
unsafe fn dangerous() {} // 定义一个不安全的函数

fn main() {
    unsafe {
        dangerous(); // 调用这个函数
    }
}
  1. 创建不安全代码的安全抽象

我们来看这样一个函数,它传入一个数组,和其中间元素的索引,然后将它分成两个切片,并且每个切片都是可变的

1
2
3
4
5
6
7
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();

    assert!(mid <= len);

    (&mut slice[..mid], &mut slice[mid..])
}

上面的代码看起来很合理,但由于借用规则,不允许借用同一个Slice两次,因此运行会报错

1
2
error[E0499]: cannot borrow `*slice` as mutable more than once at a time
--省略部分报错--

Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同部分,它只知道我们借用了同一个 slice 两次。
而本质上借用 slice 的不同部分是可以的,因为结果两个 slice 不会重叠,不过 Rust 还没有智能到能够理解这些。
当我们知道某些事是可以的而 Rust 不知道的时候,就是触及不安全代码的时候了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use std::slice;

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr(); // as_mut_ptr 方法返回 slice 的裸指针

    assert!(mid <= len); // 保证索引 mid 位于 slice 中的断言。

    unsafe {
        (
            // slice::from_raw_parts_mut 函数获取一个裸指针和一个长度来创建一个新的 slice。
            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 r = &mut v[..];

    let (a, b) = split_at_mut(r, 3);

    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5, 6]);
}

虽然 split_at_mut 使用了 unsafe,但我们无需将整个函数声明为 unsafe fn,因为我们在函数内部保证了指针的安全。
至此,我们就创建了一个不安全代码的安全抽象,其代码以一种安全的方式使用了 unsafe 代码

  1. 使用 extern 函数调用外部代码

有时你的 Rust 代码可能需要与其他语言编写的代码交互。
为此 Rust 有一个关键字,extern,有助于创建和使用外部函数接口(Foreign Function Interface, FFI)
但并不是所有语言都这么叫,例如Java叫做 JNI(Java Native Interface)

外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。

下面代码演示了如何调用 C 标准库中的 abs 函数

1
2
3
4
5
6
7
8
9
extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

extern "C" 块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名称。
"C" 部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI)
ABI 定义了如何在汇编语言层面调用此函数。"C" ABI 是最常见的,表示遵循 C 编程语言的 ABI。

当然,除了调用其他语言提供的FFI函数以外,你还可以创建一个FFI函数以供其他语言调用

1
2
3
4
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

上面的代码可以让 call_from_c 函数被 C 语言的代码调用,当然,前提是将其编译成一个共享库,然后链接到 C 语言中。

这里还有一个比较奇怪的注解 #[no_mangle],它用于告诉 Rust 编译器:不要乱改函数的名称。
默认情况下,Rust 在编译时会去修改函数的名称,为了让名称包含更多的信息,这样其它的编译部分就能从该名称获取相应的信息
这种修改会导致函数名变得相当不可读。因此,为了让 Rust 函数能顺利被其它语言调用,我们必须要禁止掉该功能。

  1. 访问或修改可变静态变量

Rust 认为访问或修改可变静态变量是不安全的。因为拥有可以全局访问的可变数据,难以保证不存在数据竞争

你可能会说,修改是不安全的,怎么访问也算呢?原因很简单,可变静态变量意味着只要你能访问到这个数据,你就有权限改,Rust 可不管你到底会不会改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

就像常规变量一样,我们使用 mut 关键来指定可变性。任何读写 COUNTER 的代码都必须位于 unsafe 块中。
这段代码可以编译并如期打印出 COUNTER: 3,因为这是单线程的。拥有多个线程访问 COUNTER 则可能导致数据竞争。

  1. 实现不安全 trait

trait 中至少有一个方法中包含编译器无法验证的内容时 trait 是不安全的。
关于 不安全的 trait 可以参考 《Rust语言圣经(Rust Course)为裸指针实现 Sync 这一节

要实现不安全的 trait 也很简单,使用 unsafe impl 关键字即可,我们告诉编译器,相应的正确性由我们自己来保证。

1
2
3
4
5
6
7
8
9
unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

fn main() {}
  1. 访问联合体中的字段

仅适用于 unsafe 的最后一个操作是访问 联合体 中的字段,union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。
联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。
参考 https://doc.rust-lang.org/reference/items/unions.html#unions

总结一下

  1. unsafe 只应该用于这五种场景,其它场景,你应该坚决的使用安全的代码。
  2. unsafe 虽然为我们提供了一些额外的能力,但需要我们自己对内存安全负责,因此我们必须要异常小心。
updatedupdated2025-03-012025-03-01