对比现代语法的高级语言如Java/Go/Python等,Rust需要对内存进行控制,即程序可在代码中编写专属内存管理系统,并将内存管理系统与语言类型相关联,在内存块与语言类型间能够自如的进行转换。但相对于C来说,rust的现代语法特性及内存安全会导致rust的内存块与类型系统的转换细节相对非常复杂,不易被透彻理解。接下来从代码层面来深入理解rust内存及内存安全:
Rust标准库内存模块代码
%本地代码路径%/src\rust\library\core\src\alloc*.*
%本地代码路径%\src\rust\library\core\src\ptr*.*
%本地代码路径%\src\rust\library\core\src\mem*.*
%本地代码路径%\src\rust\library\core\src\intrinsic.rs
%本地代码路径%\src\rust\library\alloc\src\alloc.rs
从内存角度来考察一个变量,则每个变量具备统一的内存参数:
- 变量的首地址,是一个usize的数值
- 变量类型占用的内存块大小
- 变量类型内存字节对齐的基数
- 变量类型中成员内存顺序
若是变量成员是复合类型,可递归上面的四个参数.
rust与c的“异同”
不同于C,rust则认为变量类型、成员顺序与编译优化不可分割,因此,变量成员内存顺序完全由编译器控制,C中变量类型成员的顺序是不能被编译器改动的。这使得C变量的内存布局对程序员是透明的。这种透明性导致了C语言在设计类型内存布局的操作中会出现一些"坏代码"
。如,直接用头指针+偏移数值来获得类型内部变量的指针,直接导致变量类型可修改性极差。
但与C相同之处: rust能够将一块内存块直接转换成某一类型变量。这也是rust能够操作系统内核编程及其高效的“核心”。不过也因这个转换使得代码可以绕过编译器的类型系统检查,造成了bug也绕过了编译器的某些错误检查,而这些错误很可能在系统运行很久之后才真正的出错,造成排错的极高成本。
而rust在确保这些的同时,并通过明确标识unsafe, 再加上整体的内存安全框架设计,使得此类错误更易被发现,更易被定位,极大的降低了错误的数目及排错的成本。
不过unsafe让初学rust语言的程序员产生“排斥”,但unsafe实际上是rust不可分割的部分,一个好的rust程序员绝不是不使用unsafe,而是能够准确的把握好unsafe使用的合适场合及合适范围,必要的时候必须使用,但不滥用。
关于rust中的“安全”与“不安全”
接下来为了掌握rust的内存我们会从如下几个部分入手:
- 编译器提供的固有内存操作函数
- 内存块与类型系统的结合点:裸指针
*const T/*mut T
- 裸指针的包装结构:
NonNull<T>/Unique<T>
- 未初始化内存块的处理:
MaybeUninit<T>/ManuallyDrop<T>
- 堆内存申请及释放
针对初始化变量相关的指针操作
关于裸指针
裸指针*const T/* mut T
将内存和类型系统相关联:
1、 *const T代表了一个内存块,指示了内存块首地址,大小,对齐等属性,以及元数据,但不保证这个内存块的有效性和安全性
。
2、与*const T/* mu T
不同: &T/&mut T
则保证内存块是安全和有效的,这表示&T/&mut T
满足内存块首地址对齐,内存块已经完成了初始化。
在rust中,
&T/&mut T
是被绑定在某一内存块上,只能对这一内存块读写。
对于内存块更复杂的操作,由*const T/*mut T
负责
主要有:
将usize类型数值强制转换成裸指针类型,以此数值为首地址的内存块被转换为相应的类型; 不过若是对这一转换后的内存块进行读写,可能造成内存安全问题。
在不同的裸指针类型之间进行强制转换,实质上完成了裸指针指向的内存块的类型强转,若是对这一转换后的内存块进行读写,可能造成内存安全问题。
*const u8
作为堆内存申请的内存块绑定变量 。内存块置值操作,如清零或置一个魔术值 。
显式的内存块拷贝操作,某些情况下,内存块拷贝是必须的高性能方式。
利用指针偏移计算获取新的内存块, 比如在数组及切片访问,字符串,协议字节填写,文件缓存等都需要指针偏移计算。
从外部的C函数接口对接的指针参数。
等等
rust的裸指针类型不像C语言的指针类型那样仅仅是一个地址值,为满足实现内存安全的类型系统需求,并兼顾内存使用效率和方便性,rust的裸指针实质是一个较复杂的类型结构体。
裸指针具体实现
*const T/*mut T
实质是个结构体,由两个部分组成:第一个部分是一个内存地址;第二个部分对这个内存地址的约束性描述-元数据。
伪码如下(并非真实的代码定义)
struct Pointer {
address: usize, // 当前裸指针的地址
metadata: T, // 针对当前指针地址的描述
}
接下来看看rust关于这块的定义,从下面结构定义可以看到,裸指针本质就是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只用来指定Metadata的类型。
pub trait Pointee {
/// The type for metadata in pointers and references to `Self`.
type Metadata: Copy + Send + Sync + Ord + Hash + Unpin;
}
// thin廋指针元数据是单元类型,即是空
pub trait Thin = Pointee<Metadata = ()>;
元数据的规则:
- 对于固定大小类型的指针(实现了
Sized
Trait), 在rust被定义为廋指针(thin pointer),元数据大小为0,类型为(),
需要注意的:rust中数组也是固定大小的类型,运行中对数组下标合法性的检测比较是否已经越过了数组的内存大小。
- 对于动态大小类型的指针(DST 类型),被定义为胖指针(fat pointer 或 wide pointer), 元数据为:
- 对于结构类型,如果最后一个成员是动态类型(struct中的其他成员不允许为动态类型),则元数据为此动态类型的元数据;
- 对于
str
类型, 元数据是按字节计算的长度值,元数据类型是usize; - 对于切片类型,例如
[T]
类型,元数据是数组元素的数目值,元数据类型是usize; - 对于trait对象,例如 dyn XXXTrait, 元数据则是DynMetadata<Self>
(例如:DynMetadata<dyn XXXTrait>);
伴随着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)
}
}
}
以上代码说明了编译器对每一个类型(或类型指针)都实现了Pointee中元数据类型的获取。
对于Trait对象的元数据的具体结构定义见如下代码:
//dyn Trait裸指针的元数据结构
pub struct DynMetadata<Dyn: ?Sized> {
//堆中的VTable变量的引用
vtable_ptr: &'static VTable,
// 标识结构对Dyn的所有权关系,
//其中PhantomData与具体变量的联系在初始化时由编译器自行推断完成,
// 这里PhantomData主要对编译器做出提示:在做Drop check时注意本结构体会负责对Dyn类型变量做drop。
phantom: crate::marker::PhantomData<Dyn>,
}
struct VTable {
//trait对象的drop方法的指针,这里trait对象是一个具体的结构体,它实现了trait
drop_in_place: fn(*mut ()),
//trait对象类型的内存大小
size_of: usize,
//trait对象类型的字节对齐大小
align_of: usize,
//后续是trait对象的所有方法实现的指针数组
}
元数据类型相同的裸指针可以任意的转换,例如:可以有 * 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
读取内存或寄存器,T类型字节对齐到2的幂次 -
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] { // 只读[T]指针
// 省略部分代码
}
impl <T> *mut [T] { // 可变[T]指针
// 省略部分代码
}
对于裸指针,rust标准库包含了最基础的 * const T/* mut T
, 以及在* const T/*mut T
基础上特化的切片类型[T]的裸指针* const [T]/*mut [T]
。
在标准库针对两种基础类型指针实现了一些关联函数及方法。这里一定注意,所有针对 * const T
的方法在* const [T]
上都是适用的。
以上有几点值得注意:
- 可以针对原生类型实现方法(实现trait),这也是rust类型系统的强大扩展性,也是对函数式编程的强大支持;
- 针对泛型约束实现方法,我们可以大致认为
*const T/* mut T
实质是一种泛型约束,*const [T]/*mut [T]
是更进一步的约束,这使得rust可以具备更好的数据抽象能力,简化代码,复用模块。
裸指针的创建
1、从已经初始化的变量创建裸指针:
&T as *const T;
&mut T as * mut T;
2、用usize的数值创建裸指针:并使用了unsafe
{
let a: usize = 0xf000000000000000;
unsafe {a as * const i32};
}
在操作系统内核时需要直接将一个地址数值转换为某一类型的裸指针, 故而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
}
}
裸指针函数(不属于方法)
-
ptr::drop_in_place<T: ?Sized>(to_drop: *mut T)
此函数是编译器实现的,用于由程序代码人工释放所有权,而不是交由rust编译器处理。此函数会引发T内部成员的系列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
}
rust引用&T
的安全要求
- 引用的内存地址必须满足类型T的内存对齐要求
- 引用的内存内容必须是初始化过的
举例:
#[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
}
编译器出现如下警告
|
9 | let ref1 = &test.b;
| ^^^^^^^
|
= note: `#[warn(unaligned_references)]` on by default
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
= note: for more information, see issue #82523 <https://github.com/rust-lang/rust/issues/82523>
= note: fields of packed structs are not properly aligned, and creating a misaligned reference is undefined behavior (even if that reference is never dereferenced)
= help: copy the field contents to a local variable, or replace the reference with a raw pointer and use `read_unaligned`/`write_unaligned` (loads and stores via `*p` must be properly aligned even when using raw pointers)
针对未初始化变量相关的指针操作
MaybeUninit<T>
标准库代码分析
通常rust中对于变量的要求是必须初始化后才能使用,否则就会编译告警。但在程序中,总有内存还未初始化,却需要使用的情况:
- 从堆申请的内存块,这些内存块都是没有初始化的
- 需要定义一个新的泛型变量时,并且不合适用转移所有权进行赋值时
- 需要定义一个新的变量,但希望不初始化便能使用其引用时
- 定义一个数组,但必须在后继代码对数组成员初始化时
......
为了处理这种需要在代码中使用未初始化内存的情况,rust标准库定义了MaybeUninit<T>
MaybeUninit<T>
结构定义
#[repr(transparent)]
pub union MaybeUninit<T> {
uninit: (),
value: ManuallyDrop<T>,
}
说明:
属性repr(transparent)
实际上表示外部的封装结构在内存中等价于内部的变量,
MaybeUninit<T>
的内存布局就是ManuallyDrop<T>
的内存布局.
在后面的内容中可以看到
ManuallyDrop<T>
实际就是T的内存布局。故而MaybeUninit<T>
在内存中实质也就是T类型。
MaybeUninit<T>
容器来实现对未初始化变量的封装,以便在不引发编译错误完成对T类型未初始化变量的相关操作.
如果T类型的变量未初始化,那需要显式的提醒编译器不做T类型的drop操作,因为drop操作可能会对T类型内部的变量做连锁drop处理,从而引用未初始化的内容,造成未定义行为(undefined behavior)
。
而rust是用ManuallyDrop<T>
封装结构完成了对编译器的显式提示:对于用ManuallyDrop<T>
封装的变量,生命周期终止的时候编译器不会调用drop操作。
ManuallyDrop<T>
结构及方法
源代码如下:
#[repr(transparent)]
pub struct ManuallyDrop<T: ?Sized> {
value: T,
}
重点关注的一些方法:
-
ManuallyDrop<T>::new(val:T) -> ManuallyDrop<T>
此函数返回ManuallyDrop变量拥有传入的T类型变量所有权,并将此块内存直接用ManuallyDrop封装, 对于val,编译器不再主动做drop操作。
pub const fn new(value: T) -> ManuallyDrop<T> {
//所有权转移到结构体内部,value生命周期结束时不会引发drop
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>
的变量一定是在程序运行的某一时期不需要编译器drop,所以调用这个函数的时候一定要注意正确性。 -
ManuallyDrop<T>::deref(&self)-> & T
返回内部包装的变量的引用
fn deref(&self) -> &T {
//返回后,代码可以用&T对self.value做读操作,但不改变drop的规则
&self.value
}
-
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不会再发生
MaybeUninit<T>
创建方法
-
MaybeUninit<T>::uninit()->MaybeUninit<T>
可视为在栈空间上申请内存的方法,申请的内存大小是T类型的内存大小,该内存没有初始化。利用泛型和Union内存布局,rust巧妙的利用此函数在栈上申请一块未初始化内存。此函数非常非常非常值得关注
,在需要在栈空间定义一个未初始化泛型时,应第一时间想到MaybeUninit::<T>::uninit()
。
pub const fn uninit() -> MaybeUninit<T> {
//变量内存布局与T类型完全一致
MaybeUninit { uninit: () }
}
-
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) }
}
-
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
}
对未初始化的变量赋值的方法
- 将值写入
MaybeUninit<T>
: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() }
}
初始化后解封装的方法
用assume_init返回初始化后的变量并消费掉MaybeUninit<T>
变量,这是最标准的做法:
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)
}
}
assume_init_read是不消费self的情况下获得内部T变量,内部T变量的所有权已经转移到返回变量,后继要注意不能再次调用其他解封装函数。否则解封装后,会出现双份所有权,引发两次对同一变量的drop,导致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()
}
}
与上个函数比较类似的ManuallyDrop<T>::take
方法,用take函数将变量复制并获得变量的所有权。此时原变量仍然保留在ManuallyDrop中,后继不能再调用其他解封装函数,否则可能会出现UB。这里要特别注意理解take已经把变量的所有权转移到返回变量中。
pub unsafe fn take(slot: &mut ManuallyDrop<T>) -> T {
// 拷贝内部变量,并返回内部变量的所有权
// 返回后,原有的变量所有权已经消失,不能再用into_inner来返回
// 否则会UB
unsafe { ptr::read(&slot.value) }
}
-
MaybeUninit<T>::assume_init_drop(&self)
对于已经初始化过的MaybeUninit<T>, 如果所有权一直没有转移,则必须调用此函数以触发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() }
}
这里要注意区别数组类型和数组元素的初始化。对于数组[MaybeUninit<T>;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()
}
}
MaybeUnint<T>典型案列
对T类型变量申请内存及赋值:
use std::mem::MaybeUninit;
// 获得一个未初始化的i32引用类型内存
let mut x = MaybeUninit::<&i32>::uninit();
// 将&0写入变量,完成初始化
x.write(&0);
// 将初始化后的变量解封装供后继的代码使用。
let x = unsafe { x.assume_init() };
以上代码,编译器不会对x.write进行报警,这是MaybeUninit<T>
的最重要的应用,这个例子展示了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]);
下面例子说明一块内存被 MaybeUnint<T>
封装后,编译器将不再对其做释放,必须在代码中显式释放:
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()); }
}
上例中,在没有assume_init()调用的情况下,必须手工调用drop_in_place释放内存。
MaybeUninit<T>
是一个非常重要的类型结构,未初始化内存是编程中不可避免要遇到的情况,MaybeUninit<T>
也就是rust编程中必须熟练使用的一个类型。