未检查的未初始化内存

此规则的一个有趣的例外是处理数组。安全的 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。此转移是合法的,因为在内存中,MaybeUninit<T> 看起来与 T 相同。

    但是,请注意,一般来说,Container<MaybeUninit<T>>> 看起来与 Container<T> 不同!想象一下,如果 ContainerOption,而 Tbool,那么 Option<bool> 利用了 bool 只有两个有效值,但 Option<MaybeUninit<bool>> 不能这样做,因为 bool 不必初始化。

    因此,是否允许转移 MaybeUninit 取决于 Container。对于数组,这是允许的(最终标准库将通过提供适当的方法来承认这一点)。

值得花更多时间在中间的循环上,特别是赋值运算符及其与 drop 的交互。如果我们写了类似

*x[i].as_mut_ptr() = Box::new(i as u32); // WRONG!

我们实际上会覆盖 Box<u32>,导致丢弃未初始化的数据,这将导致很多悲伤和痛苦。

如果由于某种原因我们不能使用 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 试图在您创建的值完全初始化之前丢弃它们。如果变量的范围中的每个控制路径都有析构函数,则必须在该路径结束之前初始化该值。*这包括代码恐慌*。MaybeUninit 在这里有所帮助,因为它不会隐式丢弃其内容 - 但所有这些在出现恐慌时真正意味着,您最终会遇到已经初始化部分的内存泄漏,而不是尚未初始化部分的双重释放。

请注意,要使用 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 来代替,并在有机会时将旧代码移植过来。

以上就是关于处理未初始化内存的全部内容!基本上没有任何地方期望被传递未初始化的内存,所以如果你要传递它,一定要非常小心。