未检查的未初始化内存
这个规则一个有趣的例外是处理数组。安全的 Rust 不允许你部分初始化数组。当你初始化一个数组时,你可以使用 let x = [val; N]
将每个值设置为相同的值,或者你可以使用 let x = [val1, val2, val3]
单独指定每个成员。不幸的是,这相当死板,特别是当你需要以更增量或动态的方式初始化数组时。
不安全的 Rust 为我们提供了一个强大的工具来处理这个问题:MaybeUninit
。这个类型可以用来处理尚未完全初始化的内存。
使用 MaybeUninit
,我们可以逐个元素地初始化数组,如下所示
#![allow(unused)] fn main() { use std::mem::{self, MaybeUninit}; // Size of the array is hard-coded but easy to change (meaning, changing just // the constant is sufficient). This means we can't use [a, b, c] syntax to // initialize the array, though, as we would have to keep that in sync // with `SIZE`! const SIZE: usize = 10; let x = { // Create an uninitialized array of `MaybeUninit`. The `assume_init` is // safe because the type we are claiming to have initialized here is a // bunch of `MaybeUninit`s, which do not require initialization. let mut x: [MaybeUninit<Box<u32>>; SIZE] = unsafe { MaybeUninit::uninit().assume_init() }; // Dropping a `MaybeUninit` does nothing. Thus using raw pointer // assignment instead of `ptr::write` does not cause the old // uninitialized value to be dropped. // Exception safety is not a concern because Box can't panic for i in 0..SIZE { x[i] = MaybeUninit::new(Box::new(i as u32)); } // Everything is initialized. Transmute the array to the // initialized type. unsafe { mem::transmute::<_, [Box<u32>; SIZE]>(x) } }; dbg!(x); }
这段代码分三个步骤进行
-
创建一个
MaybeUninit<T>
数组。在当前稳定的 Rust 中,我们必须为此使用不安全的代码:我们取一些未初始化的内存(MaybeUninit::uninit()
),并声称我们已经完全初始化了它(assume_init()
)。这似乎很荒谬,因为我们没有!这样做是正确的原因是,数组本身完全由MaybeUninit
组成,而MaybeUninit
实际上不需要初始化。对于大多数其他类型,执行MaybeUninit::uninit().assume_init()
会产生该类型的无效实例,所以你就得到了一些未定义行为。 -
初始化数组。这里的微妙之处在于,通常,当我们使用
=
将值赋值给 Rust 类型检查器认为已经初始化的值(如x[i]
)时,存储在左侧的旧值会被丢弃。这将会是一场灾难。然而,在这种情况下,左侧的类型是MaybeUninit<Box<u32>>
,并且丢弃它不会执行任何操作!请参见下文对这个drop
问题的更多讨论。 -
最后,我们必须更改数组的类型以删除
MaybeUninit
。在当前稳定的 Rust 中,这需要使用transmute
。这种 transmute 是合法的,因为在内存中,MaybeUninit<T>
看起来与T
相同。但是,请注意,通常,
Container<MaybeUninit<T>>
不 看起来与Container<T>
相同!想象一下,如果Container
是Option
,而T
是bool
,那么Option<bool>
利用了bool
只有两个有效值的事实,但Option<MaybeUninit<bool>>
不能这样做,因为bool
不必被初始化。因此,是否允许 transmute 掉
MaybeUninit
取决于Container
。对于数组,它是允许的(最终标准库将通过提供适当的方法来承认这一点)。
值得花更多时间研究中间的循环,特别是赋值运算符及其与 drop
的交互。如果我们写成类似
*x[i].as_mut_ptr() = Box::new(i as u32); // WRONG!
那样,我们实际上会覆盖一个 Box<u32>
,导致 drop
未初始化的数据,这将导致很多悲伤和痛苦。
如果出于某种原因我们不能使用 MaybeUninit::new
,则正确的替代方法是使用 ptr
模块。特别是,它提供了三个函数,允许我们将字节分配到内存中的某个位置,而无需丢弃旧值:write
,copy
和 copy_nonoverlapping
。
ptr::write(ptr, val)
获取一个val
并将其移动到ptr
指向的地址。ptr::copy(src, dest, count)
将count
个 T 项将占用的位从 src 复制到 dest。(这等效于 C 的 memmove -- 请注意参数顺序是相反的!)ptr::copy_nonoverlapping(src, dest, count)
执行与copy
相同的功能,但在假设两个内存范围不重叠的情况下速度稍快。(这等效于 C 的 memcpy -- 请注意参数顺序是相反的!)
不用说,如果滥用这些函数,将导致严重的破坏或直接导致未定义行为。这些函数本身的唯一要求是您要读取和写入的位置已分配并且已正确对齐。但是,将任意位写入内存的任意位置可能破坏事物的方式基本上是数不清的!
值得注意的是,您不必担心使用不实现 Drop
或包含 Drop
类型的 ptr::write
风格的诡计,因为 Rust 知道不要尝试丢弃它们。这就是我们在上面的示例中所依赖的。
但是,当使用未初始化的内存时,您需要时刻警惕 Rust 在完全初始化之前尝试丢弃您这样创建的值。如果变量具有析构函数,则通过该变量范围的每个控制路径都必须在结束之前初始化该值。这包括代码发生 panic。MaybeUninit
在这里有所帮助,因为它不会隐式丢弃其内容 - 但在发生 panic 的情况下,所有这些都意味着您最终会泄漏已初始化的部分的内存,而不是尚未初始化部分的双重释放。
请注意,要使用 ptr
方法,您需要首先获取指向要初始化的数据的原始指针。构造指向未初始化数据的引用是非法的,这意味着在获取上述原始指针时必须小心
- 对于
T
的数组,您可以使用base_ptr.add(idx)
,其中base_ptr: *mut T
来计算数组索引idx
的地址。这依赖于数组在内存中的布局方式。 - 但是,对于结构体,我们通常不知道它是如何布局的,并且我们也不能使用
&mut base_ptr.field
,因为这将创建一个引用。因此,您必须小心使用addr_of_mut
宏。这会创建一个指向字段的原始指针,而不会创建中间引用
#![allow(unused)] fn main() { use std::{ptr, mem::MaybeUninit}; struct Demo { field: bool, } let mut uninit = MaybeUninit::<Demo>::uninit(); // `&uninit.as_mut().field` would create a reference to an uninitialized `bool`, // and thus be Undefined Behavior! let f1_ptr = unsafe { ptr::addr_of_mut!((*uninit.as_mut_ptr()).field) }; unsafe { f1_ptr.write(true); } let init = unsafe { uninit.assume_init() }; }
最后一点说明:在阅读旧的 Rust 代码时,您可能会偶然发现已弃用的 mem::uninitialized
函数。该函数曾经是处理堆栈上未初始化内存的唯一方法,但事实证明不可能与该语言的其余部分正确集成。在新的代码中始终使用 MaybeUninit
代替,并在您有机会时将旧代码移植过来。
这就是关于使用未初始化内存的全部内容!基本上没有任何地方期望被传递未初始化的内存,因此,如果您要传递它,请务必非常小心。