未检查的未初始化内存

这个规则一个有趣的例外是处理数组。安全的 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);
}

这段代码分三个步骤进行

  1. 创建一个 MaybeUninit<T> 数组。在当前稳定的 Rust 中,我们必须为此使用不安全的代码:我们取一些未初始化的内存(MaybeUninit::uninit()),并声称我们已经完全初始化了它(assume_init())。这似乎很荒谬,因为我们没有!这样做是正确的原因是,数组本身完全由 MaybeUninit 组成,而 MaybeUninit 实际上不需要初始化。对于大多数其他类型,执行 MaybeUninit::uninit().assume_init() 会产生该类型的无效实例,所以你就得到了一些未定义行为。

  2. 初始化数组。这里的微妙之处在于,通常,当我们使用 = 将值赋值给 Rust 类型检查器认为已经初始化的值(如 x[i])时,存储在左侧的旧值会被丢弃。这将会是一场灾难。然而,在这种情况下,左侧的类型是 MaybeUninit<Box<u32>>,并且丢弃它不会执行任何操作!请参见下文对这个 drop 问题的更多讨论。

  3. 最后,我们必须更改数组的类型以删除 MaybeUninit。在当前稳定的 Rust 中,这需要使用 transmute。这种 transmute 是合法的,因为在内存中,MaybeUninit<T> 看起来与 T 相同。

    但是,请注意,通常,Container<MaybeUninit<T>> 看起来与 Container<T> 相同!想象一下,如果 ContainerOption,而 Tbool,那么 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 模块。特别是,它提供了三个函数,允许我们将字节分配到内存中的某个位置,而无需丢弃旧值:writecopycopy_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 在完全初始化之前尝试丢弃您这样创建的值。如果变量具有析构函数,则通过该变量范围的每个控制路径都必须在结束之前初始化该值。这包括代码发生 panicMaybeUninit 在这里有所帮助,因为它不会隐式丢弃其内容 - 但在发生 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 代替,并在您有机会时将旧代码移植过来。

这就是关于使用未初始化内存的全部内容!基本上没有任何地方期望被传递未初始化的内存,因此,如果您要传递它,请务必非常小心。