Rust 编写非安全代码

Rust通常要求我们明确限制非安全Rust代码的作用域。可是,现实情况其实要更复杂一些。举个例子,看一下下面的代码:

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

这个函数是安全和正确的。我们检查了索引值有没有越界。如果没有,就从数组中用不安全的方式取出对应的值。然而,哪怕是这么简单的一个函数,unsafe代码块的范围也不是绝对明确的。想象一下,如果把 <改成 <=

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

这段程序就有潜在的问题了,但我们其实只修改了安全代码的部分。这是安全机制的一个根本性问题:非本地性。意思是,非安全代码的稳定性其实依赖于另一些“安全”代码的状态。

是否进入非安全代码块,并不受其他部分代码正确性的影响,从这个角度看安全机制是模块化的。比如,是否对一个slice进行不安全索引,不受slice是不是null或者是不是包含未初始化的内存这些事情的影响。但是,由于程序本身是有状态的,非安全操作的结果实际依赖于其他部分的状态,从这个角度看安全机制又是非模块化的。

在处理持久化状态时,非本地性带来的问题就更加明显了。看一下Vec的一个简单实现:

use std::ptr;

// 注意:这个定义十分简单。参考实现Vec的章节
pub struct Vec<T> {
    ptr: *mut T,
    len: usize,
    cap: usize,
}

// 注意:这个实现未考虑大小为0的类型。参考实现Vec的章节
impl<T> Vec<T> {
    pub fn push(&mut self, elem: T) {
        if self.len == self.cap {
            // 与例子本身无关
            self.reallocate();
        }
        unsafe {
            ptr::write(self.ptr.offset(self.len as isize), elem);
            self.len += 1;
        }
    }
}

这段代码很简单,便于审查和修改。现在考虑给它添加一个新的方法:

fn make_room(&mut self) {
    // 增加容量
    self.cap += 1;
}

这段代码是100%的安全Rust但是彻底的不稳定。改变容量违反了Vec的不变性(cap表示分配给Vec的空间大小)。Vec的其他部分并不会保护它,我们只能信任它的值是正确的,因为本来没有修改它的方法。

因为代码逻辑依赖于struct的某个成员的不变性,那段unsafe的代码不仅仅污染了它所在的函数,它还污染了整个module。一般来说,只有在一个私有的module里非安全代码才可能是真正安全的。

上面的改动其实是可以正常工作的。make_room方法并不会导致Vec的问题,因为我们没有设置它为public。只有定义这个方法的module可以调用它。同时,make_room直接访问了Vec的私有成员,所以它也只能在Vec所在的module内使用。

这允许我们基于一些复杂的不变性写一些绝对安全的抽象。在考虑安全Rust和非安全Rust的关系时,这一点非常重要。

我们已经了解了非安全代码必须信任一部分安全代码,但是不应该信任所有的安全代码。出于相似的原因,私有成员的限制对于非安全代码很重要:我们不需要无条件信任世界上所有的安全代码并且任由他们搞乱我们的可信任状态。

下一章:Rust repr(Rust)

底层编程经常需要关注数据布局。每种类型都有一个数据对齐属性(alignment)。一种类型的对齐属性决定了哪些内存地址可以合法地存储该类型的值。如果对齐属性是n,那么它的值的存储地址必须是n的倍数。所以,对齐 ...