使用 Unsafe

Rust 通常只提供了在范围和二元方式下讨论 Unsafe Rust 的工具。不幸的是,现实要复杂得多。例如,考虑下面的示例函数

#![allow(unused)]
fn main() {
fn index(idx: usize, arr: &[u8]) -> Option<u8> {
    if idx < arr.len() {
        unsafe {
            Some(*arr.get_unchecked(idx))
        }
    } else {
        None
    }
}
}

这个函数是安全且正确的。我们检查索引是否在界限内,如果是在界限内,则以不检查的方式访问数组。我们称这种正确地使用 Unsafe 实现的函数是健全的 (sound),这意味着安全代码无法通过它导致未定义行为(请记住,这是 Safe Rust 的唯一基本属性)。

但即使在这样一个微不足道的函数中,unsafe 块的作用范围也值得怀疑。考虑将 < 改为 <=

#![allow(unused)]
fn main() {
fn index(idx: usize, arr: &[u8]) -> Option<u8> {
    if idx <= arr.len() {
        unsafe {
            Some(*arr.get_unchecked(idx))
        }
    } else {
        None
    }
}
}

这个程序现在是不健全的 (unsound),Safe Rust 会导致未定义行为,然而我们只修改了安全代码。这是安全性的根本问题:它是非局部的。我们的 unsafe 操作的健全性必然依赖于由其他“安全”操作建立的状态。

安全性在某种意义上是模块化的,即选择使用 unsafe 并不要求你考虑任意其他类型的错误。例如,对切片进行 unchecked 索引并不意味着你需要突然担心切片为空或包含未初始化内存。没有什么根本性的变化。然而,安全性在另一种意义上不是模块化的,因为程序本身是有状态的,你的 unsafe 操作可能依赖于任意其他状态。

当我们引入实际的持久状态时,这种非局部性会变得更糟。考虑一个简单的 Vec 实现

use std::ptr;

// Note: This definition is naive. See the chapter on implementing Vec.
pub struct Vec<T> {
    ptr: *mut T,
    len: usize,
    cap: usize,
}

// Note this implementation does not correctly handle zero-sized types.
// See the chapter on implementing Vec.
impl<T> Vec<T> {
    pub fn push(&mut self, elem: T) {
        if self.len == self.cap {
            // not important for this example
            self.reallocate();
        }
        unsafe {
            ptr::write(self.ptr.add(self.len), elem);
            self.len += 1;
        }
    }
    fn reallocate(&mut self) { }
}

fn main() {}

这段代码足够简单,可以合理地审计和非正式地验证。现在考虑添加以下方法

fn make_room(&mut self) {
    // grow the capacity
    self.cap += 1;
}

这段代码是 100% 的 Safe Rust,但它也完全不健全 (unsound)。修改容量违反了 Vec 的不变性(即 cap 反映了 Vec 中已分配的空间)。Vec 的其余部分无法防范这一点。它必须信任容量字段,因为它无法验证。

由于它依赖于结构体字段的不变性,这段 unsafe 代码不仅“污染”了整个函数,还“污染”了整个模块。一般来说,限制 unsafe 代码范围的唯一万无一失的方法是在模块边界通过私有性。

然而,这完美地解决了问题。make_room 的存在对 Vec 的健全性不是问题,因为我们没有将其标记为公有 (public)。只有定义此函数的模块可以调用它。此外,make_room 直接访问 Vec 的私有字段,因此它只能在与 Vec 相同的模块中编写。

因此,我们可以编写一个依赖于复杂不变性的完全安全的抽象。这对于 Safe Rust 和 Unsafe Rust 之间的关系是至关重要的

我们已经看到,Unsafe 代码必须信任一些 Safe 代码,但不应该信任泛型 (generic) Safe 代码。私有性 (Privacy) 对 unsafe 代码也很重要,原因类似:它阻止我们必须信任宇宙中所有安全代码来修改我们的受信任状态。

安全性长存!