Appearance
15.3 UnsafeCell
接下来,我们来分析 Cell/RefCell 的实现原理。我们先来考虑两个问题,标准库中的 Cell 类型是怎样实现的?假如让我们自己来实现一遍,是否可行呢?
模仿标准库中的 Cell 类型的公开方法(只考虑最简单的 new、get、set 这三个方法),我们先来一个最简单的版本 V1:
rust
struct CellV1<T> {
value: T
}
impl<T> CellV1<T> {
fn new(v: T) -> Self where T: Copy {
CellV1 { value: v}
}
fn set(&self, v: T) {
self.value = v;
}
fn get(&self) -> T where T: Copy {
self.value
}
}
这个版本是一个 new type 类型,内部包含了一个 T 类型的成员。成员方法对类型 T 都有恰当的约束。这些都没错。只有一个关键问题需要注意:对于 set 方法,直接这样写是肯定行不通的,因为 self 是只读引用,我们不可能直接对 self.value 赋值。而且,Cell 类型最有用的地方就在于,它可以通过不可变引用改变内部的值。那么这个问题怎么解决呢?可以使用 unsafe 关键字。后面还有一章专门讲解 unsafe,此处只需知道,用 unsafe 包起来的代码块可以突破编译器的一些限制,做一些平常不能做的事情。
以下是修正版:
rust
struct CellV2<T> {
value: T
}
impl<T> CellV2<T> {
fn new(v: T) -> Self where T: Copy {
CellV2 { value: v}
}
fn set(&self, v: T) where T: Copy {
unsafe {
let p = &(self.value) as *const T as *mut T;//此处实际上引入了未定义行为
*p = v;
}
}
fn get(&self) -> T where T: Copy {
self.value
}
}
在使用 unsafe 语句块之后,这段代码可以编译通过了。这里的关键是,在 unsafe 代码中,我们可以把*const T 类型强制转换为*mut T 类型。这是初学者最直观的解决方案,但这个方案是错误的。通过这种方式,我们获得了写权限。通过下面简单的示例可以看到,这段代码是符合我们的预期的:
rust
fn main() {
let c = CellV2::new(1_isize);
let p = &c;
p.set(2);
println!("{}", c.get());
}
从以上代码可以看出,这正是内部可变性类型的特点,即通过共享指针,修改了内部的值。
事情就这么简单么?很可惜,有这种想法的人都过于 naive 了。下面这个示例会给大家泼一盆冷水:
rust
struct Table<'arg> {
cell: CellV2<&'arg isize>
}
fn evil<'long,'short>(t: &Table<'long>, s: &'short isize)
where 'long : 'short
{
// The following assignment is not legal, but it escapes from lifetime checking
let u: &Table<'short> = t;
u.cell.set(s);
}
fn innocent<'long>(t: &Table<'long>) {
let foo: isize = 1;
evil(t, &foo);
}
fn main() {
let local = 100;
let table = Table { cell: CellV2::new(&local) };
innocent(&table);
// reads `foo`, which has been destroyed
let p = table.cell.get();
println!("{}", p);
}
如果我们用 rustc temp.rs 编译 debug 版本,可以看到执行结果为 1。
如果我们用 rustc-O temp.rs 编译 release 版本,可以看到执行结果为 140733369053192。
这是怎么回事呢?因为这段代码中出现了野指针。我们来分析一下这段测试代码。在这段测试代码中,我们在 CellV2 类型里面保存了一个引用。main 函数调用了 innocent 函数,继而又调用了 evil 函数。这里需要特别注意的是:在 evil 函数中,我们调用了 CellV2 类型的 set 方法,改变了它里面存储的指针。修改后的指针指向的谁呢?是 innocent 函数内部的一个局部变量。最后在 main 函数中,innocent 函数返回后,再把这个 CellV2 里面的指针拿出来使用,就得到了一个野指针。
我们继续从生命周期的角度深入分析,这个野指针的成因。在 main 函数的开始,table.cell 变量保存了一个指向 local 变量的指针。这是没问题的,因为 local 的生命周期比 table 更长,table.cell 指向它肯定不会有问题。有问题的是 table.cell 在 evil 函数中被重新赋值。这个赋值导致了 table.cell 保存了一个指向局部调用栈上的变量。也就是这里出的问题:
rust
// t: &Table<'long>
let u: &Table<'short> = t;
// s: &'short isize
u.cell.set(s);
我们知道,在'long:'short 的情况下,&'long 类型的指针向&'short 类型赋值是没问题的。但是这里的&Table<'long>类型的变量赋值给&Table<'short>类型的变量合理吗?事实证明,不合理。证明如下。我们把上例中的 CellV2 类型改用标准库中的 Cell 类型试试:
rust
type CellV2<T> = std::cell::Cell<T>;
其他测试代码不变。编译,提示错误为:
rust
error[E0308]: mismatched types
--> temp.rs:11:29
|
11 | let u: &Table<'short> = t;
| ^ lifetime mismatch
|
= note: expected type `&Table<'short>`
= note: found type `&Table<'long>`
果然是这里的问题。使用我们自己写的 CellV2 版本,这段测试代码可以编译通过,并制造出了内存不安全。使用标准库中的 Cell 类型,编译器成功发现了这里的生命周期问题,给出了提示。
这说明了 CellV2 的实现依然是错误的。虽然最基本的测试用例通过了,但是碰到复杂的测试用例,它还是不够“健壮”。而 Rust 对于“内存不安全”问题是绝对禁止的。不像 C/C++,在 Rust 语言中,如果有机会让用户在不用 unsafe 的情况下制造出内存不安全,这个责任不是由用户来承担,而是应该归因于写编译器或者写库的人。在 Rust 中,写库的人不需要去用一堆文档来向用户保证内存安全,而是必须要通过编译错误来保证。这个示例中的内存安全问题,不能归因于测试代码写得不对,因为在测试代码中没有用到任何 unsafe 代码,用户是正常使用而已。这个问题出现的根源还是 CellV2 的实现有问题,具体来说就是那段 unsafe 代码有问题。按照 Rust 的代码质量标准,CellV2 版本是完全无法接受的垃圾代码。
那么,这个 bug 该如何修正呢?为什么&'long 类型的指针可以向&'short 类型赋值,而&Cell<'long>类型的变量不能向&Cell<'short>类型的变量赋值?因为对于具有内部可变性特点的 Cell 类型而言,它里面本来是要保存&'long 型指针的,结果我们给了它一个&'short 型指针,那么在后面取出指针使用的时候,这个指针所指向的内容已经销毁,就出现了野指针。这个 bug 的解决方案是,禁止具有内部可变性的类型,针对生命周期参数具有“协变/逆变”特性。这个功能是通过标准库中的 UnsafeCell 类型实现的:
rust
#[lang = "unsafe_cell"]
#[stable(feature = "rust1", since = "1.0.0")]
pub struct UnsafeCell<T: ?Sized> {
value: T,
}
请注意这个类型上面的标记#[lang=...]
。这个标记意味着这个类型是个特殊类型,是被编译器特别照顾的类型。这个类型的说明文档需要特别提示读一下:
rust
The core primitive for interior mutability in Rust.
UnsafeCell<T> is a type that wraps some T and indicates unsafe interior operations on the wrapped type. Types with an UnsafeCell<T> field are considered to have an 'unsafe interior'. The UnsafeCell<T> type is the only legal way to obtain aliasable data that is considered mutable. In general, transmuting an &T type into an &mut T is considered undefined behavior.
Types like Cell<T> and RefCell<T> use this type to wrap their internal data.
所有具有内部可变性特点的类型都必须基于 UnsafeCell 来实现,否则必然出现各种问题。这个类型是唯一合法的将&T 类型转为&mut T 类型的办法。绝对不允许把&T 直接转换为&mutT 而获得可变性。这是未定义行为。
大家可以自行读一下 Cell 和 RefCell 的源码,可以发现,它们能够正常工作的关键在于它们都是基于 UnsafeCell 实现的,而 UnsafeCell 本身是编译器特殊照顾的类型。所以我们说“内部可变性”这个概念是 Rust 语言提供的一个核心概念,而不是通过库模拟出来的。
实际上,上面那个 CellV2 示例也正说明了写 unsafe 代码的困难之处。许多时候,我们的确需要使用 unsafe 代码来完成功能,比如调用 C 代码写出来的库等。但是却有可能一不小心违反了 Rust 编译器的规则。比如,你没读过上面这段文档的话,不大可能知道简单地通过裸指针强制类型转换实现&T
到&mut T
的类型转换是错误的。这么做会在编译器的生命周期静态检查过程中制造出一个漏洞,而且这个漏洞用简单的测试代码测不出来,只有在某些复杂场景下才会导致内存不安全。Rust 代码中写 unsafe 代码最困难的地方其实就在这样的细节中,有些人在没有完全理解掌握 Rust 的 safe 代码和 unsafe 代码之间的界限的情况下,乱写 unsafe 代码,这是不负责任的。本书后面还会有一章专门讲解 unsafe 关键字。