一篇9月4日发布的技术博客,把Rust的dyn Trait内存布局拆得明明白白。作者是个C++背景的Rust新手,用transmute把一个胖指针硬生生掰成两个数字,肉眼可见"数据指针+vtable指针"的结构,还顺手发现Rust里一个空结构体的size_of居然是0——这在C++里是违规的,标准规定每个对象至少占1字节。文章写得清楚,读着也爽。
问题是,那种"这就是Rust真相"的确定语气,恰恰是Rust官方文档反复叫人别用的语气。文中拆出来的很多"事实",官方自己标注的身份是"当前实现,不作保证"。
胖指针里到底装了什么
C++的虚函数表指针长在对象身体里,对象一出生就带着它。Rust的dyn Trait不这样干:一个&dyn Draw是两个指针拼起来的胖指针,一个指向数据,一个指向vtable,类型信息是调用时才现场配对的。这也是为什么同一个Duck同时实现Fly和Swim,两个trait对象共享同一个数据指针,却各自挂着不同的vtable。
这个结构本身没问题,原文讲得也直观。真正被跳过的一步,是官方文档就在同一份Reference里,紧跟着这套模型写了一句话:胖指针恰好是两个usize大小,只是当前实现,不是语言承诺。DynMetadata文档说得更直白——vtable的字段排布是"概念性"的,不构成稳定ABI。
官方标了警戒线,笔记却没抄
这不是吹毛求疵。Rust核心库源码里还有一条更扎眼的警告:vtable指针的相等性不能拿来判断具体类型身份。同一个实现的vtable可能因为codegen unit被复制出好几份,内容相同的不同vtable也可能被编译器去重合并成一份。也就是说,你今天写代码比较两个vtable指针是否相等,来判断"这是不是同一个类型",这个判断在不同编译版本、不同优化级别下都可能翻车。
原文另一个被指出的问题,是把C++的CRTP直接等同于Rust的单态化。普通C++模板本身就按具体类型实例化,CRTP只是利用模板玩出的一种静态多态写法,两者不是一回事。作者把ZST身份问题归因于"Rust靠所有权而非地址追踪身份",也把两个不相干的概念焊在了一起——所有权管的是谁能用这个值,地址身份管的是这个值住在哪儿,更严谨的说法应该是"零尺寸值的引用不保证有不同地址",跟所有权没有必然关系。
至于文中反复用来做实验的transmute,把胖指针硬拆成(usize, usize)观察——这本身需要unsafe,恰恰说明编译器自己都不担保这份布局对下一个版本还成立。这是实现观察,不是语言语义。文章配套的代码仓库目前访问已经404,搜索也没找到任何人对这些实验做过公开审阅或勘误,这些"意外发现"目前只是一个人的探索记录,没有经过社区校验。
- 风险.把rustc当前行为当成语言规则内化,编译器换版本、换优化级别,基于错误心智模型写的代码可能悄悄变脆弱。
dyn兼容(原称object safety)的规则也比原文暗示的更细。原文只提了"方法不能返回Self""方法不能有泛型参数"两条,但官方规则里还留了where Self: Sized这类例外——加上这个约束的方法,即便签名看起来违规,也可以被排除在vtable之外,让整个trait依然可以做成trait object。这类细节教程里经常被简化掉,却恰恰是新手踩坑最多的地方。
眼见为实,未必为真。
古人讲"名不正则言不顺",技术文档里也是一样:一份把"实现细节"和"语言保证"混着讲的教程,读起来流畅,用起来危险。这篇笔记的价值不小——它把trait object在内存里的样子讲得比大多数入门材料都直观,ZST那一段的"WHAT???"式惊讶也是真实的学习反应,值得肯定。但它缺了一道免责声明:哪些是Rust承诺给你的,哪些只是rustc今天心情好。
- 结论.遇到用
transmute、unsafe、"我做了个实验发现"这类表述的教程,先把它当成观察记录,别当成语言规范去背。真正想确认布局稳不稳,去翻Reference和源码里那句"不保证",比信任任何一篇个人博客都靠得住。
