使用不安全(Working with Unsafe)

Rust通常只为我们提供了以范围和二进制方式讨论Unsafe Rust的工具.不幸的是,现实要复杂得多.例如,考虑以下玩具函数:

  1. fn index(idx: usize, arr: &[u8]) -> Option<u8> {
  2. if idx < arr.len() {
  3. unsafe {
  4. Some(*arr.get_unchecked(idx))
  5. }
  6. } else {
  7. None
  8. }
  9. }

此函数安全且正确的.我们检查索引是否在边界内,如果是,则以未经检查的方式索引到数组中.我们说这样一个正确的不安全实现的函数是 可靠的(sound) ,这意味着安全代码无法通过它导致未定义行为(记住,这是Safe Rust的唯一基本属性).

但即使在这样一个微不足道的函数中,不安全块的范围也值得怀疑.考虑将<更改为<=:

  1. fn index(idx: usize, arr: &[u8]) -> Option<u8> {
  2. if idx <= arr.len() {
  3. unsafe {
  4. Some(*arr.get_unchecked(idx))
  5. }
  6. } else {
  7. None
  8. }
  9. }

这个程序现在是 不可靠的(unsound) ,Safe Rust会导致未定义的行为,但 我们只修改了安全代码(we only modified safe code) .这是安全的根本问题:它是非局部的(non-local). 我们不安全操作的可靠性必然取决于其他”安全”操作所建立的状态.

安全性是模块化,从某种意义上说,选择不安全并不要求你考虑任意其他类型的不良.例如,对切片执行未经检查的索引并不意味着你突然需要担心切片为空(null)或包含未初始化的内存.没有什么从根本上改变.然而,安全性 不是(isn’t) 模块化的,从某种意义上说,程序本质上是有状态的,并且你的不安全操作可能依赖于任意其他状态,

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

  1. use std::ptr;
  2. // Note: This definition is naive. See the chapter on implementing Vec.
  3. pub struct Vec<T> {
  4. ptr: *mut T,
  5. len: usize,
  6. cap: usize,
  7. }
  8. // Note this implementation does not correctly handle zero-sized types.
  9. // See the chapter on implementing Vec.
  10. impl<T> Vec<T> {
  11. pub fn push(&mut self, elem: T) {
  12. if self.len == self.cap {
  13. // not important for this example
  14. self.reallocate();
  15. }
  16. unsafe {
  17. ptr::write(self.ptr.add(self.len), elem);
  18. self.len += 1;
  19. }
  20. }
  21. # fn reallocate(&mut self) { }
  22. }
  23. # fn main() {}

此代码非常简单,可以合理地审核和非正式验证.现在考虑添加以下方法:

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

此代码是100%的Safe Rust,但它也完全不健全.更改容量违反了Vec的不变量(该cap反映了Vec中分配的空间).这不是Vec其他东西可以防范的事情.它 必须(has) 信任容量字段,因为无法验证它.

因为它依赖于struct字段的不变量,所以这个unsafe的代码不仅会污染整个函数:它会污染整个 模块(module) .通常,限制不安全代码范围的唯一防弹(bullet-proof)方式是在隐私的模块边界.

然而,这非常 有效(perfectly) .make_room的存在对于Vec的健全性来说 不是(not) 问题,因为我们没有将其标记为公有.只有定义此函数的模块才能调用它.此外,make_room直接访问Vec的私有字段,因此它只能与Vec在同一模块中编写.

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

我们已经看到Unsafe代码必须信任 某些(some) Safe代码,但不应该信任 泛型(generic) Safe代码.出于类似的原因,隐私对于不安全的代码很重要:它使我们不必相信宇宙中的所有安全代码都会破坏我们的可信状态.

安全活着!