本书github链接:inside-rust-std-library
前面章节参见:
深入RUST标准库内核(序言) - 简书 (jianshu.com)
深入RUST标准库内核(一 概述) - 简书 (jianshu.com)
RUST标准库内存相关模块代码分析
理解RUST程序的最关键点就是理解RUST内存相关的标准库代码。内存基本库代码给出了RUST最基本的一些规则如所有权转移,借用,生命周期的奥秘。
RUST内存相关主要包括:从内存角度看RUST类型,内存分配与释放,内存拷贝,置值,内存地址操作等。
RUST类型系统的内存布局
类型内存布局是指类型的内部变量在内存布局中,内存顺序,内存大小,内存字节对齐等内容。
对于GC机制的高级语言,类型内存布局一般是交由编译器决定的。程序员不需要关心。C/C++语言中类型只有固定的一种内存布局排序方式和一经配置即固定的对齐方式,编译器不会对此进行优化,程序员可预测类型内存布局。
RUST则不同,因为泛型,闭包,编译器优化的关系,类型内存布局方式编译器会根据需要对内存布局做调整,对程序员来说类型的内存布局是不可预测的,而在内存操作中,类型内存布局的一些信息是必须要使用的,所以,RUST提供了Layout
内存布局类型。此布局类型结构是类型内存操作的基础。
Layout
的数据结构如下:
pub struct Layout {
// size of the requested block of memory, measured in bytes.
// 类型需占用的内存大小,用字节数目表示
size_: usize,
// 按照此字节数目进行类型内存对齐, NonZeroUsize见代码后面文字分析
align_: NonZeroUsize,
}
NonZeroUsize
是一种非0值的usize, 这种类型主要应用于不可取0的值,本结构中, 字节对齐属性变量不能被置0,所以用NonZeroUsize
来确保安全性。如果用usize类型,那代码中就可能会把0置给align_,导致bug产生。这是RUST的一个设计规则,所有的限制要在类型定义即显性化,从而使bug在编译中就被发现。
每一个RUST的类型都有自身独特的内存布局Layout。一种类型的Layout可以用intrinsic::<T>::size_of()
及intrinsic::<T>::min_align_of()
获得的类型内存大小和对齐来获得。
RUST的内存布局更详细原理阐述请参考[RUST内存布局] (https://doc.rust-lang.org/nomicon/data.html),
#[repr(transparent)]
内存布局模式
repr(transparent)用于仅包含一个成员变量的类型,该类型的内存布局与成员变量类型的内存布局完全一致。类型仅仅具备编译阶段的意义,在运行时,类型变量与其成员变量可以认为是一个相同变量,可以相互无障碍类型转换。使用repr(transparent)布局的类型基本是一种封装结构。
#[repr(packed)]
内存布局模式
强制类型成员变量以1字节对齐,此种结构在协议分析和结构化二进制数据文件中经常使用
#[repr(RUST)]
内存布局模式
默认的布局方式,采用此种布局,RUST编译器会根据情况来自行优化内存
#[repr(C)]
内存布局模式
采用C语言布局方式, 所有结构变量按照声明的顺序在内存排列。默认4字节对齐。
RUST内存的类型与函数库体系
intrinsic 固有函数库——内存部分
intrinsics函数由编译器内置实现,并提供给其他模块使用,对于固有函数,没必要去关注如何实现,重要的是了解其功能和如何使用,intrinsics内存函数一般不由库以外的代码直接调用,而是由mem模块和ptr模块封装后再提供给其他模块。
intrinsics::drop_in_place<T:Sized?>(to_drop: * mut T)
在编译器无法自动drop时, 手工调用此函数将内存释放
intrinsics::forget<T:Sized?> (_:T)
, 通知编译器不回收forget的变量内存
intrinsics::needs_drop<T>()->bool
, 判断T类型是否需要做drop操作,实现了Copy Trait的类型会返回false
intrinsics::transmute<T,U>(e:T)->U
, 对于内存布局相同的类型 T和U, 完成将类型T变量转换为类型U变量
intrinsics::offset<T>(dst: *const T, offset: usize)->* const T
, 相当于C的类型指针加减计算
intrinsics::copy<T>(src:*const T, dst: *mut T, count:usize)
, 内存拷贝, src和dst内存可重叠, 类似c语言中的memmove
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实现
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
返回指针指向的变量对齐字节大小
intrinsics::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_offset_from<T>(ptr: *const T, base: *const T) -> isize
基于类型T内存布局的偏移量
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
ptr模块初探
ptr模块是RUST的对指针的实现模块。相比于C指针内存地址的简单,RUST指针实现机制复杂的多,以满足实现内存安全的类型系统需求,并兼顾内存使用效率和方便性。对内存的操作需要对ptr的若干概念先做一个理解,本节主要基于intrinsics模块的基础对ptr模块的一些类型结构及函数做出分析,为下节的内存类型和函数库做一个基础。
ptr模块中原生指针具体实现
RUST的原生指针类型(*const T/*mut T
)实质是个数据结构体,由两个部分组成,第一个部分是一个内存地址,第二个部分对这个内存地址的限制性描述-元数据
//从下面结构定义可以看到,*const 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 (), * const T编译器已经默认还带有元数据。
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 = ()>;
元数据的规则:
- 对于固定大小类型的指针(实现了
Sized
Trait), RUST定义为廋指针(thin pointer),元数据大小为0,类型为(),这里要注意,RUST中数组也是固定大小的类型,运行中对数组下标合法性的检测,就是比较是否已经越过了数组的内存大小。 - 对于动态大小类型的指针(DST 类型),RUST定义为胖指针(fat pointer 或 wide pointer), 元数据为:
- 对于结构类型,如果最后一个成员是动态大小类型(结构的其他成员不允许为动态大小类型),则元数据为此动态大小类型
的元数据 - 对于
str
类型, 元数据是按字节计算的长度值,元数据类型是usize - 对于切片类型,例如
[T]
类型,元数据是数组元素的数目值,元数据类型是usize - 对于trait对象,例如 dyn SomeTrait, 元数据是 [DynMetadata<Self>][DynMetadata](后面代码解释)
(例如:DynMetadata<dyn SomeTrait>)
随着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(..)
// If returned by `struct_tail_without_normalization` this is a unit struct
// without any fields, or not a struct, and therefore is Sized.
| ty::Adt(..)
// If returned by `struct_tail_without_normalization` this is the empty tuple,
// a.k.a. unit type, which is Sized
// 如果是固定类型,元数据是单元类型 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> {
//堆中的函数VTTable变量的指针
vtable_ptr: &'static VTable,
//标示结构对Dyn的所有权关系
phantom: crate::marker::PhantomData<Dyn>,
}
struct VTable {
drop_in_place: fn(*mut ()),
size_of: usize,
align_of: usize,
}
PhantomData
的含义英文如下:
Zero-sized type used to mark things that "act like" they own a T
.
一个零占用的变量,使得结构拥有了一个T类型的变量。在RUST中,这一用法常常为了表示生命周期关系,以作为安全性的一个判断。
VTable 中包含4个成员,上面的结构体仅列出了前三个,即指向实现Trait的结构的drop_in_place函数的指针; 结构内存占用字节大小;结构内存对齐字节大小;VTable结构后面的内存为Trait的所有行为的函数指针数组。
ptr模块函数
ptr::drop_in_place<T: ?Sized>(to_drop: *mut T)
此函数是编译器实现的,用于不需要RUST自动drop时,由程序代码调用以释放内存
ptr::metadata<T: ?Sized>(ptr: *const T) -> <T as Pointee>::Metadata
用来返回原生指针的元数据
ptr::null<T>() -> *const T
返回0值的*const T
,因为RUST安全代码中指针不能为0,所以只能用这个函数获得0值的* const T,这个函数也是RUST安全性的一个体现。
ptr::null_mut<T>()->*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]
由原生指针类型及切片长度获得原生切片类型指针
ptr模块的函数大部分逻辑都比较简单。很多就是对intrinsic 函数做调用。*const T/* mut T
被使用的场景如下:
1.需要做内存布局相同的两个类型之间的转换,
2.对于数组或切片做头指针偏移以获取元素变量
3.由内存头指针生成数组或切片指针
4.内存拷贝或内存读出/写入
以上4个场景实际上都是编程中最基础的操作。
由* const T
生成*const [T]
的函数代码如下:
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 {
// SAFETY: Accessing the value from the `PtrRepr` union is safe since *const T
// and PtrComponents<T> have the same memory layouts. Only std can make this
// guarantee.
//由以下这个操作可以确认 * const T实质是个结构体。
unsafe { PtrRepr { components: PtrComponents { data_address, metadata } }.const_ptr }
}
*const T/*mut T/*const [T]/*mut [T]
若干方法
ptr::*const T::is_null(self)->bool
ptr::*mut T::is_null(self)->bool此
函数判断原生指针的地址值是否为0
ptr::*const T::cast<U>(self) -> *const U
,本质上就是一个*const T as *const U
。
ptr::*mut T::cast<U>(self)->*mut U
cast函数主要完成不同类型的原生指针的互相转换,这里需要程序员确保U与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::as_ref<`a>(self) -> Option<&`a T>
将原生指针转换为引用,因为*const T可能为零,所有需要转换为Option<& `a T>
类型,转换的安全性由程序员保证,尤其注意满足RUST对引用的安全要求。转换后,数据进入安全的RUST环境。
ptr::*mut T::as_ref<`a>(self)->Option<&`a T>
ptr::*mut T::as_mut<`a>(self)->Option<&`a mut T>
同上,但转化类型为 &mut T。
ptr::*const T::offset(self, count:isize)->* const T
*mut T::offset(self, count:isize)->* mut T
实质是intrinsics::offset的封装
ptr::*const [T]::len()->usize
获取切片元素数量
*const T
及*mut T
的方法的逻辑基本也都比较简单,但涉及到较多的指针类型转换,有时需要细致分析,举例如下:
//该方法给* mut T置一个新值
pub fn set_ptr_value(mut self, val: *const u8) -> Self {
// 指针类型分析如下
// self: * mut T
// &mut self:&mut *mut T
// &mut self as *mut *const T: *mut *mut T as *mut *const T
// &mut self as *mut *const T as *mut *const u8: *mut *const T as * mut *const u8
let thin = &mut self as *mut *const T as *mut *const u8;
// 指针类型分析如下
//*thin: *(*mut *const u8)即mut *const u8
unsafe { *thin = val };
self
}
RUST引用&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
}