首页 / 资讯中心 / 文章详情

Rust 胖指针元数据操作 API 设计解读:基于 RFC 2580 的 Pointee、DynMetadata 与 from_raw_parts

Rust 胖指针元数据操作 API 设计解读:基于 RFC 2580 的 Pointee、DynMetadata 与 from_raw_parts ★ FEATURED ARTICLE
文档【免费下载链接】rfcsRFCs for changes to Rust项目地址https://gitcode.com/gh_mirrors/rf/rfcs点击查看免费下载本篇技术指南以 Rust RFC 仓库中的 RFC 2580《ptr-meta》 为骨架系统讲解 Rust 如何为胖指针fat pointer提供一套稳定的通用元数据操作 API命名元数据类型、从指针中提取元数据、用数据指针与元数据重建指针以及将 trait 对象的 vtable 表示为一个带有限 API 的具体类型。阅读本文后你将理解Pointee关联类型、ptr::metadata、DynMetadata、ptr::from_raw_parts的完整设计动机与用法并能在unsafe代码如自定义集合库、自定义智能指针中自由地拆装dyn Trait、切片、str等 DST 指针。本 RFC 对应的实现跟踪问题为 rust#81513其核心 API 后续已在标准库core::ptr/std::ptr中落地。背景胖指针由数据指针 元数据构成在 Rust 中指向动态大小类型DSTdynamically-sized type的指针或引用并非一个裸地址而是由两部分组成数据指针data pointer存放值的内存地址元数据metadata描述该值形状的附加信息例如切片的长度、str的字节长度、trait 对象的 vtable 指针。RFC 2580 在其Pointeetrait 的文档注释中归纳了各类 DST 的元数据规则对实现了Sized的静态大小类型以及extern类型指针是瘦的thin元数据零大小类型为()对最后一个字段是 DST 的结构体元数据即该末字段的元数据对str类型元数据是字节长度usize对切片类型如[T]元数据是元素个数usize对 trait 对象如dyn SomeTrait元数据是DynMetadatadyn SomeTrait即指向 vtable 的指针。普通高层代码无需关心这些细节——写一个Foo它就那样工作无论Foo是否为 DST。但 unsafe 代码例如自定义集合库常常需要分别访问胖指针的两个组成部分。历史遗留std::raw的教训RFC 2580 回顾了 Rust 1.11 的一次标准库清理当时从std移除了 [std::raw::Repr] trait 和 [std::raw::Slice] 类型。Slice因为内存布局与[U]/mut [U]完全一致可以被transmute成切片引用但这类 API 极其不安全。取而代之的是更具体、危险程度更可控的std::slice::from_raw_parts/from_raw_parts_mut配合as_ptr与len方法分别提取胖指针的各个组成部分。但对于 trait 对象当时仍只有一个不稳定的std::raw::TraitObject且只能配合transmute使用#[repr(C)] pub struct TraitObject { pub data: *mut (), pub vtable: *mut (), }也就是说稳定版 Rust 已经能可靠地操作切片却仍然无法安全、可靠地拆装 trait 对象指针。RFC 2580 将这一缺口称为现有 Rust 类型操作 API 中的一个洞a hole in available APIs并指出即便把TraitObject稳定下来也不合适——它鼓励危险的transmute调用。动机unstable 与 unsafe 之外的第三条路RFC 指出现状是需要该功能的代码例如动机一节所链接的自定义库把多个大小不一的 trait 对象连同各自的 vtable 指针连续存放在一块内存中迭代时再根据数据指针和 vtable 指针重建胖指针必须transmute到std::raw::TraitObject或它在 Stable Rust 中的本地复制品在需要按具体类型对齐来构造数据指针的场景甚至要捏造一个悬垂指针如0x8000_0000_usize as *mut ()。而std::mem::align_of(*ptr)其中ptr: *const dyn SomeTrait配合悬垂数据指针是否构成未定义行为当时并不明确。此外Thintrait 别名见下文还能把一批因无法在泛型代码中表达指针瘦性而被迫限制为Sized的标准库 API 扩展到extern类型上。extern类型是 RFC 1861 提出的概念——用extern { type Foo; }声明一种对 Rust 类型系统不透明的类型它是 DST不实现Sized而指向它的指针元数据恰好是()因此是瘦指针。提案摘要SummaryRFC 2580 提议新增一组通用 API用于操作胖指针的元数据命名元数据的类型作为关联类型从指针中提取元数据用数据指针 元数据重建指针将 trait 对象的元数据——vtable——表示为一个带有限 API 的具体类型。一个重要的边界声明是本 RFC 不提出定义自定义动态大小类型的机制但设计上力求与未来这类提案custom DSTs兼容。Guide-level 实战实现一个ThinBoxRFC 用一段完整的实战代码演示了这套 API 的价值。目标构建一个类似Boxdyn Trait、但把 vtable 指针存在堆上值旁边的泛型类型使指针本身是瘦的thin。先看样板代码use std::marker::{PhantomData, Unsize}; use std::ptr::{self, DynMetadata}; trait DynTraitDyn PointeeMetadataDynMetadataDyn; pub struct ThinBoxDyn: ?Sized DynTraitDyn { ptr: ptr::NonNullWithMeta(), phantom: PhantomDataDyn, } #[repr(C)] struct WithMetaT: ?Sized { vtable: DynMetadata, value: T, }关键点DynTraitDyn是一个约束指针元数据恰好是DynMetadataDyn的 trait 别名RFC 写作时尚无 trait 别名语法若提前实现则用其展开定义代替WithMetaT用#[repr(C)]把 vtable 与值打包到连续内存NonNullWithMeta()记录了整块内存的首地址。由于 unsized rvalues局部变量/参数/表达式支持无大小类型通过变长 alloca 实现当时尚未实现构造函数需要从实现该 trait 的具体类型非大小化unsize而来。Unsize约束保证可以从S强转成Dyntrait 对象并构造出相应元数据。让Box负责内存布局计算与分配implDyn: ?Sized DynTrait ThinBoxDyn { pub fn new_unsizeS(value: S) - Self where S: UnsizeDyn { let vtable ptr::metadata(value as Dyn); let ptr NonNull::from(Box::leak(Box::new(WithMeta { vtable, value }))).cast(); ThinBox { ptr, phantom: PhantomData } } }RFC 还提到另一种候选构造器pub fn new_copy(value: Dyn) where Dyn: Copy只是需要稍多代码。访问值需要知道其对齐——vtable 恰好提供了这些信息implDyn: ?Sized DynTrait ThinBoxDyn { fn data_ptr(self) - *mut () { unsafe { let offset std::mem::size_of::DynMetadataDyn(); let value_align self.ptr.as_ref().vtable.align(); let offset align_up_to(offset, value_align); (self.ptr.as_ptr() as *mut u8).add(offset) as *mut () } } } /// https://github.com/rust-lang/rust/blob/1.30.0/src/libcore/alloc.rs#L199-L219 fn align_up_to(offset: usize, align: usize) - usize { offset.wrapping_add(align).wrapping_sub(1) !align.wrapping_sub(1) } // Similarly Deref implDyn: ?Sized DynTrait DerefMut for ThinBoxDyn { fn deref_mut(mut self) - mut Dyn { unsafe { mut **mut Dyn::from_raw_parts(self.data_ptr(), *self.ptr.as_ref().vtable) } } }这里正是新 API 的核心价值DynMetadata::align()从 vtable 读出对齐值align_up_to按对齐把数据指针推进到值区起始处该辅助函数参考了 Rust 1.30.0 时期libcore/alloc.rs中Layout的对齐逻辑随后用from_raw_parts把数据指针 元数据重新拼装成完整的*mut Dyn胖指针再解引用为mut Dyn。最后是Drop此时原始的Sized类型S不再静态可知无法再次借助Box需要手工布局与释放implDyn: ?Sized DynTrait Drop for ThinBoxDyn { fn drop(mut self) { unsafe { let layout /* left as an exercise for the reader */; ptr::drop_in_place::Dyn(mut **self); alloc::dealloc(self.ptr.cast(), layout); } } }Reference-level五件套 API 详解RFC 将以下 API 加入core::ptr并在std::ptr中重导出Pointeetrait——为所有类型自动实现类似Sized与Unsize的自动实现机制Thintrait 别名若本 RFC 先于 trait 别名落地则以展开定义代替metadata自由函数DynMetadata结构体为*const T、*mut T、NonNullT各添加一个from_raw_parts构造器。Pointee元数据类型的命名通道/// This trait is automatically implemented for every type. /// /// Raw pointer types and reference types in Rust can be thought of as made of two parts: /// a data pointer that contains the memory address of the value, and some metadata. /// /// For statically-sized types (that implement the Sized traits) /// as well as for extern types, /// pointers are said to be “thin”: metadata is zero-sized and its type is (). /// /// Pointers to [dynamically-sized types][dst] are said to be “fat” /// and have non-zero-sized metadata: /// /// * For structs whose last field is a DST, metadata is the metadata for the last field /// * For the str type, metadata is the length in bytes as usize /// * For slice types like [T], metadata is the length in items as usize /// * For trait objects like dyn SomeTrait, metadata is [DynMetadataSelf][DynMetadata] /// (e.g. DynMetadatadyn SomeTrait). /// /// In the future, the Rust language may gain new kinds of types /// that have different pointer metadata. /// /// Pointer metadata can be extracted from a pointer or reference with the [metadata] function. /// The data pointer can be extracted by casting a (fat) pointer /// to a (thin) pointer to a Sized type with the as operator, /// for example (x: dyn SomeTrait) as *const SomeTrait as *const () /// or (x: *const dyn SomeTrait).cast::()(). /// /// [dst]: https://doc.rust-lang.org/nomicon/exotic-sizes.html#dynamically-sized-types-dsts #[lang pointee] pub trait Pointee { /// The type for metadata in pointers and references to Self. type Metadata: Copy Send Sync Ord Hash Unpin; }设计要点Pointee被标记为#[lang pointee]即由编译器提供语言级支撑所有类型都实现它。泛型代码即使没有显式写T: Pointee约束也可以依赖这一点——RFC 以Anytrait 作类比因为存在implT: static Any for T所以只要T: static就能用Any而无需显式T: Any约束区别在于Pointee不要求static关联类型Metadata带有Copy Send Sync Ord Hash Unpin的完备约束确保元数据值可以被复制、跨线程传递、排序与哈希将元数据类型抽象为关联类型而非硬编码为未来可能出现的新型 DST 预留了空间。Thin表达指针瘦性的 trait 别名/// Pointers to types implementing this trait alias are “thin”: /// /// rust /// fn this_never_panicsT: std::ptr::Thin() { /// assert_eq!(std::mem::size_of::T(), std::mem::size_of::usize()) /// } /// pub trait Thin PointeeMetadata();凡是Metadata ()的类型其指针与usize等宽——包括Sized类型与extern类型。RFC 特别指出Thin的价值在于此前泛型代码中无法表达指针瘦性导致一批 API 被无谓地限制为T: Sized。metadata提取元数据/// Extract the metadata component of a pointer. /// /// Values of type *mut T, T, or mut T can be passed directly to this function /// as they implicitly coerce to *const T. /// For example: /// /// /// assert_eq(std::ptr::metadata(foo), 3_usize); /// /// /// Note that the data component of a (fat) pointer can be extracted by casting /// to a (thin) pointer to any Sized type: /// /// /// # trait SomeTrait {} /// # fn example(something: SomeTrait) { /// let object: SomeTrait something; /// let data_ptr object as *const SomeTrait as *const (); /// # } /// pub fn metadataT: ?Sized(ptr: *const T) - T as Pointee::Metadata {…}用法说明*mut T、T、mut T都能隐式强转成*const T后直接传入对str这类瘦语义之外的对象返回的是长度foo的元数据为3_usize数据部分则靠把胖指针as成指向Sized类型的瘦指针来提取例如object as *const SomeTrait as *const ()。from_raw_parts重建胖指针implT: ?Sized *const T { pub fn from_raw_parts(data: *const (), meta: T as Pointee::Metadata) - Self {…} } implT: ?Sized *mut T { pub fn from_raw_parts(data: *mut (), meta: T as Pointee::Metadata) - Self {…} } implT: ?Sized NonNullT { pub fn from_raw_parts(data: NonNull(), meta: T as Pointee::Metadata) - Self { unsafe { NonNull::new_unchecked(*mut _::from_raw_parts(data.as_ptr(), meta)) } } }三个重载分别覆盖只读指针、可变指针与NonNull后者用new_unchecked从*mut版本转换语义上保持非空不变式。DynMetadata可编程的 vtable/// The metadata for a DynTrait dyn SomeTrait trait object type. /// /// It is a pointer to a vtable (virtual call table) /// that represents all the necessary information /// to manipulate the concrete type stored inside a trait object. /// The vtable notably it contains: /// /// * type size /// * type alignment /// * a pointer to the type’s drop_in_place impl (may be a no-op for plain-old-data) /// * pointers to all the methods for the type’s implementation of the trait /// /// Note that the first three are special because they’re necessary to allocate, drop, /// and deallocate any trait object. /// /// It is possible to name this struct with a type parameter that is not a dyn trait object /// (for example DynMetadatau64) but not to obtain a meaningful value of that struct. #[derive(Copy, Clone)] pub struct DynMetadataDynTrait: ?Sized { // Private fields vtable_ptr: ptr::NonNull(), phantom: PhantomDataDynTrait } implDynTrait: ?Sized DynMetadataDynTrait { /// Returns the size of the type associated with this vtable. pub fn size(self) - usize { ... } /// Returns the alignment of the type associated with this vtable. pub fn align(self) - usize { ... } /// Returns the size and alignment together as a Layout pub fn layout(self) - alloc::Layout { unsafe { alloc::Layout::from_size_align_unchecked(self.size(), self.align()) } } }要点vtable 中除了 trait 方法的函数指针还固定包含三个特殊条目类型大小、类型对齐、drop_in_place实现指针对 plain-old-data 可能是空操作——前三个对任何 trait 对象的分配、析构与释放都是必需的DynMetadata是Copy Clone的字段私有vtable_ptr: NonNull()PhantomDataDynTrait对外仅暴露size/align/layout三个只读方法避免像TraitObject那样允许直接摆弄裸 vtable 指针可以用非 trait 对象类型参数命名它如DynMetadatau64但无法获得该类型的有意义值——这是一种可命名但不可构造的类型层面的占位。对既有 API 边界的放宽RFC 同时调整了同模块内一批函数的泛型约束从隐式的T: Sized放宽为T: ?Sized Thinnull()与null_mut()的边界NonNull::dangling构造器NonNull::cast方法的U类型参数。这使上述函数可以用于extern类型。另外就as运算符的指针强转而言指向T的指针在T: Thin而非T: Sized时即被视为瘦指针同样涵盖 extern 类型。std::raw::TraitObject与整个std::raw模块被标记为弃用并最终移除。设计取舍与备选方案RFC 在 Rationale 一节明确对比了备选路径维持现状意味着只能transmute到std::raw::TraitObject或其稳定复制品且构造数据指针时需要知道具体类型的对齐必要时得捏造悬垂指针而align_of(*ptr)配合悬垂指针是否 UB 并不明确——这构成了不可接受的不确定性。**前代方案RFC PR #2579**曾提议一个只对dyn SomeTrait这类 trait 对象实现的DynTraittrait没有Metadata关联类型DynMetadata被硬编码进 trait。本 RFC 相比之下更通用也有望与未来的自定义 DST 提案更兼容同时它回答了一个超胖指针问题——如果将来 trait 对象携带多个 vtable 指针可以选用不同的元数据类型例如(DynMetadatadyn Trait, DynMetadatadyn OtherTrait)。先例参考Prior art此前的 Custom Dynamically-Sized TypesCDSTRFC #1524 被推迟Internals 论坛线程 #6663Lets fix DSTs采用了同样的思路且野心更大、更通用除DynMetadata的方法外本 RFC 基本是该线程方案的子集。遗留问题Unresolved questionsRFC 明确列出了尚未敲定的设计问题反映其在当时的开放性Pointee的命名Internals 线程 #6663 曾用ReferentDynMetadata的位置——除了std::ptr是否有更合适的模块DynMetadata是否应该不带类型参数不带可降低单态化成本但会强制每个 vtable 中 size/alignment/destruction 指针位于相同偏移——不过保持同偏移本就有利于减小代码体积Thin的命名——它暗示T本身是瘦的而实际语义是指向T的指针是瘦的Thin的位置——放在std::marker是否更好是否把Thin设为Sized的 supertrait或者说未来是否可能出现指向静态大小类型的胖指针是否还有其它标准库泛型 API如ptr::null()存在不必要的T: Sized边界而排除了 extern 类型*mut _::from_raw_parts及其同类是否应为unsafe fnAPI 形态自由函数 vs*mut _/*const _上的方法/构造器是否增加into_raw_parts返回(*const (), T::Metadata)用cast到Sized类型来提取地址作为瘦指针的方式可发现性较差也许可以取代metadata函数。后续演进与仓库中其它 RFC 的衔接RFC 2580 并非孤立提案它与 Rust RFC 仓库中的多个相关提案构成一条完整的技术脉络RFC 1861extern typesextern 类型是 DST其指针元数据为()指针与usize等宽RFC 2580 中Thin边界放宽的null()/NonNull::dangling/cast正是为了让这些 API 服务 extern 类型RFC 1909unsized rvalues让局部变量、函数参数和部分表达式支持 unsized 类型实现为变长 allocaRFC 2580 的ThinBox示例依赖该特性尚未实现的前提采用Unsize构造路径RFC 3519arbitrary self types v2后续的自定义智能指针提案在处理Receiver/Deref目标、以及把自定义指针类型用于dyn Trait强转时同样依赖指针元数据与Pointee概念例如其中讨论的外语引用抽象type Pointee;可见指针由数据与元数据两部分组成已成为 Rust 类型系统后续演化的公共地基。在仓库中进一步阅读本 RFC 全文text/2580-ptr-meta.md相关实现跟踪rust#81513对应 Rust 仓库的ptr_metadata特性RFC 撰写模板0000-template.md本文档遵循其 Summary / Motivation / Guide-level explanation / Reference-level explanation / Rationale / Prior art / Unresolved questions 的标准结构RFC 流程总览README.md介绍 RFC 从 PR 到 merge 再到实现的完整生命周期配套 DST 相关提案text/1861-extern-types.md、text/1909-unsized-rvalues.md综上所述RFC 2580 为 Rust 的胖指针操作提供了命名元数据类型、提取元数据、重建指针、程序化访问 vtable四件套能力取代了std::raw::TraitObject这类依赖transmute的危险 API并为 extern 类型、未来的自定义 DST 与自定义智能指针铺平了道路。对于需要深入unsafe层操作 trait 对象与 DST 的库作者而言这套 API 是继切片from_raw_parts之后填补可自由且可靠地拆装所有胖指针这一空白的基石。赞分享文档【免费下载链接】rfcsRFCs for changes to Rust项目地址https://gitcode.com/gh_mirrors/rf/rfcs点击查看免费下载相关推荐Rust RFC 1135 深度解读为胖裸指针实现相等比较、拒绝排序比较Rust RFC 1135 深度解读为胖裸指针实现相等比较、拒绝排序比较 导读 本文围绕 Rust 官方 RFC 仓库中的 1135 raw pointer文档Rust RFC 3192 深度解读为 trait 对象设计基于类型的泛型数据访问provide_any / DemandRust RFC 3192 深度解读为 trait 对象设计基于类型的泛型数据访问provide_any / Demand 导读 本文围绕仓库中的 RFC文档Apache OpenDAL 基于查询的元数据缓存设计RFC-1398 深度解析Apache OpenDAL 基于查询的元数据缓存设计RFC 1398 深度解析 导读本文以 Apache OpenDAL 的 RFC 1398 quer数据存储后端上一篇CodeCombat游戏化编程学习平台如何让编程学习像玩游戏一样上瘾下一篇Interceptor键盘驱动封装库深度解析实现原理与性能优化指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站