作者:张汉东 / 审校:CrLF0710

记录 Trait Upcasting系列 系列 PR 过程。

PR 系列:

  1. Refactor vtable codegen #86291
  2. Change vtable memory representation to use tcx allocated allocations.#86475
  3. Refactor vtable format for upcoming trait_upcasting feature. #86461
  4. Trait upcasting (part1) #86264
  5. Trait upcasting (part2)

本文为 第二个 PR 的描述。


在第一个 PR 发出之后,收到了官方成员(Member)的一些 review 意见。其中之一就是促进第二个 PR 的原因,被记录于 issues #86324

第二个 PR 的目标是在#86291 (comment) 中描述:

第一步是重构 miri 中的 vtable 生成,以在Machine上下文之外创建一个Allocation

cg_{clif,ssa}中的 vtable 代码生成器的地方可以调用此函数,然后再调用任何用于降级到后端常量分配的方法。

trait + type -> allocation 的映射添加到 tcx.alloc_map 或类似的东西来替换后端内部实现也不错。

一句话描述:修改miri和两套codegen 以便让它使用tcx中构建的用allocation表示的 vtable。

编译器内部概念说明

tcx 是指类型上下文,是由编译器内部 rustc_middle::ty模块定义的,它是编译器内部核心数据结构。

Rust 的类型在编译器内部,由 Ty表示。当我们说Ty的时候,是指rustc_middle::ty::Ty,而不是指rustc_hir::Ty,了解它们之间的区别是比较重要的。

rustc_hir::Ty vs ty::Ty

rustc_hir::Ty表示脱糖以后的类型,而ty::Ty 代表了类型的语义。

例如,fn foo(x: u32) → u32 { x } 这个函数中,u32 出现两次。从 HIR 的角度看,这是两个不同的类型实例,因为它们出现在程序中不同的地方,也就是说,它们有两个不同的 Span (位置)。但是对于 ty::Ty来说,u32 在整个程序中都是同一个类型,它代表的不是具体的类型实例。

除此之外,HIR 还会有更多的信息丢失。例如, fn foo(x: &u32) -> &u32,在 HIR 看来,它不需要 lifetime 信息,所以 &u32 是不完整的。但是对于 ty::Ty 来说,它是完整的包含了 lifetime 信息。

一个简单总结:

rustc_hir::Ty ty::Ty
描述类型的「语法」 描述类型的「语义」
每一个 rustc_hir::Ty
都有自己的 Span
整个程序而言都是同一个类型,并不特指某个类型实例
rustc_hir::Ty
有泛型和生命周期; 但是,其中一些生命周期是特殊标记,例如 LifetimeName::Implicit
ty::Ty
具有完整的类型,包括泛型和生命周期,即使用户将它们排除在外

HIR 是从 AST 中构建的,它产生在 ty::Ty 之前。在 HIR 构建之后,一些基本的类型推导和类型检查就完成了。ty::Ty就是被用于类型检查,并且确保所有的东西都有预期的类型。 rustc_typeck::astconv 模块负责将 rustc_hir::Ty转换为ty::TY

ty::Ty 实现

rustc_middle::ty::Ty实际上是&TyS的一个类型别名。&TySType Structure的简称。一般情况下,总是会通过 ty::Ty 这个类型别名来使用 &TyS

要分配一个新的类型,你可以使用tcx上定义的各种mk_方法。这些方法的名称主要与各种类型相对应。例如:

  1. let array_ty = tcx.mk_array(elem_ty, len * 2); // 返回 Ty<'tcx>

你也可以通过访问tcx的字段来找到tcx本身的各种常见类型:tcx.types.booltcx.types.char,等等。

修改文件概述

本次修改涉及 21 个文件。

  1. compiler/rustc_codegen_cranelift/src/common.rs
  2. compiler/rustc_codegen_cranelift/src/constant.rs
  3. compiler/rustc_codegen_cranelift/src/lib.rs
  4. compiler/rustc_codegen_cranelift/src/unsize.rs
  5. compiler/rustc_codegen_cranelift/src/vtable.rs
  6. compiler/rustc_codegen_llvm/src/common.rs
  7. compiler/rustc_codegen_ssa/src/meth.rs
  8. compiler/rustc_codegen_ssa/src/traits/consts.rs
  9. compiler/rustc_middle/src/ty/context.rs
  10. compiler/rustc_middle/src/ty/mod.rs
  11. compiler/rustc_middle/src/ty/vtable.rs
  12. compiler/rustc_mir/src/interpret/eval_context.rs
  13. compiler/rustc_mir/src/interpret/intern.rs
  14. compiler/rustc_mir/src/interpret/memory.rs
  15. compiler/rustc_mir/src/interpret/terminator.rs
  16. compiler/rustc_mir/src/interpret/traits.rs
  17. src/test/ui/consts/const-eval/ub-upvars.32bit.stderr
  18. src/test/ui/consts/const-eval/ub-upvars.64bit.stderr
  19. src/test/ui/consts/issue-79690.64bit.stderr
  20. src/test/ui/consts/miri_unleashed/mutable_references_err.32bit.stderr
  21. src/test/ui/consts/miri_unleashed/mutable_references_err.64bit.stderr

修改主要涉及 五个组件:

  1. rustc_middle,属于 rust 编译器的 main crate ,包含rustc“家族”中的其他crate使用的通用类型定义,包括 HIR/MIR/Types。
  2. rustc_codegen_ssa,截至2021年1月,RustC_Codegen_SSA 为所有后端提供了一个抽象的接口,以允许其他Codegen后端(例如Cranelift)。
  3. rustc_mir,用于操作 MIR 的库。
  4. rustc_codegen_cranelift,是 基于 cranelift 的编译器后端,专门用于 debug 模式
  5. rustc_codegen_llvm,是 基于 llvm 的编译器后端,专门用于 release 模式

rustc_middle 库中的修改

  1. 首先新增 src/ty/vtable.rs 模块,将 vtable 的内存分配移动到 rustc_middle,以达到通用的目的。
  2. src/ty/mod.rs 中将 vtable 模块导入
  3. src/ty/context.rs 中增加 vtables_cache

**src/ty/vtable.rs** 模块

  1. use std::convert::TryFrom;
  2. use crate::mir::interpret::{alloc_range, AllocId, Allocation, Pointer, Scalar};
  3. use crate::ty::fold::TypeFoldable;
  4. use crate::ty::{self, DefId, SubstsRef, Ty, TyCtxt}; // 导入 `ty`模块中相关类型
  5. use rustc_ast::Mutability;
  6. #[derive(Clone, Copy, Debug, PartialEq, HashStable)]
  7. pub enum VtblEntry<'tcx> {
  8. MetadataDropInPlace,
  9. MetadataSize,
  10. MetadataAlign,
  11. Vacant,
  12. Method(DefId, SubstsRef<'tcx>),
  13. }
  14. pub const COMMON_VTABLE_ENTRIES: &[VtblEntry<'_>] =
  15. &[VtblEntry::MetadataDropInPlace, VtblEntry::MetadataSize, VtblEntry::MetadataAlign];
  16. pub const COMMON_VTABLE_ENTRIES_DROPINPLACE: usize = 0;
  17. pub const COMMON_VTABLE_ENTRIES_SIZE: usize = 1;
  18. pub const COMMON_VTABLE_ENTRIES_ALIGN: usize = 2;
  19. impl<'tcx> TyCtxt<'tcx> {
  20. // 给 vtable 分配内存,`TyCtxt` 中包含一个缓存,所以必须删除其重复数据
  21. /// Retrieves an allocation that represents the contents of a vtable.
  22. /// There's a cache within `TyCtxt` so it will be deduplicated.
  23. pub fn vtable_allocation(
  24. self,
  25. ty: Ty<'tcx>,
  26. poly_trait_ref: Option<ty::PolyExistentialTraitRef<'tcx>>,
  27. ) -> AllocId {
  28. let tcx = self;
  29. let vtables_cache = tcx.vtables_cache.lock();
  30. if let Some(alloc_id) = vtables_cache.get(&(ty, poly_trait_ref)).cloned() {
  31. return alloc_id;
  32. }
  33. drop(vtables_cache);
  34. // See https://github.com/rust-lang/rust/pull/86475#discussion_r655162674
  35. assert!(
  36. !ty.needs_subst() && !poly_trait_ref.map_or(false, |trait_ref| trait_ref.needs_subst())
  37. );
  38. let param_env = ty::ParamEnv::reveal_all();
  39. let vtable_entries = if let Some(poly_trait_ref) = poly_trait_ref {
  40. let trait_ref = poly_trait_ref.with_self_ty(tcx, ty);
  41. let trait_ref = tcx.erase_regions(trait_ref);
  42. tcx.vtable_entries(trait_ref)
  43. } else {
  44. COMMON_VTABLE_ENTRIES
  45. };
  46. let layout =
  47. tcx.layout_of(param_env.and(ty)).expect("failed to build vtable representation");
  48. assert!(!layout.is_unsized(), "can't create a vtable for an unsized type");
  49. let size = layout.size.bytes();
  50. let align = layout.align.abi.bytes();
  51. let ptr_size = tcx.data_layout.pointer_size;
  52. let ptr_align = tcx.data_layout.pointer_align.abi;
  53. let vtable_size = ptr_size * u64::try_from(vtable_entries.len()).unwrap();
  54. let mut vtable = Allocation::uninit(vtable_size, ptr_align);
  55. // 无需对下面的内存访问进行任何对齐检查,因为我们知道
  56. // 分配正确对齐,因为我们在上面创建了它。 我们也只是抵消了
  57. // `ptr_align` 的倍数,这意味着它将与 `ptr_align` 保持对齐
  58. // No need to do any alignment checks on the memory accesses below, because we know the
  59. // allocation is correctly aligned as we created it above. Also we're only offsetting by
  60. // multiples of `ptr_align`, which means that it will stay aligned to `ptr_align`.
  61. for (idx, entry) in vtable_entries.iter().enumerate() {
  62. let idx: u64 = u64::try_from(idx).unwrap();
  63. let scalar = match entry {
  64. VtblEntry::MetadataDropInPlace => {
  65. let instance = ty::Instance::resolve_drop_in_place(tcx, ty);
  66. let fn_alloc_id = tcx.create_fn_alloc(instance);
  67. let fn_ptr = Pointer::from(fn_alloc_id);
  68. fn_ptr.into()
  69. }
  70. VtblEntry::MetadataSize => Scalar::from_uint(size, ptr_size).into(),
  71. VtblEntry::MetadataAlign => Scalar::from_uint(align, ptr_size).into(),
  72. VtblEntry::Vacant => continue,
  73. VtblEntry::Method(def_id, substs) => {
  74. // See https://github.com/rust-lang/rust/pull/86475#discussion_r655162674
  75. assert!(!substs.needs_subst());
  76. // Prepare the fn ptr we write into the vtable.
  77. let instance =
  78. ty::Instance::resolve_for_vtable(tcx, param_env, *def_id, substs)
  79. .expect("resolution failed during building vtable representation")
  80. .polymorphize(tcx);
  81. let fn_alloc_id = tcx.create_fn_alloc(instance);
  82. let fn_ptr = Pointer::from(fn_alloc_id);
  83. fn_ptr.into()
  84. }
  85. };
  86. vtable
  87. .write_scalar(&tcx, alloc_range(ptr_size * idx, ptr_size), scalar)
  88. .expect("failed to build vtable representation");
  89. }
  90. vtable.mutability = Mutability::Not;
  91. let alloc_id = tcx.create_memory_alloc(tcx.intern_const_alloc(vtable));
  92. let mut vtables_cache = self.vtables_cache.lock();
  93. vtables_cache.insert((ty, poly_trait_ref), alloc_id);
  94. alloc_id
  95. }
  96. }

**src/ty/context.rs**

  1. pub struct GlobalCtxt<'tcx> {
  2. // ...
  3. // 不过在合并以后,eddyb 对此代码提出了异议: https://github.com/rust-lang/rust/pull/86475/files#r680788892
  4. // FxHashMap 是 rustc 内部使用的一个 hashmap 结构,使用了比 fnv 还快的 hasher,因为这里没有必要防止 DoS 攻击
  5. pub(super) vtables_cache:
  6. Lock<FxHashMap<(Ty<'tcx>, Option<ty::PolyExistentialTraitRef<'tcx>>), AllocId>>,
  7. }
  8. impl<'tcx> TyCtxt<'tcx> {
  9. pub fn create_global_ctxt( /* ... */ ) {
  10. // ...
  11. GlobalCtxt {
  12. // ...
  13. vtables_cache: Default::default(),
  14. }
  15. }
  16. }

rustc_codegen_ssa 中的修改

修改 src/traits/consts.rs 中的 ConstMethods trait,该 trait 定义了一些方法用于调用不同 后端的相关实现。比如在 rustc_codegen_llvm中:

  1. impl ConstMethods<'tcx> for CodegenCx<'ll, 'tcx> {
  2. // ...
  3. }

src/traits/consts.rs 中 :

  1. pub trait ConstMethods<'tcx>: BackendTypes {
  2. // ...
  3. fn const_data_from_alloc(&self, alloc: &Allocation) -> Self::Value;
  4. // ...
  5. }

然后在src/meth.rs 中引入 ty::Ty,并移除 vtable 内存分配相关代码

  1. use rustc_middle::ty::{self, Ty};
  2. pub fn get_vtable<'tcx, Cx: CodegenMethods<'tcx>>(
  3. cx: &Cx,
  4. ty: Ty<'tcx>,
  5. trait_ref: Option<ty::PolyExistentialTraitRef<'tcx>>,
  6. ) -> Cx::Value {
  7. let tcx = cx.tcx();
  8. debug!("get_vtable(ty={:?}, trait_ref={:?})", ty, trait_ref);
  9. // Check the cache.
  10. if let Some(&val) = cx.vtables().borrow().get(&(ty, trait_ref)) {
  11. return val;
  12. }
  13. // 新增
  14. let vtable_alloc_id = tcx.vtable_allocation(ty, trait_ref);
  15. let vtable_allocation = tcx.global_alloc(vtable_alloc_id).unwrap_memory();
  16. let vtable_const = cx.const_data_from_alloc(vtable_allocation);
  17. let align = cx.data_layout().pointer_align.abi;
  18. let vtable = cx.static_addr_of(vtable_const, align, Some("vtable"));
  19. cx.create_vtable_metadata(ty, vtable);
  20. cx.vtables().borrow_mut().insert((ty, trait_ref), vtable);
  21. vtable
  22. }

rustc_mir 中的修改

viable 内存分配已经被定义在了 rustc_middle::ty::Ty 中,所以要移除 rustc_mir 中 vtable 内存分配相关代码。

rustc_mir 中修改的是 miri 相关代码,miri 用于编译器常量计算。

compiler/rustc_mir/src/interpret/intern.rs 内删除 Vtable 相关内存类型。 该模块用于 常量计算的全局内存分配。

  1. // compiler/rustc_mir/src/interpret/intern.rs
  2. fn intern_shallow<'rt, 'mir, 'tcx, M: CompileTimeMachine<'mir, 'tcx, const_eval::MemoryKind>>(
  3. ecx: &'rt mut InterpCx<'mir, 'tcx, M>,
  4. leftover_allocations: &'rt mut FxHashSet<AllocId>,
  5. alloc_id: AllocId,
  6. mode: InternMode,
  7. ty: Option<Ty<'tcx>>,
  8. ) -> Option<IsStaticOrFn> {
  9. // ...
  10. match kind {
  11. MemoryKind::Stack
  12. | MemoryKind::Machine(const_eval::MemoryKind::Heap)
  13. // | MemoryKind::Vtable // 移除
  14. | MemoryKind::CallerLocation => {}
  15. }
  16. // ...
  17. }

compiler/rustc_mir/src/interpret/eval_context.rs 中删除 vtable cache相关:

  1. // compiler/rustc_mir/src/interpret/eval_context.rs
  2. pub struct InterpCx<'mir, 'tcx, M: Machine<'mir, 'tcx>> {
  3. // ...
  4. // 移除下面三行
  5. // /// A cache for deduplicating vtables
  6. // pub(super) vtables:
  7. // FxHashMap<(Ty<'tcx>, Option<ty::PolyExistentialTraitRef<'tcx>>), Pointer<M::PointerTag>>,
  8. // ...
  9. }
  10. impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
  11. pub fn new(
  12. tcx: TyCtxt<'tcx>,
  13. root_span: Span,
  14. param_env: ty::ParamEnv<'tcx>,
  15. machine: M,
  16. memory_extra: M::MemoryExtra,
  17. ) -> Self {
  18. InterpCx {
  19. machine,
  20. tcx: tcx.at(root_span),
  21. param_env,
  22. memory: Memory::new(tcx, memory_extra),
  23. // vtables: FxHashMap::default(), // 移除此行
  24. }
  25. }
  26. // ...
  27. }

compiler/rustc_mir/src/interpret/memory.rs 中:

  1. impl<T: MayLeak> MayLeak for MemoryKind<T> {
  2. #[inline]
  3. fn may_leak(self) -> bool {
  4. match self {
  5. MemoryKind::Stack => false,
  6. // MemoryKind::Vtable => true, // 移除此行
  7. MemoryKind::CallerLocation => true,
  8. MemoryKind::Machine(k) => k.may_leak(),
  9. }
  10. }
  11. }
  12. impl<T: fmt::Display> fmt::Display for MemoryKind<T> {
  13. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  14. match self {
  15. MemoryKind::Stack => write!(f, "stack variable"),
  16. // MemoryKind::Vtable => write!(f, "vtable"), // 移除此行
  17. MemoryKind::CallerLocation => write!(f, "caller location"),
  18. MemoryKind::Machine(m) => write!(f, "{}", m),
  19. }
  20. }
  21. }

compiler/rustc_mir/src/interpret/terminator.rs 中:

  1. impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
  2. // ...
  3. /// Call this function -- pushing the stack frame and initializing the arguments.
  4. fn eval_fn_call(
  5. &mut self,
  6. fn_val: FnVal<'tcx, M::ExtraFnVal>,
  7. caller_abi: Abi,
  8. args: &[OpTy<'tcx, M::PointerTag>],
  9. ret: Option<(&PlaceTy<'tcx, M::PointerTag>, mir::BasicBlock)>,
  10. mut unwind: StackPopUnwind,
  11. ) -> InterpResult<'tcx> {
  12. // ...
  13. // 这里处理trait对象
  14. ty::InstanceDef::Virtual(_, idx) => {
  15. // ...
  16. // Find and consult vtable
  17. let vtable = receiver_place.vtable();
  18. let fn_val = self.get_vtable_slot(vtable, u64::try_from(idx).unwrap())?; // 修改 `drop_val` 为 `fn_val`
  19. // ...
  20. // recurse with concrete function
  21. self.eval_fn_call(fn_val, caller_abi, &args, ret, unwind)
  22. }
  23. }
  24. // ...
  25. }

compiler/rustc_mir/src/interpret/traits.rs 中:

  1. impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
  2. /// Creates a dynamic vtable for the given type and vtable origin. This is used only for
  3. /// objects.
  4. ///
  5. /// The `trait_ref` encodes the erased self type. Hence, if we are
  6. /// making an object `Foo<Trait>` from a value of type `Foo<T>`, then
  7. /// `trait_ref` would map `T: Trait`.
  8. pub fn get_vtable(
  9. &mut self,
  10. ty: Ty<'tcx>,
  11. poly_trait_ref: Option<ty::PolyExistentialTraitRef<'tcx>>,
  12. ) -> InterpResult<'tcx, Pointer<M::PointerTag>> {
  13. trace!("get_vtable(trait_ref={:?})", poly_trait_ref);
  14. let (ty, poly_trait_ref) = self.tcx.erase_regions((ty, poly_trait_ref));
  15. // All vtables must be monomorphic, bail out otherwise.
  16. ensure_monomorphic_enough(*self.tcx, ty)?;
  17. ensure_monomorphic_enough(*self.tcx, poly_trait_ref)?;
  18. // 移除了之前的大部分代码,浓缩为这两行
  19. // 为 vtable 分配内存,并拿到相关指针
  20. let vtable_allocation = self.tcx.vtable_allocation(ty, poly_trait_ref);
  21. let vtable_ptr = self.memory.global_base_pointer(Pointer::from(vtable_allocation))?;
  22. Ok(vtable_ptr)
  23. }
  24. }

rustc_codegen_cranelift 中的修改

rustc_codegen_cranelift 中也是移除 vtable 内存分配相关代码。

上一个 PR 分析文章中说到, rustc_codgen_cranelift 因为没有依赖 rust_codgen_ssa的一些关键trait,所以vtable 内存分配这里还存在冗余代码。在重构 vtable 内存分配之后,就可以将这些冗余代码消除了。

compiler/rustc_codegen_cranelift/src/vtable.rs 中:

  1. pub(crate) fn get_vtable<'tcx>(
  2. fx: &mut FunctionCx<'_, '_, 'tcx>,
  3. ty: Ty<'tcx>, // 这里使用了 `ty::Ty`
  4. trait_ref: Option<ty::PolyExistentialTraitRef<'tcx>>,
  5. ) -> Value {
  6. // 删除了之前的内存分配相关代码(主要是 build_vtable 函数),精简很多
  7. let vtable_ptr = if let Some(vtable_ptr) = fx.vtables.get(&(ty, trait_ref)) {
  8. *vtable_ptr
  9. } else {
  10. let vtable_alloc_id = fx.tcx.vtable_allocation(ty, trait_ref);
  11. let vtable_allocation = fx.tcx.global_alloc(vtable_alloc_id).unwrap_memory();
  12. let vtable_ptr = pointer_for_allocation(fx, vtable_allocation);
  13. fx.vtables.insert((ty, trait_ref), vtable_ptr);
  14. vtable_ptr
  15. };
  16. vtable_ptr.get_addr(fx)
  17. }

主要是这个方法的修改,其他修改都是围绕该方法的琐碎修改。

rustc_codegen_llvm 中的修改

compiler/rustc_codegen_llvm/src/common.rs 中:

  1. impl ConstMethods<'tcx> for CodegenCx<'ll, 'tcx> {
  2. // ...
  3. fn const_data_from_alloc(&self, alloc: &Allocation) -> Self::Value {
  4. const_alloc_to_llvm(self, alloc)
  5. }
  6. // ...
  7. }

小结

这次 PR 主要是将 vtable 的内存分配重构到 rustc_middle::ty::Ty ,以便其他组件可以公用。这里只是一个大概梳理,还有很多细节可以深究。