1.3. 编写非安全代码
原文链接:doc.rust-lang.org/nomicon/working-...
编写非安全代码#
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 的关系时,这一点非常重要。
我们已经了解了非安全代码必须信任一部分安全代码,但是不应该信任所有的安全代码。出于相似的原因,私有成员的限制对于非安全代码很重要:我们不需要无条件信任世界上所有的安全代码并且任由他们搞乱我们的可信任状态。
安全机制万岁!