使用 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 } } }
此函数是安全且正确的。我们检查索引是否在范围内,如果是,则以未经检查的方式索引到数组中。我们说这样一个正确的不安全实现的函数是*可靠的*,这意味着安全代码不能通过它导致未定义行为(请记住,这是安全 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 可能会导致未定义行为,但*我们只修改了安全代码*。这是安全性的根本问题:它不是局部的。我们不安全操作的可靠性必然取决于其他“安全”操作建立的状态。
安全性是模块化的,因为选择不安全性并不要求您考虑任意其他类型的错误。例如,对切片执行未经检查的索引并不意味着您突然需要担心切片为空或包含未初始化的内存。从根本上说,什么都没有改变。然而,安全性*不是*模块化的,因为程序本质上是有状态的,并且您的不安全操作可能取决于任意其他状态。
当我们合并实际的持久状态时,这种非局部性会变得更糟。考虑 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
代码不仅仅污染了整个函数:它污染了整个*模块*。通常,限制 unsafe 代码范围的唯一防弹方法是在模块边界使用私有性。
然而,这*完美地*解决了问题。make_room
的存在*不会*对 Vec 的可靠性造成问题,因为我们没有将其标记为公开的。只有定义此函数的模块才能调用它。此外,make_room
直接访问 Vec 的私有字段,因此它只能在与 Vec 相同的模块中编写。
因此,我们可以编写一个完全安全的抽象,它依赖于复杂的不变性。这对于安全 Rust 和 Unsafe Rust 之间的关系*至关重要*。
我们已经看到 Unsafe 代码必须信任*某些*安全代码,但不应该信任*通用的*安全代码。对于 unsafe 代码来说,私有性很重要,原因与此类似:它可以防止我们不得不信任宇宙中所有安全代码来干扰我们信任的状态。
安全万岁!