深入RUST标准库内核—内存之MaybeUninit<T>

本文摘自 inside-rust-std-library

mem模块结构及函数

MaybeUninit

MaybeUninit结构定义

源代码如下:


    #[repr(transparent)] 

    pub union MaybeUninit<T> {

        uninit: (),

        value: ManuallyDrop<T>,

    }

MaybeUninit的内存布局就是ManuallyDrop<T>的内存布局,从后文可以看到,ManuallyDrop<T>实际就是T的内存布局。所以MaybeUninit在内存中实质也就是T类型。

RUST的引用使用的内存块必须保证是内存对齐及赋以初始值,未初始化的内存块和清零的内存块都不能满足引用的条件。但堆内存申请后都是未初始化的,且在程序中某些情况下也需要先将内存设置为未初始化,尤其在处理泛型时。因此,RUST提供了MaybeUninit容器来实现对未初始化变量的封装,以便在不引发编译错误完成对T类型未初始化变量的相关操作.

MaybeUninit利用ManuallyDrop的方式对T的未初始化进行了一个标识。这对T也有一个保护,使得未初始化的变量免于被RUST自动调用drop所释放掉.

ManuallyDrop 结构及方法

源代码如下:


#[repr(transparent)]

pub struct ManuallyDrop<T: ?Sized> {

    value: T,

}

一个变量被ManuallyDrop获取所有权后,RUST编译器将不再对其自动调用drop操作。需要代码显式的调用drop来释放置入ManuallyDrop的T类型变量。

ManuallyDrop主要使用场景:

  1. 作为MaybeUninit的内部结构,对未初始化的内存做一个保护和标识。

  2. 希望由代码显式释放变量时。

重点关注的一些方法:

ManuallyDrop<T>::new(val:T) -> ManuallyDrop<T>, 此函数返回ManuallyDrop变量拥有传入的T类型变量所有权,并将此块内存直接用ManuallyDrop封装, 对于val,编译器不再主动做drop操作。


    pub const fn new(value: T) -> ManuallyDrop<T> {

        //所有权转移到结构体内部,编译器将忽略这个所有权

        ManuallyDrop { value }

    }

ManuallyDrop<T>::into_inner(slot: ManuallyDrop<T>)->T, 将封装的T类型变量所有权转移出来,编译器会重新将返回的变量纳入drop管理体系。


    pub const fn into_inner(slot: ManuallyDrop<T>) -> T {

        //将value解封装,所有权转移到返回值中,编译器重新对所有权做处理

        slot.value

    }

ManuallyDrop<T>::drop(slot: &mut ManuallyDrop<T>),手动drop掉内部变量。

ManuallyDrop<T>::deref(&self)-> & T, 返回内部包装的变量的引用


    fn deref(&self) -> &T {

        //返回后,代码可以用&T对self.value做改动

        &self.value

    }

ManuallyDrop<T>::deref_mut(&mut self)-> & mut T返回内部包装的变量的可变引用,返回的引用可正常使用

ManuallyDrop代码举例:


    use std::mem::ManuallyDrop;

    let mut x = ManuallyDrop::new(String::from("Hello World!"));

    x.truncate(5); // 此时会调用deref

    assert_eq!(*x, "Hello");

    // 但对x的drop不会再发生
MaybeUninit 方法

MaybeUninit提供了在GlobalAlloc Trait之外的一种获取内存的方法, 实际上可类比为泛型 new()的一种实现方式,不过返回的不是指针,而是变量。MaybeUninit获取的内存位于栈空间。

MaybeUninit<T>::uninit()->MaybeUninit<T>, 是MaybeUninit栈上申请内存的方法,申请的内存大小是T类型的内存大小,该内存没有初始化。利用泛型和Union内存布局,RUST巧妙的实现了在栈上申请一块未初始化内存。此函数非常非常非常值得关注,是非常多场景下的代码解决方案。


    pub const fn uninit() -> MaybeUninit<T> {

        //变量内存布局与T类型完全一致

        MaybeUninit { uninit: () }

    }

MaybeUninit<T>::new(val:T)->MaybeUninit<T>, 内部用ManuallyDrop封装了val, 然后用MaybeUninit封装ManuallyDrop。因为如果T没有初始化过,调用这个函数会编译失败,所以此时内存实际上已经初始化过了。


    pub const fn new(val: T) -> MaybeUninit<T> {

        //val这个时候是初始化过的。

        MaybeUninit { value: ManuallyDrop::new(val) }

    }

MaybeUninit<T>::zeroed()->MaybeUninit<T>, 申请了T类型内存并清零。


    pub fn zeroed() -> MaybeUninit<T> {

        let mut u = MaybeUninit::<T>::uninit();

        // SAFETY: `u.as_mut_ptr()` points to allocated memory.

        unsafe {

            //必须使用write_bytes,否则无法给内存清0

            u.as_mut_ptr().write_bytes(0u8, 1);

        }

        u

    }

ManuallyDrop<T>::take(slot: &mut ManuallyDrop<T>)->T,实质是复制一个变量,原变量仍然保留在ManuallyDrop中,但所有权已经转移到复制的变量中,后继不能再调用take或into_inner函数,否则可能会导致悬垂指针的问题。


    pub unsafe fn take(slot: &mut ManuallyDrop<T>) -> T {

        // 拷贝内部变量,并返回内部变量的所有权

        unsafe { ptr::read(&slot.value) }

    }

    //此函即ptr::read, 会复制一个变量,此时注意,实际上src指向的变量的所有权已经转移给了返回变量,

    //所以调用此函数的前提是src的所有权必须有后继处理,例如src本身处于ManallyDrop,或src后继调用forget,

    //或给src赋以新的所有权。

    //但在assume_init_read()中使用此函数不会导致问题,因为src被ManuallyDrop封装,不会被释放。

    pub const unsafe fn read<T>(src: *const T) -> T {` 

        //利用MaybeUninit::uninit申请未初始化的T类型内存

        let mut tmp = MaybeUninit::<T>::uninit();

        unsafe {

            //完成内存拷贝

            copy_nonoverlapping(src, tmp.as_mut_ptr(), 1);

            //初始化后的内存移出ManuallyDrop 并返回

            tmp.assume_init()

        }

    }

MaybeUninit<T>::assume_init()->T,代码如下:


    pub const unsafe fn assume_init(self) -> T {

        // 调用者必须保证self已经初始化了

        unsafe {

            intrinsics::assert_inhabited::<T>();

            //把T的所有权返回,编译器会主动对T调用drop

            ManuallyDrop::into_inner(self.value)

        }

    }

MaybeUninit<T>::assume_init_read()->T 此函数最后会调用ptr::read()函数。代码如下:


    pub const unsafe fn assume_init_read(&self) -> T {

        unsafe {

            intrinsics::assert_inhabited::<T>();

            //会调用ptr::read

            self.as_ptr().read()

        }

    }

可见,assume_init_read 方法实际上是从一个已有类型生成并复制一个新的变量。MaybeUninit的变量后继不能再调用assume_init,可能会导致重复drop。

MaybeUninit<T>::assume_init_drop() 对内部变量进行drop操作

MaybeUninit<T>::assume_init_ref()->&T 返回内部T类型变量的借用,调用者应保证内部T类型变量已经初始化,&T此时是完全正常的

MaybeUninit<T>::assume_init_mut()->&mut T返回内部T类型变量的可变借用,调用者应保证内部T类型变量已经初始化,&mut T此时是完全正常的

MaybeUninit<T>::write(val)->&mut T, 代码如下:


    pub const fn write(&mut self, val: T) -> &mut T {

        //通常情况下,如果*self是初始化过得,那调用下面的等式时,会立刻调用*self拥有所有权变量的drop。但因为MaybeUninit<T>封装的变量不会被drop。所以下面这个等式实际上隐含了 *self必须是未初始化的,否则的话,这里会丢失掉已初始化的变量所有权信息,可能造成内存泄漏。

        *self = MaybeUninit::new(val);

        // SAFETY: We just initialized this value.

        unsafe { self.assume_init_mut() }

    }

MaybeUninit<T>::uninit_array<const LEN:usize>()->[Self; LEN] 此处对LEN的使用方式需要注意,这是不常见的一个泛型写法,这个函数同样的申请了一块内存。代码:


    pub const fn uninit_array<const LEN: usize>() -> [Self; LEN] {

        // SAFETY: An uninitialized `[MaybeUninit<_>; LEN]` is valid.

        unsafe { MaybeUninit::<[MaybeUninit<T>; LEN]>::uninit().assume_init() }

    }

这里要注意区别数组类型和数组元素的初始化。对于数组[MaybeUninit;LEN]这一类型本身来说,初始化就是确定整体的内存大小,所以数组类型在声明后就已经完成了。所以此时assume_init()是正确的。这是一个理解上的盲点。

MaybeUninit<T>::array_assume_init<const N:usize>(array: [Self; N]) -> [T; N] 这个函数没有把所有权转移出来,代码分析如下:


    pub unsafe fn array_assume_init<const N: usize>(array: [Self; N]) -> [T; N] {

        // SAFETY:

        // * The caller guarantees that all elements of the array are initialized

        // * `MaybeUninit<T>` and T are guaranteed to have the same layout

        // * `MaybeUninit` does not drop, so there are no double-frees

        // And thus the conversion is safe

        unsafe {

            //最后是调用是*const T::read(),此处 as *const _的写法可以简化代码,这里没有把T类型变量所有权转移到返回值

            //返回后,此MaybeUninit变量应该被丢弃

            (&array as *const _ as *const [T; N]).read()

        }

    }

MaybeUninit一些典型使用代码例子:


    use std::mem::MaybeUninit;

    // Create an explicitly uninitialized reference. The compiler knows that data inside

    // a `MaybeUninit<T>` may be invalid, and hence this is not UB:

    // 获得一个未初始化的i32引用类型内存

    let mut x = MaybeUninit::<&i32>::uninit();

    // Set it to a valid value.

    // 将&0写入变量,完成初始化

    x.write(&0);

    // Extract the initialized data -- this is only allowed *after* properly

    // initializing `x`!

    // 将初始化后的变量解封装供后继的代码使用。

    let x = unsafe { x.assume_init() };

以上代码,编译器不会对x.write进行报警,这是MaybeUninit的最重要的应用,这个例子展示了RUST如何给未初始化内存赋值的处理方式。调用assume_init前,必须保证变量已经被正确初始化。

更复杂的例子:


    use std::mem::{self, MaybeUninit};

    let data = {

    // 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.

    // data在声明后实际上就已经初始化完毕。

    let mut data: [MaybeUninit<Vec<u32>>; 1000] = unsafe {

        //这里注意实际调用是MaybeUninit::<[MaybeUninit<Vec<u32>>;1000]>::uninit(), RUST的类型推断机制完成了泛型实例化

        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. Also if there is a panic during

    // this loop, we have a memory leak, but there is no memory safety

    // issue.

    for elem in &mut data[..] {

    elem.write(vec![42]);

    }

    // Everything is initialized. Transmute the array to the

    // initialized type.

    // 直接用transmute完成整个数组类型的转换

    unsafe { mem::transmute::<_, [Vec<u32>; 1000]>(data) }

    };

    assert_eq!(&data[0], &[42]);

下面例子说明一块内存被 MaybeUnint封装后,编译器将不再对其做释放,必须在代码中显式释放:


    use std::mem::MaybeUninit;

    use std::ptr;

    // 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 data: [MaybeUninit<String>; 1000] = unsafe { MaybeUninit::uninit().assume_init() };

    // 初始化了500个String变量

    let mut data_len: usize = 0;

    for elem in &mut data[0..500] {

        //write没有将所有权转移出ManuallyDrop

        elem.write(String::from("hello"));

        data_len += 1;

    }

    // For each item in the array, drop if we allocated it.

    //rust不能自动去释放已经申请的String, 必须手工调用drop_in_place释放

    for elem in &mut data[0..data_len] {

        unsafe { ptr::drop_in_place(elem.as_mut_ptr()); }

    }

上例中,在没有assume_init()调用的情况下,必须手工调用drop_in_place释放内存。

MaybeUninit是一个非常重要的类型结构,未初始化内存是编程中不可避免要遇到的情况,MaybeUninit也就是RUST编程中必须熟练使用的一个类型。

本作品采用《CC 协议》,转载必须注明作者和本文链接
Warren Ren
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!