使用不安全代码
Rust大体上只给我们了以一种(区域)限制的二进制方式使用不安全Rust的工具。不幸的是,现实远比这来得更复杂。例如,考虑下面这个例子函数:
fn index(idx: usize, arr: &[u8]) -> Option<u8> {
if idx < arr.len() {
unsafe {
Some(*arr.get_unchecked(idx))
}
} else {
None
}
}
很明显这个函数是安全的。我们检查了索引是在界内的,并且如果如此,我们使用了一个未检查的方式索引了数组。不过即便这样一个普通的函数,不安全块的部分也是值得怀疑的。考虑以下将<
换成一个<=
:
fn index(idx: usize, arr: &[u8]) -> Option<u8> {
if idx <= arr.len() {
unsafe {
Some(*arr.get_unchecked(idx))
}
} else {
None
}
}
现在这个函数就不太稳了,并且我仅仅修改了安全代码。这是安全性的根本性问题:它是非局部的(non-local)。我们非安全操作的稳定性必须依赖靠所谓的“安全”操作建立的状态。
在选择进入不安全状态并不需要你考虑任何其他的有害形式的意义上讲,安全性是模块化(分区域)的。例如,对一个切片(slice)进行未检查索引并不意味着突然间你就需要担心这个切片是空的或包含未初始化内存。本质上什么都没有变化。然而,在程序天生就是有状态的和你的不安全操作可能依赖人任何其他状态的意义上讲,安全性“不是”模块化的。
当我们真正进入了有状态的情况时事情就变得更复杂了。考虑一个Vec
的简单实现:
use std::ptr;
// Note this definition is insufficient. See the section on implementing Vec.
pub struct Vec<T> {
ptr: *mut T,
len: usize,
cap: usize,
}
// Note this implementation does not correctly handle zero-sized types.
// We currently live in a nice imaginary world of only positive fixed-size
// types.
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.offset(self.len as isize), elem);
self.len += 1;
}
}
}
这个代码足够简单到能经受合理的审查和验证的考验。现在考虑加入如下方法:
fn make_room(&mut self) {
// grow the capacity
self.cap += 1;
}
这些代码是100%的安全Rust,不过也是完全的不安全的。改变容量违反了Vec
的不可变性(cap
代表了Vec
分配的空间大小)。这并不是Vec
的其余部分可以避免的。它必须相信容量字段因为木有验证它的方法。
unsafe
并不仅仅污染了一整个函数:它污染了一整个模块。通常,唯一的确实的限制不安全代码的作用域的方式是使用私有的模块边界。
并且这完美的起作用了。make_room
的存在对Vec
的稳定性并不是一个问题,因为我们并没有把它标记为公有。只有定义了这个函数的模块可以调用它。另外,make_room
直接访问了Vec
的私有字段,所以它只能被写进与Vec
相同的模块。
因此我们可以编写一个依赖复杂不可变量的完全安全的抽象。这对安全Rust和不安全Rust的关系是极为关键的。我们已经看到了不安全代码必须相信一些安全代码,不过不能相信一般(所有)的安全代码。它(不安全代码)不能相信一个trait的任意实现或者任何传递给它的函数会以一个安全代码并不关心的方法好好表现。
然而如果不安全代码不能避免客户(调用它的)安全代码以任意的方式搞乱它的状态的话,安全性将注定要失败的。万幸的是,因为私有性它可以避免任意代码破坏关键状态。
安全长存!