使用 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 代码也很重要,原因类似:它阻止我们必须信任宇宙中所有安全代码来修改我们的受信任状态。
安全性长存!