当前位置:   article > 正文

深入RUST标准库内核(二)裸指针/MaybeUninit<T>_rust maybeuninit

rust maybeuninit

本书摘自《深入理解RUST标准库》,即将发售,敬请期待

RUST泛型小议

RUST是一门生存在泛型的基础之上的语言。其他语言不使用泛型也不影响编程,泛型只是一个语法中的强大工具。与之相对,RUST是离开了泛型就无法完成程序编写,泛型与语法共生。

直接针对泛型的方法和trait实现

其他语言的泛型,是作为类型结构体成员,或是函数的输入/返回参数出现在代码中,是配角。RUST的泛型则可以作为主角,可以直接对泛型实现方法和trait。如:

//T:?Sized基本上就是所有的类型,直接impl <T> Borrow<T>实际上隐含了 T:Sized。所以 T:?Sized比T范围更广阔
impl<T: ?Sized> Borrow<T> for T {
    fn borrow(&self) -> &T {
        self
    }
}

impl<T: ?Sized> BorrowMut<T> for T {
    fn borrow_mut(&mut self) -> &mut T {
        self
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

以上代码基本上就是对所有的类型都实现了Borrow的trait。
直接针对泛型做方法和trait的实现是强大的工具,它的作用:

  • 针对泛型的代码会更内聚,方法总比函数具备更明显的模块性
  • 逻辑更清晰及系统化更好
  • 具备更好的可扩展性
  • 更好的支持函数式编程

泛型的层次关系

RUST的泛型从一般到特殊会形成一种层次结构,有些类似于面对对象的基类和子类关系:
最基层: T 没有任何约束的T是泛型的基类
一级子层: 裸指针类型* const T/* mut T; 切片类型[T]; 数组类型[T;N]; 引用类型&T/&mut T; trait约束类型T:trait; 泛型元组(T, U...); 泛型复合类型struct <T>; enum <T>; union<T> 及具体类型 u8/u16/i8/bool/f32/&str/String...
二级子层: 对一级子层的T赋以具体类型 如:* const u8; [i32],或者将一级子层中的T再次做一级子层的具化,例如:* const [T]; [*const T]; &(*const T); * const T where T:trait; struct <T:trait>

可以一直递归下去,但没有太多的意义。
显然,针对基层类型实现的方法和trait可以应用到层级高的泛型类型中。
例如:

impl <T> Option<T> {...}
impl<T, U> Option<(T, U)> {...}
impl<T: Copy> Option<&T> {...}
impl<T: Default> Option<T> {...}
  • 1
  • 2
  • 3
  • 4

以上是标准库对Option 的不同泛型进行的方法实现定义。一般先针对基层泛型实现方法及trait,然后再针对高层次的泛型做方法及trait实现。

类似的实现再试举如下几例:

impl <T:?Sized> *const T {...}
impl <T:?Sized> *const [T] {...}
impl <T:?Sized> *mut T{ ...}
impl <T:?Sized> *mut [T] {...}
impl <T> [T] { ...}
impl <T, const N:usize> [T;N]{...}
impl AsRef<[u8]> for str {...}
impl AsRef<str> for str {...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

RUST中,可以定义新的trait, 并根据需要在已定义的类型上实现新的trait。这就显然比其他的面对对象的语言具备更好的可扩展性。

RUST标准库内存模块代码分析

内存模块的代码路径举例如下(以作者电脑上的路径):
%USER%.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\alloc*.*
%USER%.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\ptr*.*
%USER%.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\mem*.*
%USER%.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\intrinsic.rs
%USER%.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\alloc\src\alloc.rs

RUST之所以被认为难学,是因为RUST与C相同,需要对内存做彻底的控制,即程序可以在代码中编写专属内存管理系统,并将内存管理系统与语言类型相关联,将内存块与语言类型做自如的转换。对于当前现代语法的高级语言如Java/Python/JS/Go,内存管理实际上是编译器的任务,这就导致大部分程序员对于内存管理缺乏经验,所以对RUST内存安全相关的所有权/生命周期等缺乏实践认知。相对于C,RUST的现代语法特性及内存安全导致RUST的内存块与类型系统的转换的细节不容易被透彻理解。本节将从标准库的内存模块的代码分析中给出RUST内存的本质。理解了RUST内存及内存安全,RUST语言的最难关便过了。

从内存角度考察一个变量,则每个变量具备统一的内存参数,这些参数是:

  1. 变量的首地址,是一个usize的数值
  2. 变量类型占用的内存块大小
  3. 变量类型内存字节对齐的基数
  4. 变量类型中成员内存顺序

如果变量成员是复合类型,可递归上面的四个参数。
RUST认为变量类型成员顺序与编译优化不可分割,因此,变量成员内存顺序完全由编译器控制,这与C不同,C中变量类型成员的顺序是不能被编译器改动的。这使得C变量的内存布局对程序员是透明的。这种透明性导致了C语言在设计类型内存布局的操作中会出现很多坏代码。如,直接用头指针+偏移数值来获得类型内部变量的指针,直接导致变量类型可修改性极差。
与C相同,RUST具备将一块内存块直接转换成某一类型变量的能力。这一能力是RUST操作系统内核编程及高效的一个基石。但因为这个转换使得代码可以绕过编译器的类型系统检查,造成了BUG也绕过了编译器的某些错误检查,而这些错误很可能在系统运行很久之后才真正的出错,造成排错的极高成本。
GC类语言去掉了这一能力,但也牺牲了性能,且无法作为系统级别语言。RUST没有因噎废食,在保留能力的同时给出这一能力明确的危险标识unsafe, 加上整体的内存安全框架设计,使得此类错误更易被发现,更易被定位,极大的降低了错误的数目及排错的成本。
unsafe容易让初学RUST语言的程序员产生排斥感,但unsafe实际上是RUST不可分割的部分,一个好的RUST程序员绝不是不使用unsafe,而是能够准确的把握好unsafe使用的合适场合及合适范围,必要的时候必须使用,但不滥用。

掌握RUST的内存,主要有如下几个部分:

  1. 编译器提供的固有内存操作函数
  2. 内存块与类型系统的结合点:裸指针 *const T/*mut T
  3. 裸指针的包装结构: NonNull<T>/Unique<T>
  4. 未初始化内存块的处理:MaybeUninit<T>/ManuallyDrop<T>
  5. 堆内存申请及释放

裸指针标准库代码分析

裸指针*const T/* mut T将内存和类型系统相连接,*const T代表了一个内存块,指示了内存块首地址,大小,对齐等属性,以及后文提到的元数据,但不保证这个内存块的有效性和安全性。
*const T/* mu T不同,&T/&mut T则保证内存块是安全和有效的,这表示&T/&mut T满足内存块首地址对齐,内存块已经完成了初始化。在RUST中,&T/&mut T是被绑定在某一内存块上,只能用于读写这一内存块。
对于内存块更复杂的操作,由*const T/*mut T 负责,主要有:

  1. 将usize类型数值强制转换成裸指针类型,以此数值为首地址的内存块被转换为相应的类型。这一转换是不安全的。
  2. 在不同的裸指针类型之间进行强制转换,实质上完成了裸指针指向的内存块的类型强转,这一转换是不安全的。
  3. *const u8作为堆内存申请的内存块绑定变量
  4. 内存块置值操作,如清零或置一个魔术值
  5. 显示的内存块拷贝操作,某些情况下,内存块拷贝是必须的高性能方式。
  6. 利用指针偏移计算获取新的内存块, 在数组及切片访问,字符串,协议字节填写,文件缓存等都需要指针偏移计算。
  7. 从外部的C函数接口对接的指针参数
    8…

RUST的裸指针类型不象C语言的指针类型那样仅仅是一个地址值,为满足实现内存安全的类型系统需求,并兼顾内存使用效率和方便性,RUST的裸指针实质是一个较复杂的类型结构体。

裸指针具体实现

*const T/*mut T实质是个数据结构体,由两个部分组成,第一个部分是一个内存地址,第二个部分对这个内存地址的约束性描述-元数据

//从下面结构定义可以看到,裸指针本质就是PtrComponents<T>
pub(crate) union PtrRepr<T: ?Sized> {
    pub(crate) const_ptr: *const T,
    pub(crate) mut_ptr: *mut T,
    pub(crate) components: PtrComponents<T>,
}

pub(crate) struct PtrComponents<T: ?Sized> {
    //*const ()保证元数据部分是空 
    pub(crate) data_address: *const (),
    //不同类型指针的元数据
    pub(crate) metadata: <T as Pointee>::Metadata,
}

//从下面Pointee的定义可以看到一个RUST的编程技巧,即Trait可以只用来实现对关联类型的指定,Pointee这一Trait即只用来指定Metadata的类型。
pub trait Pointee {
    /// The type for metadata in pointers and references to `Self`.
    type Metadata: Copy + Send + Sync + Ord + Hash + Unpin;
}
//廋指针元数据是单元类型,即是空
pub trait Thin = Pointee<Metadata = ()>;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

元数据的规则:

  • 对于固定大小类型的指针(实现了 Sized Trait), RUST定义为廋指针(thin pointer),元数据大小为0,类型为(),这里要注意,RUST中数组也是固定大小的类型,运行中对数组下标合法性的检测,就是比较是否已经越过了数组的内存大小。
  • 对于动态大小类型的指针(DST 类型),RUST定义为胖指针(fat pointer 或 wide pointer), 元数据为:
    • 对于结构类型,如果最后一个成员是动态大小类型(结构的其他成员不允许为动态大小类型),则元数据为此动态大小类型
      的元数据
    • 对于str类型, 元数据是按字节计算的长度值,元数据类型是usize
    • 对于切片类型,例如[T]类型,元数据是数组元素的数目值,元数据类型是usize
    • 对于trait对象,例如 dyn SomeTrait, 元数据是 [DynMetadata][DynMetadata](后面代码解释)
      (例如:DynMetadata)
      随着RUST的发展,有可能会根据需要引入新的元数据种类。

在标准库代码当中没有指针类型如何实现Pointee Trait的代码,编译器针对每个类型自动的实现了Pointee。
如下为rust编译器代码的一个摘录

    pub fn ptr_metadata_ty(&'tcx self, tcx: TyCtxt<'tcx>) -> Ty<'tcx> {
        // FIXME: should this normalize?
        let tail = tcx.struct_tail_without_normalization(self);
        match tail.kind() {
            // Sized types
            ty::Infer(ty::IntVar(_) | ty::FloatVar(_))
            | ty::Uint(_)
            | ty::Int(_)
            | ty::Bool
            | ty::Float(_)
            | ty::FnDef(..)
            | ty::FnPtr(_)
            | ty::RawPtr(..)
            | ty::Char
            | ty::Ref(..)
            | ty::Generator(..)
            | ty::GeneratorWitness(..)
            | ty::Array(..)
            | ty::Closure(..)
            | ty::Never
            | ty::Error(_)
            | ty::Foreign(..)
            | ty::Adt(..)
            // 如果是固定类型,元数据是单元类型 tcx.types.unit,即为空
            | ty::Tuple(..) => tcx.types.unit,

            //对于字符串和切片类型,元数据为长度tcx.types.usize,是元素长度
            ty::Str | ty::Slice(_) => tcx.types.usize,

            //对于dyn Trait类型, 元数据从具体的DynMetadata获取*
            ty::Dynamic(..) => {
                let dyn_metadata = tcx.lang_items().dyn_metadata().unwrap();
                tcx.type_of(dyn_metadata).subst(tcx, &[tail.into()])
            },
            
            //以下类型不应有元数据
            ty::Projection(_)
            | ty::Param(_)
            | ty::Opaque(..)
            | ty::Infer(ty::TyVar(_))
            | ty::Bound(..)
            | ty::Placeholder(..)
            | ty::Infer(ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_)) => {
                bug!("`ptr_metadata_ty` applied to unexpected type: {:?}", tail)
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

以上代码中的中文注释比较清晰的说明了编译器对每一个类型(或类型指针)都实现了Pointee中元数据类型的获取。
对于Trait对象的元数据的具体结构定义见如下代码:

//dyn Trait裸指针的元数据结构
pub struct DynMetadata<Dyn: ?Sized> {
    //堆中的函数VTTable变量的指针
    vtable_ptr: &'static VTable,
    //标示结构对Dyn的所有权关系,
    //其中PhantomData与具体变量的联系在初始化时由编译器自行推断完成, 这里PhantomData主要对编译器做出
    //提示做Drop check是需要注意Dyn由此结构drop。
    phantom: crate::marker::PhantomData<Dyn>,
}

struct VTable {
    //指向实现Trait的类型结构体的drop_in_place函数的指针
    drop_in_place: fn(*mut ()),
    //指向实现Trait的类型结构体的大小
    size_of: usize,
    //指向实现Trait的类型结构体字节对齐
    align_of: usize,
    //后继是结构体实现Trait的所有方法的指针数组
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

元数据类型相同的裸指针可以任意的转换,例如:可以有 * const [usize; 3] as * const[usize; 5] 这种语句
元数据类型不同的裸指针之间不能转换,例如;* const [usize;3] as *const[usize] 这种语句无法通过编译器

裸指针的操作函数——intrinsic模块内存相关固有函数

intrinsics模块中的函数由编译器内置实现,并提供给其他模块使用。固有函数不提供代码,所以对其主要是了解功能和如何使用,intrinsics模块的内存函数一般不由库以外的代码直接调用,而是由mem模块和ptr模块封装后再提供给其他模块。

内存申请及释放函数:
intrinsics::drop_in_place<T:Sized?>(to_drop: * mut T) 在某些情况下,我们会主动的将变量设置成不允许编译器自动调用变量的drop函数, 此时如果仍然需要对变量调用drop,则在代码中显示调用此函数以出发对T类型的drop调用。
intrinsics::forget<T:Sized?> (_:T), 代码中调用这个函数后,编译器不对forget的变量自动调用变量的drop函数。
intrinsics::needs_drop<T>()->bool, 判断T类型是否需要做drop操作,实现了Copy Trait的类型会返回false

类型转换:
intrinsics::transmute<T,U>(e:T)->U, 对于内存布局相同的类型 T和U, 完成将类型T变量转换为类型U变量,此时T的所有权将转换为U的所有权

指针偏移函数:
intrinsics::offset<T>(dst: *const T, offset: usize)->* const T, 相当于C的类型指针加计算
intrinsics::ptr_offset_from<T>(ptr: *const T, base: *const T) -> isize 基于类型T内存布局的两个裸指针之间的偏移量

内存块内容修改函数:
intrinsics::copy<T>(src:*const T, dst: *mut T, count:usize), 内存拷贝, src和dst内存可重叠, 类似c语言中的memmove, 此时dst原有内存如果已经初始化,则会出现内存泄漏。src的所有权实际会被复制,从而也造成重复drop问题。
intrinsics::copy_no_overlapping<T>(src:*const T, dst: * mut T, count:usize), 内存拷贝, src和dst内存不重叠
intrinsics::write_bytes(dst: *mut T, val:u8, count:usize) , C语言的memset的RUST实现, 此时,原内存如果已经初始化,则原内存的变量可能造成内存泄漏,且因为编译器会继续对dst的内存块做drop调用,有可能会UB。

类型内存参数函数:
intrinsics::size_of<T>()->usize 类型内存空间字节大小
intrinsics::min_align_of<T>()->usize 返回类型对齐字节大小
intrinsics::size_of_val<T>(_:*const T)->usize返回指针指向的变量内存空间字节大小
intrinsics::min_align_of_val<T>(_: * const T)->usize 返回指针指向的变量对齐字节大小

禁止优化的内存函数:
形如volatile_xxxx 的函数是通知编译器不做内存优化的操作函数,一般硬件相关操作需要禁止优化。
intrinsics::volatile_copy_nonoverlapping_memory<T>(dst: *mut T, src: *const T, count: usize) 内存拷贝
intrinsics::volatile_copy_memory<T>(dst: *mut T, src: *const T, count: usize) 功能类似C语言memmove
intrinsics::volatile_set_memory<T>(dst: *mut T, val: u8, count: usize) 功能类似C语言memset
intrinsics::volatile_load<T>(src: *const T) -> T读取内存或寄存器,字节对齐
intrinsics::volatile_store<T>(dst: *mut T, val: T)内存或寄存器写入,字节对齐
intrinsics::unaligned_volatile_load<T>(src: *const T) -> T 字节非对齐
intrinsics::unaligned_volatile_store<T>(dst: *mut T, val: T)字节非对齐

内存比较函数:
intrinsics::raw_eq<T>(a: &T, b: &T) -> bool 内存比较,类似C语言memcmp
pub fn ptr_guaranteed_eq<T>(ptr: *const T, other: *const T) -> bool 判断两个指针是否判断, 相等返回ture, 不等返回false
pub fn ptr_guaranteed_ne<T>(ptr: *const T, other: *const T) -> bool 判断两个指针是否不等,不等返回true

裸指针方法

RUST针对*const T/*mut T的类型实现了若干方法,能够对语言的原生类型实现方法,并能够扩展,这个是在其他语言中少见的:

impl <T:?Sized> * const T {
    ...
}
impl <T:?Sized> *mut T{
    ...
}
impl <T> *const [T] {
    ...
}
impl <T> *mut [T] {
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

对于裸指针,RUST 将之分为最基础的 * const T/* mut T, 以及在* const T/*mut T 基础上特化的切片类型[T]的裸指针* const [T]/*mut [T], 可以认为 * const[T]* const T的子类,当然,RUST不使用这个定义。
标准库针对这两种类型实现了一些关联函数及方法。这里一定注意,所有针对 * const T的方法在* const [T]上都是适用的。

以上有几点值得注意:

  1. 可以针对原生类型实现方法(实现Trait),这体现了RUST类型系统的强大扩展性,也是对函数式编程的强大支持
  2. 针对泛型约束实现方法,我们可以大致认为*const T/* mut T实质是一种泛型约束,*const [T]/*mut [T]是更进一步的约束,这使得RUST可以具备更好的数据抽象能力,简化代码,复用模块。
裸指针的创建

直接从已经初始化的变量创建裸指针:

    &T as *const T;
    &mut T as * mut T;
  • 1
  • 2

直接用usize的数值创建裸指针:

    {
        let  a: usize = 0xf000000000000000;
        unsafe {a as * const T}
    }
  • 1
  • 2
  • 3
  • 4

操作系统内核经常需要直接将一个地址数值转换为某一类型的裸指针

RUST也提供了一些其他的裸指针创建关联函数:
ptr::null<T>() -> *const T 创建一个0值的*const T,实际上就是 0 as *const T,用null()函数明显更符合程序员的习惯
ptr::null_mut<T>()->*mut T 除了类型以外,其他同上
ptr::from_raw_parts<T: ?Sized>(data_address: *const (), metadata: <T as Pointee>::Metadata) -> *const T 从内存地址和元数据创建裸指针
ptr::from_raw_parts_mut<T: ?Sized>(data_address: *mut (), metadata: <T as Pointee>::Metadata) -> *mut T 功能同上,创建可变裸指针
RUST裸指针类型转换时,经常使用以上两个函数获得需要的指针类型。

切片类型的裸指针创建函数如下:
ptr::slice_from_raw_parts<T>(data: *const T, len: usize) -> *const [T]
ptr::slice_from_raw_parts_mut<T>(data: *mut T, len: usize) -> *mut [T] 由裸指针类型及切片长度获得切片类型裸指针,调用代码应保证data事实上就是切片的裸指针地址。由类型裸指针转换为切片类型裸指针最突出的应用之一是内存申请,申请的内存返回 * const u8的指针,这个裸指针是没有包含内存大小的,只有头地址,因此需要将这个指针转换为 * const [u8],将申请的内存大小包含入裸指针结构体中。
slice_from_raw_parts代码如下:

pub const fn slice_from_raw_parts<T>(data: *const T, len: usize) -> *const [T] {
    //data.cast()将*const T转换为 *const()
    from_raw_parts(data.cast(), len)
}

pub const fn from_raw_parts<T: ?Sized>(
    data_address: *const (),
    metadata: <T as Pointee>::Metadata,
) -> *const T {
    //由以下代码可以确认 * const T实质就是PtrRepr类型结构体。
    unsafe { PtrRepr { components: PtrComponents { data_address, metadata } }.const_ptr }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
不属于方法的裸指针函数

ptr::drop_in_place<T: ?Sized>(to_drop: *mut T) 此函数是编译器实现的,用于不需要RUST自动drop时,由程序代码调用以释放内存
ptr::metadata<T: ?Sized>(ptr: *const T) -> <T as Pointee>::Metadata 用来返回裸指针的元数据
ptr::eq<T>(a: *const T, b: *const T)->bool 比较指针,此处需要注意,地址比较不但是地址,也比较元数据
ptr模块的函数大部分逻辑都比较简单。很多就是对intrinsic 函数做调用。

裸指针类型转换方法

裸指针类型之间的转换:
*const T::cast<U>(self) -> *const U ,本质上就是一个*const T as *const U。利用RUST的类型推断,此函数可以简化代码并支持链式调用。
*mut T::cast<U>(self)->*mut U 同上。
调用以上的函数要注意,如果后继要把返回的指针转换成引用,那必须保证T类型与U类型内存布局完全一致。如果仅仅是将返回值做数值应用,则此约束可以不遵守,cast函数转换后的类型通常由编译器自行推断,有时需要仔细分析。

裸指针与引用之间的类型转换:
*const T::as_ref<`a>(self) -> Option<&`a T>将裸指针转换为引用,因为*const T可能为零,所有需要转换为Option<& `a T>类型,转换的安全性由程序员保证,尤其注意满足RUST对引用的安全要求。这里要注意,转换后的生命周期实际上与原指针指向变量的生命周期相独立。因此,生命周期的正确性将由程序员保证。
*mut T::as_ref<`a>(self)->Option<&`a T>同上
*mut T::as_mut<`a>(self)->Option<&`a mut T>同上,但转化类型为 &mut T。

切片类型裸指针类型转换:
ptr::*const [T]::as_ptr(self) -> *const T 将切片类型的裸指针转换为切片成员类型的裸指针, 这个转换会导致指针的元数据丢失
ptr::*mut [T]::as_mut_ptr(self) -> *mut T 同上

裸指针结构体属性相关方法:

ptr::*const T::to_raw_parts(self) -> (*const (), <T as super::Pointee>::Metadata)
ptr::*mut T::to_raw_parts(self)->(* const (), <T as super::Pointee>::Metadata) 由裸指针获得地址及元数据
ptr::*const T::is_null(self)->bool
ptr::*mut T::is_null(self)->bool此函数判断裸指针的地址值是否为0

切片类型裸指针:
ptr::*const [T]:: len(self) -> usize 获取切片长度,直接从裸指针的元数据获取长度
ptr:: *mut [T]:: len(self) -> usize 同上

裸指针偏移计算相关方法

ptr::*const T::offset(self, count:isize)->* const T 得到偏移后的裸指针
ptr::*const T::wrapping_offset(self, count: isize) -> *const T 考虑溢出绕回的offset
ptr::*const T::offset_from(self, origin: *const T) -> isize 计算两个裸指针的offset值
ptr::*mut T::offset(self, count:isize)->* mut T 偏移后的裸指针
ptr::*const T::wrapping_offset(self, count: isize) -> *const T 考虑溢出绕回的offset
ptr::*const T::offset_from(self, origin: *const T) -> isize 计算两个裸指针的offset值
以上两个方法基本上通过intrinsic的函数实现

ptr::*const T::add(self, count: usize) -> Self
ptr::*const T::wraping_add(self, count: usize)->Self
ptr::*const T::sub(self, count:usize) -> Self
ptr::*const T::wrapping_sub(self, count:usize) -> Self
ptr::*mut T::add(self, count: usize) -> Self
ptr::*mut T::wraping_add(self, count: usize)->Self
ptr::*mut T::sub(self, count:usize) -> Self
ptr::*mut T::wrapping_sub(self, count:usize) -> Self
以上是对offset函数的包装,使之更符合语义习惯,并便于理解

裸指针直接赋值方法
    //该方法用于仅给指针结构体的 address部分赋值  
    pub fn set_ptr_value(mut self, val: *const u8) -> Self {
        // 以下代码因为只修改PtrComponent.address,所以不能直接用相等
        // 代码采取的方案是取self的可变引用,将此引用转换为裸指针的裸指针,
        let thin = &mut self as *mut *const T as *mut *const u8;
        // 这个赋值仅仅做了address的赋值,对于瘦指针,这个相当于赋值操作,
        // 对于胖指针,则没有改变胖指针的元数据。这种操作方式仅仅在极少数的情况下
        // 可以使用,极度危险。
        unsafe { *thin = val };
        self
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

本节还有一部分裸指针方法没有介绍,留到mem模块分析完以后再介绍会更易于理解。

裸指针小结

裸指针相关的代码多数比较简单,重要的是理解裸指针的概念,理解intrinsic 相关函数,这样才能够准确的理解代码。

RUST引用&T的安全要求
  1. 引用的内存地址必须满足类型T的内存对齐要求
  2. 引用的内存内容必须是初始化过的
    举例:
   #[repr(packed)]
   struct RefTest {a:u8, b:u16, c:u32}
   fn main() {
       let test = RefTest{a:1, b:2, c:3};
       //下面代码无法通过编译,因为test.b 内存字节位于奇数,无法用于借用
       let ref1 = &test.b
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

MaybeUninit标准库代码分析

RUST对于变量的要求是必须初始化后才能使用,否则就会编译告警。但在程序中,总有内存还未初始化,但需要使用的情况:

  1. 从堆申请的内存块,这些内存块都是没有初始化的
  2. 需要定义一个新的泛型变量时,并且不合适用转移所有权进行赋值时
  3. 需要定义一个新的变量,但希望不初始化便能使用其引用时
  4. 定义一个数组,但必须在后继代码对数组成员初始化时

为了处理这种需要在代码中使用未初始化内存的情况,RUST标准库定义了MaybeUninit

MaybeUninit结构定义

源代码如下:

    #[repr(transparent)] 
    pub union MaybeUninit<T> {
        uninit: (),
        value: ManuallyDrop<T>,
    }
  • 1
  • 2
  • 3
  • 4
  • 5

属性repr(transparent)实际上表示外部的封装结构在内存中等价于内部的变量,
MaybeUninit的内存布局就是ManuallyDrop<T>的内存布局,从后文可以看到,ManuallyDrop<T>实际就是T的内存布局。所以MaybeUninit在内存中实质也就是T类型。
RUST的引用使用的内存块必须保证内存对齐及赋以初始值,未初始化的内存块和清零的内存块都不能满足引用的条件。但堆内存申请后都是未初始化的,且在程序中某些情况下也需要先将内存设置为未初始化,尤其在处理泛型时。因此,RUST提供了MaybeUninit容器来实现对未初始化变量的封装,以便在不引发编译错误完成对T类型未初始化变量的相关操作.
如果T类型的变量未初始化,那需要显式的提醒编译器不做T类型的drop操作,因为drop操作可能会对T类型内部的变量做处理,从而引用未初始化的内容,造成UB。实际上,未初始化的内存不必做drop。
RUST用ManuallyDrop封装结构完成了对编译器的显示提示,对于用ManuallyDrop封装的变量,生命周期终止的时候编译器不会调用drop操作。

ManuallyDrop 结构及方法

源代码如下:

#[repr(transparent)]
pub struct ManuallyDrop<T: ?Sized> {
    value: T,
}
  • 1
  • 2
  • 3
  • 4

一个变量被ManuallyDrop获取所有权后,RUST编译器将不再对其自动调用drop操作。因此如果封装入ManuallyDrop的变量实际上需要drop,那必须将ManuallyDrop的变量的所有权在后继转移出去。因为对于模块外的代码,value是私有的,所以必须调用方法才能将value的所有权转移出去。

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

    pub const fn new(value: T) -> ManuallyDrop<T> {
        //所有权转移到结构体内部,value生命周期结束时不会引发drop
        ManuallyDrop { value }
    }
  • 1
  • 2
  • 3
  • 4

ManuallyDrop<T>::into_inner(slot: ManuallyDrop<T>)->T, 将封装的T类型变量所有权转移出来,转移出来的变量生命周期终止时,编译器会自动调用类型的drop。

    pub const fn into_inner(slot: ManuallyDrop<T>) -> T {
        //将value解封装,所有权转移到返回值中,编译器重新对所有权做处理
        slot.value
    }
  • 1
  • 2
  • 3
  • 4

ManuallyDrop<T>::drop(slot: &mut ManuallyDrop<T>),drop掉内部变量,封装入ManuallyDrop的变量一定是在程序运行的某一时期不需要编译器drop,所以调用这个函数的时候一定要注意正确性。
ManuallyDrop<T>::deref(&self)-> & T, 返回内部包装的变量的引用

    fn deref(&self) -> &T {
        //返回后,代码可以用&T对self.value做读操作,但不改变drop的规则
        &self.value
    }
  • 1
  • 2
  • 3
  • 4

ManuallyDrop<T>::deref_mut(&mut self)-> & mut T返回内部包装的变量的可变引用,调用代码可以利用可变引用对内部变量赋值,但不改变drop机制

ManuallyDrop代码举例:

    use std::mem::ManuallyDrop;
    let mut x = ManuallyDrop::new(String::from("Hello World!"));
    x.truncate(5); // 此时会调用deref
    assert_eq!(*x, "Hello");
    // 但对x的drop不会再发生
  • 1
  • 2
  • 3
  • 4
  • 5
MaybeUninit 创建方法

MaybeUninit<T>::uninit()->MaybeUninit<T>, 可视为在栈空间上申请内存的方法,申请的内存大小是T类型的内存大小,该内存没有初始化。利用泛型和Union内存布局,RUST巧妙的利用此函数在栈上申请一块未初始化内存。此函数非常非常非常值得关注,在需要在栈空间定义一个未初始化泛型时,应第一时间想到MaybeUninit::::uninit()。

    pub const fn uninit() -> MaybeUninit<T> {
        //变量内存布局与T类型完全一致
        MaybeUninit { uninit: () }
    }
  • 1
  • 2
  • 3
  • 4

MaybeUninit<T>::new(val:T)->MaybeUninit<T>, 内部用ManuallyDrop封装了val, 然后用MaybeUninit封装ManuallyDrop。因为如果T没有初始化过,调用这个函数会编译失败,所以此时内存实际上已经初始化过了。调用此函数要额外注意val的drop必须在后继有交代。

    pub const fn new(val: T) -> MaybeUninit<T> {
        //val这个时候是初始化过的。
        MaybeUninit { value: ManuallyDrop::new(val) }
    }
  • 1
  • 2
  • 3
  • 4

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

    pub fn zeroed() -> MaybeUninit<T> {
        let mut u = MaybeUninit::<T>::uninit();
        unsafe {
            //因为没有初始化,所以不存在所有权问题,
            //必须使用ptr::write_bytes,否则无法给内存清0
            //ptr::write_bytes直接调用了intrinsics::write_bytes
            u.as_mut_ptr().write_bytes(0u8, 1);
        }
        u
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
对未初始化的变量赋值的方法

将值写入MaybeUninit
MaybeUninit<T>::write(val)->&mut T, 这个函数是在未初始化时使用,如果已经调用过write,且不希望解封,那后继的赋值使用返回的&mut T。代码如下:

    pub const fn write(&mut self, val: T) -> &mut T {
        //下面这个赋值,会导致原*self的MaybeUninit<T>的变量生命周期截止,会调用drop。但不会对内部的T类型变量做drop调用。所以如果*self内部的T类型变量已经被初始化且需要做drop,那会造成内存泄漏。所以下面这个等式实际上隐含了self内部的T类型变量必须是未初始化的或者T类型变量不需要drop。
        *self = MaybeUninit::new(val);
        // 函数调用后的赋值用返回的&mut T来做。
        unsafe { self.assume_init_mut() }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
初始化后解封装的方法

用assume_init返回初始化后的变量并消费掉MaybeUninit变量,这是最标准的做法:
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)
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

assume_init_read是不消费self的情况下获得内部T变量,内部T变量的所有权已经转移到返回变量,后继要注意不能再次调用其他解封装函数,否则会出现双份所有权,导致UB

    pub const unsafe fn assume_init_read(&self) -> T {
        
        unsafe {
            intrinsics::assert_inhabited::<T>();
            //会调用ptr::read
            self.as_ptr().read()
        }
    }
    //此函即ptr::read, 会复制一个变量,此时注意,实际上src指向的变量的所有权已经转移给了返回变量,
    //所以调用此函数的前提是src后继一定不能调用T类型的drop函数,例如src本身处于ManallyDrop,或后继对src调用forget,或给src绑定新变量。
    //在RUST中,不支持 let xxx = *(&T) 这种转移所有权的方式,因此对于只有指针输入,又要转移所有权的,智能利用浅拷贝进行粗暴转移。
    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);
            //初始化后的内存解封装并返回
            tmp.assume_init()
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

与上个函数比较类似的ManuallyDrop::take方法,用take函数将变量复制并获得变量的所有权。此时原变量仍然保留在ManuallyDrop中,后继不能再调用其他解封装函数,否则可能会出现UB。这里要特别注意理解take已经把变量的所有权转移到返回变量中。

    pub unsafe fn take(slot: &mut ManuallyDrop<T>) -> T {
        // 拷贝内部变量,并返回内部变量的所有权
        // 返回后,原有的变量所有权已经消失,不能再用into_inner来返回
        // 否则会UB
        unsafe { ptr::read(&slot.value) }
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

MaybeUninit<T>::assume_init_drop(&self) 对于已经初始化过的MaybeUninit, 如果不做他用,必须调用此函数以触发T类型的drop函数。
MaybeUninit<T>::assume_init_ref(&self)->&T 返回内部T类型变量的借用,调用者应保证内部T类型变量已经初始化,返回值按照一个普通的引用使用。应注意返回值的生命周期应该小于self的生命周期
MaybeUninit<T>::assume_init_mut(&mut self)->&mut T返回内部T类型变量的可变借用,调用者应保证内部T类型变量已经初始化,返回值按照一个普通的可变引用使用。应注意返回值的生命周期应该小于self的生命周期

MaybeUninit<[T]>的方法

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

    pub const fn uninit_array<const LEN: usize>() -> [Self; LEN] {
        unsafe { MaybeUninit::<[MaybeUninit<T>; LEN]>::uninit().assume_init() }
    }
  • 1
  • 2
  • 3

这里要注意区别数组类型和数组元素的初始化。对于数组[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] {
        unsafe {
            //最后调用是*const T::read(),此处 as *const _的写法可以简化代码,read后,所有权已经转移到返回值
            //返回后,此数组内所有的MaybeUninit变量成员不能再解封装
            (&array as *const _ as *const [T; N]).read()
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
MaybeUnint典型案列

对T类型变量申请内存及赋值:

    use std::mem::MaybeUninit;

    // 获得一个未初始化的i32引用类型内存
    let mut x = MaybeUninit::<&i32>::uninit();
    // 将&0写入变量,完成初始化
    x.write(&0);
    // 将初始化后的变量解封装供后继的代码使用。
    let x = unsafe { x.assume_init() };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

更复杂的初始化例子:

    use std::mem::{self, MaybeUninit};
    
    let data = {
    // data在声明后实际上就已经初始化完毕。
    let mut data: [MaybeUninit<Vec<u32>>; 1000] = unsafe {
        //这里注意实际调用是MaybeUninit::<[MaybeUninit<Vec<u32>>;1000]>::uninit(), RUST的类型推断机制完成了泛型实例化
        MaybeUninit::uninit().assume_init()
    };
    
    for elem in &mut data[..] {
    elem.write(vec![42]);
    }
    
    // 直接用transmute完成整个数组类型的转换
    // 仔细思考一下,这里除了用transmute,似乎没有其他办法了,
    unsafe { mem::transmute::<_, [Vec<u32>; 1000]>(data) }
    };
    
    assert_eq!(&data[0], &[42]);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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

    use std::mem::MaybeUninit;
    use std::ptr;
   
    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;
    }
    //编译器无法自动调用drop释放String变量, 必须显示用drop_in_place释放
    for elem in &mut data[0..data_len] {
        //实际上也可以调用assume_init_drop来完成此工作
        unsafe { ptr::drop_in_place(elem.as_mut_ptr()); }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

上例中,在没有assume_init()调用的情况下,必须手工调用drop_in_place释放内存。
MaybeUninit是一个非常重要的类型结构,未初始化内存是编程中不可避免要遇到的情况,MaybeUninit也就是RUST编程中必须熟练使用的一个类型。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/702714
推荐阅读
相关标签
  

闽ICP备14008679号