使用不安全代码
Rust 通常只为我们提供以作用域和二元方式讨论不安全 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 函数是健全的,这意味着安全代码不会通过它导致未定义行为(记住,这是安全 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 } } }
这个程序现在是不健全的,安全 Rust 可以导致未定义行为,然而我们只修改了安全代码。这是安全性的根本问题:它是非局部的。我们的 unsafe 操作的健全性必然取决于由其他“安全”操作建立的状态。
安全性在某种意义上是模块化的,即选择不安全并不需要你考虑其他任意类型的坏处。例如,对切片进行未检查的索引并不意味着你突然需要担心切片为空或包含未初始化的内存。没有任何根本性的变化。然而,安全性不是模块化的,因为程序本质上是有状态的,并且你的 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% 是安全 Rust,但它也是完全不健全的。更改容量违反了 Vec 的不变性(cap
反映了 Vec 中分配的空间)。这不是 Vec 的其余部分可以防范的东西。它必须信任容量字段,因为没有办法验证它。
因为它依赖于结构字段的不变性,所以这段 unsafe
代码不仅仅污染了一个函数:它污染了整个模块。通常,限制不安全代码作用域的唯一万无一失的方法是在模块边界使用隐私。
然而,这工作完美。 make_room
的存在对于 Vec 的健全性不是问题,因为我们没有将其标记为 public。只有定义此函数的模块才能调用它。此外,make_room
直接访问 Vec 的私有字段,因此它只能在与 Vec 相同的模块中编写。
因此,我们有可能编写一个完全安全的抽象,该抽象依赖于复杂的不变性。这对于安全 Rust 和不安全 Rust 之间的关系至关重要。
我们已经看到不安全代码必须信任某些安全代码,但不应该信任通用的安全代码。隐私对于不安全代码也很重要,原因类似:它可以防止我们不得不信任宇宙中所有的安全代码来破坏我们信任的状态。
安全万岁!