资讯动态

.NET Runtime 托管类型系统(Managed Type System)深入解析:AOT 与 IL 验证的新一代基础设施

发布时间:2026/9/17 2:49:46 来源:尧图企业网站定制
.NET Runtime 托管类型系统Managed Type System深入解析AOT 与 IL 验证的新一代基础设施【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime本文基于仓库文档 managed-type-system.md 并结合源码展开。Managed Type System 是新一代 .NET 工具AOT 编译、IL 验证的核心基础设施它将 CoreCLR 原生类型系统用 C# 重新实现为编译器、验证器与运行时提供统一的类型表示与高层服务。读完本文你将理解其设计哲学低开销、并发、可扩展、核心类层次、可插拔算法机制以及它如何在 ECMA-335 元数据之上构建运行时语义。托管类型系统Managed Type System是新一代 .NET 工具面向 AOT 与 IL 验证的核心组件。它表示程序中的模块module、类型type、方法method与字段field并向类型系统的使用者提供高层服务使其能够回答各种关键问题。从本质上看托管类型系统是 CoreCLR 原生类型系统 用 C# 重写的等价物——将运行时功能用 C# 实现一直是 .NET 团队的长期目标而托管类型系统正是让这一目标得以落地的基础设施。高层服务类型系统能回答什么类型系统向使用者提供的高层服务包括从元数据加载新类型按需解析并物化新的类型对象计算某个类型实现的接口集合包括从基类继承而来的全部接口计算静态与实例字段布局为每个字段分配内存偏移计算类型的静态与实例 GC 布局识别对象/类数据中的 GC 指针供 GC 精确扫描使用计算 VTable 布局并解析虚方法到槽位为虚方法分配 VTable 槽位并将虚方法调用解析为具体槽位判定一个类型能否存储到另一个类型的位置实现 ECMA-335 中可赋值性assignability等语义判定。这些服务构成编译期与运行期共同依赖的语义层。例如 AOT 编译器需要字段布局来生成对象内存布局需要 VTable 布局来生成调度代码IL 验证器需要可赋值性判定来校验 IL 指令合法性。三大设计主题类型系统的设计由三个主题驱动低开销与高性能Low overhead and high performance并发Concurrency可扩展性与可复用性Extensibility and reusability低开销懒加载 保守缓存低开销通过**懒加载lazy loading**实现——类型系统不会在创建类型对象时急切地填充字段、属性、名称等信息而是按需从底层数据源元数据读取缓存的使用非常保守。这一点在源码中有直接体现。在 TypeDesc.cs 中注释明确指出最常用的类型属性被缓存以避免过多的虚调用// The most frequently used type properties are cached here to avoid excessive virtual calls private TypeFlags _typeFlags;类型标志如IsInterface、IsValueType、IsPrimitive、IsGCPointer等通过 GetTypeFlags 惰性计算并以TypeFlags位掩码缓存首次访问时经InitializeTypeFlags调用虚方法ComputeTypeFlags计算后写入缓存后续访问直接命中缓存位[MethodImpl(MethodImplOptions.AggressiveInlining)] protected internal TypeFlags GetTypeFlags(TypeFlags mask) { TypeFlags flags _typeFlags mask; if (flags ! 0) return flags; return InitializeTypeFlags(mask); }以IsGCPointer为例TypeDesc.cs它直接基于缓存的类别标志判定类Class、数组Array/SzArray与接口Interface的实例位置引用 GC 堆上的对象因此属于 GC 指针public bool IsGCPointer { get { TypeFlags category GetTypeFlags(TypeFlags.CategoryMask); return category TypeFlags.Class || category TypeFlags.Array || category TypeFlags.SzArray || category TypeFlags.Interface; } }并发无锁读者哈希表并发性体现在类型系统上下文TypeSystemContext对构造类型的规范化管理上。在 TypeSystemContext.cs 中构造函数初始化了多张LockFreeReaderHashtable用于缓存数组类型、ByRef 类型、指针类型、函数指针类型、实例化类型与实例化方法等_instantiatedTypes new InstantiatedTypeKey.InstantiatedTypeKeyHashtable(); _arrayTypes new ArrayTypeKey.ArrayTypeKeyHashtable(); _byRefTypes new ByRefHashtable(); _pointerTypes new PointerHashtable(); _functionPointerTypes new FunctionPointerHashtable(); _instantiatedMethods new InstantiatedMethodKey.InstantiatedMethodKeyHashtable(); _methodForInstantiatedTypes new MethodForInstantiatedTypeKey.MethodForInstantiatedTypeKeyHashtable(); _fieldForInstantiatedTypes new FieldForInstantiatedTypeKey.FieldForInstantiatedTypeKeyHashtable(); _signatureVariables new SignatureVariableHashtable(this);LockFreeReaderHashtable允许多个读者并发访问而不加锁只有写者需要同步这为编译流水线中的并发场景如并行处理多个方法体提供了基础。可扩展性源码级复用与可插拔算法目标 3可扩展性与可复用性并非通过多态和对象层级来实现而是通过部分类partial class、扩展方法与可插拔算法pluggable algorithms达成。类型系统的复用发生在源码层面——通过源码包含不同的文件集合来获得不同的功能组合。这允许在不大幅牺牲性能目标 1的前提下实现扩展。从目录结构可以清楚看到这种组织方式src/coreclr/tools/Common/TypeSystem/Common 存放公共类型系统而 src/coreclr/tools/Common/TypeSystem/Ecma 存放读取 ECMA-335 元数据的具体实现TypeDesc.cs、MethodDesc.cs、FieldDesc.cs、DefType.cs等核心类都以partial形式分布在多个文件中按需源码包含。值得强调的是纯粹形态的类型系统即不附加任何部分类扩展力求不引入 ECMA-335 规范之外的概念。ECMA-335 被本文档视为前置阅读材料文中使用的各种术语均以该规范的定义为准。与元数据的关系元数据如 ECMA-335 描述的文件格式与类型系统关系密切但两者有明确分工元数据描述类型的物理形态如类型的基类是什么、有哪些字段类型系统在物理形态之上构建高层概念如运行时存储一个类型实例需要多少字节、类型实现了哪些接口含继承得来的接口。类型系统提供对底层元数据大部分内容的访问但抽象了元数据的获取方式。这使得由其他格式元数据支撑、甚至完全没有元数据支撑的类型和成员也能在同一个类型系统上下文中被表示。典型例子数组类型的合成方法一个没有底层元数据支撑的典型例子是数组类型上的方法。例如整数数组拥有Get(int)、Set(int, int)、Address(int)以及构造函数等成员但它们不会出现在任何程序集的元数据表中而是由类型系统合成出来。在源码中这由 ArrayType.cs 的ArrayMethodKind枚举定义public enum ArrayMethodKind { Get, Set, Address, AddressWithHiddenArg, Ctor }数组方法通过 GetArrayMethod 按需获取。值得注意的是Address与AddressWithHiddenArg的微妙区分ArrayType.cs 注释IL 引用Address方法时它需要在调用点进行类型检查但该类型只在调用点可知——类型系统通过告诉 codegen 该方法需要一个隐藏的实例化参数这一技巧来捕获调用点的类型而在编译方法体本身时则编译为显式把隐藏参数列入签名、可作为普通参数使用的AddressWithHiddenArg版本。类型系统类层次结构上图中展示了类型系统中表示类型的各个类。图中大多数类不应当被类型系统使用者派生其中许多类被sealed修饰以阻止派生。图中以深色背景标出的类才是真正可扩展的并且实际上是抽象类类。具体类应当基于某种逻辑如从 ECMA-335 模块文件读取元数据为抽象方法和虚方法提供实现——类型系统已经为MetadataType提供了这样的实现其派生类EcmaType即从 ECMA-335 元数据读取类型信息。在 EcmaType.cs 中可以看到这一设计/// summary /// Override of MetadataType that uses actual Ecma335 metadata. /// /summary public sealed partial class EcmaType : MetadataType, EcmaModule.IEntityHandleObject { private EcmaModule _module; private TypeDefinitionHandle _handle; ... }EcmaType持有EcmaModule模块与TypeDefinitionHandle类型定义句柄并缓存名称指针、泛型参数、基类等计算代价较高的信息。类型系统的设计意图是让使用者通过抽象类型工作——无论类型来自 ECMA-335 模块、由编译器合成、还是表示某个泛型实例化抽象类型都能提供所需的全部信息。因此类型系统使用者应尽可能在抽象类上操作仅在创建新实例时才使用具体类将实例强转为EcmaType这类具体实现类型是不被鼓励的做法。类型系统核心类逐层解析TypeDesc所有类型的基类TypeDesc是类型系统中所有类型的基类TypeDesc.cs定义了一组所有类都必须支持的操作。并非所有操作对TypeDesc的每个子类都有意义——例如向指针类型请求方法列表没有意义——但类型系统会为每个特定子类提供有意义的实现如指针类型的方法列表为空。TypeDesc还承载了引用同一性reference identity语义。其Equals实现TypeDesc.cs断言两个比较对象属于同一上下文并用引用相等来判断类型相等public override bool Equals(object o) { // Its only valid to compare two TypeDescs in the same context Debug.Assert(o is not TypeDesc || ReferenceEquals(((TypeDesc)o).Context, this.Context)); return ReferenceEquals(this, o); }这正是文档所述两个TypeDesc对象表示相同类型当且仅当它们是同一对象实例的代码印证。TypeDesc上还提供了丰富的判定属性IsInterface、IsValueType、IsPrimitive、IsEnum、IsDelegate、IsString、IsObject、IsArray/IsSzArray/IsMdArray、IsByRef、IsPointer、IsFunctionPointer、IsGenericParameter、IsGCPointer、IsByRefLike等以及GetMethods()、GetFields()、GetStaticConstructor()、GetFinalizer()、GetTypeDefinition()等成员访问方法。ParameterizedTypeArrayType / ByRefType / PointerType这些是带单个参数的类型构造constructed types包括三种数组可以是多维数组multi-dimensional array也可以是向量vector即单维且下界隐含为零的数组托管引用managed reference即 ByRef 类型非托管指针unmanaged pointer。需要特别警惕的是秩rank为 1 的多维数组与向量之间的区别至关重要也是类型系统使用者潜在 bug 的一大来源。TypeDesc上为此提供了明确的区分手段——IsArray对两种数组都返回 true而IsSzArray仅对向量返回 trueIsMdArray仅对多维数组返回 true见 TypeDesc.cs。在 ArrayType.cs 中秩为 -1 表示向量否则为多维数组protected override TypeFlags ComputeTypeFlags(TypeFlags mask) { TypeFlags flags _rank -1 ? TypeFlags.SzArray : TypeFlags.Array; ... }DefTypeNoMetadataType / MetadataTypeDefType表示值类型、接口或类。虽然大多数DefType实例是MetadataType的子类即基于完整描述类型的元数据但存在完整元数据不可用的场景——此时仅能获得受限信息如类型实例在 GC 堆上占用的字节数或该类型是否为值类型。类型系统必须能够在这样的类型上继续工作例如一个元数据受限的类型可以作为某个元数据完整的类型的基类而字段布局算法必须能计算出这样一个类型的字段布局。GenericParameterGenericParameter表示泛型参数及其约束。泛型定义被表示为对泛型参数的实例化instantiations over generic parameters。对于熟悉 .NET 反射类型系统的读者这里有一个重要区别需要指出.NET 反射类型系统不区分泛型定义如ListT与泛型类型的开放实例化如List!0而托管类型系统明确区分这两者。这一区分在表示 IL 方法体中的成员引用时至关重要——例如IL 中通过LDTOKEN指令对ListT.Add的引用应始终指向未实例化的定义而对List!0.Add的引用则会在替换签名变量后指向具体的方法。SignatureVariableSignatureTypeVariable / SignatureMethodVariable签名变量表示可以被系统中其他类型替换的变量。它们与泛型参数不同——例如它们没有约束constraints或变体variance语义仅仅是作为**实例化instantiation**流程中待替换的占位符。签名变量有一个索引指向实例化上下文中的位置。考虑类FooT及其方法BarU(T x, U y)当 IL 在签名中引用该方法时T变为!0一个SignatureTypeVariable表示声明类型的第一个类型实参U变为!!0一个SignatureMethodVariable表示声明方法的第一个类型实参。在 TypeSystemContext.cs 中签名变量同样由SignatureVariableHashtable规范化管理并通过GetSignatureVariable(int index, bool method)按索引获取其中method标志用最高位0x80000000编码到组合索引中。其他类型系统类TypeSystemContext类型宇宙的容器每次使用类型系统都始于创建一个类型系统上下文type system context。上下文代表一个类型宇宙type universe其中所有类型共享引用同一性——两个TypeDesc对象表示相同类型当且仅当它们是同一对象实例。上下文被用来解析宇宙中的所有模块和构造类型在类型系统上下文之外创建新的构造类型实例是非法的。所有类型系统上下文共享同一个基类TypeSystemContext但每个上下文配置不同的可插拔算法、以不同方式加载程序集并可能支持不同的合成类型。例如NativeAOT 编译器上下文与ReadyToRuncrossgen2编译器上下文使用不同的字段布局算法以补偿细微差异——比如System.Object中的MethodTable指针在 NativeAOT 中是常规指针字段而在 CoreCLR 类型系统中只是被腾空的指针大小空间。TypeSystemContext同时是构造类型的规范化工厂TypeSystemContext.cspublic ArrayType GetArrayType(TypeDesc elementType) { return GetArrayType(elementType, -1); }以及通过GetInstantiatedType、GetByRefType、GetPointerType、GetFunctionPointerType、GetInstantiatedMethod、GetMethodForInstantiatedType、GetFieldForInstantiatedType、GetSignatureVariable等方法统一创建/复用各类构造实体。MethodDesc方法的可扩展层次与TypeDesc层次结构相似MethodDesc遵循同样的可扩展层次模式表示类型系统中的所有方法。其部分子类包括EcmaMethod从 ECMA-335 元数据读取的方法ArrayMethod合成的数组方法InstantiatedMethod泛型方法实例化如Foo.BarintILStubMethod编译器生成的桩方法用于 P/Invoke 封送marshalling等场景。在 ArrayType.cs 中可以看到ArrayMethod的实现形态它持有ArrayType所属数组类型与ArrayMethodKind方法种类属于MethodDesc的密封子类。ModuleDesc 与 IAssemblyDescModuleDesc描述单个模块如果该模块是一个程序集assembly它还可以实现IAssemblyDesc接口。ModuleDesc通常是模块内类型/方法/字段定义的所有者并负责维护这些定义的引用同一性——即保证同一个定义在宇宙中只有一个对象实例。可插拔算法Pluggable algorithms类型系统提供的大多数算法如字段布局算法都是可插拔的。类型系统上下文可以通过提供不同的算法实现来影响算法的选择。算法作为扩展机制被用在部分类和源码包含不足以胜任的地方。特定算法的选择可能依赖多个因素类型系统使用者可能希望根据一组在运行时确定的条件使用多种算法——例如为常规DefType计算运行时接口列表与为数组类型计算运行时接口列表使用不同的算法。在源码中这一抽象由 TypeSystemContext.cs 中的一组虚方法体现public RuntimeInterfacesAlgorithm GetRuntimeInterfacesAlgorithmForType(TypeDesc type) { if (type.IsDefType) { return GetRuntimeInterfacesAlgorithmForDefType((DefType)type); } else if (type.IsArray) { ArrayType arrType (ArrayType)type; TypeDesc elementType arrType.ElementType; if (arrType.IsSzArray !elementType.IsPointer !elementType.IsFunctionPointer) { return GetRuntimeInterfacesAlgorithmForNonPointerArrayType((ArrayType)type); } else { return BaseTypeRuntimeInterfacesAlgorithm.Instance; } } return null; }可以看到数组类型特别是元素非指针/函数指针的向量类型与常规DefType走不同的接口计算算法GetVirtualMethodAlgorithmForType则负责虚方法解析算法的选择。相关的算法抽象定义分布在 RuntimeInterfacesAlgorithm.cs、VirtualMethodAlgorithm.cs、FieldLayoutAlgorithm.cs 等文件中而MetadataTypeSystemContext等上下文类型则为这些算法提供具体实现。类型系统的哈希码类型系统一个有趣的特性是它能够计算在编译期和运行期都可被可靠计算的类型或方法哈希码。编译期与运行期拥有相同哈希码这一点被用于在 AOT 编译代码中构建高性能查找表。哈希码由类型名称计算而来并作为运行时数据结构的一部分被保留从而在编译器把类型名称优化掉之后仍然可用。其实现位于 TypeHashingAlgorithms.cs核心是ComputeNameHashCode——一个基于0x6DA3B944种子与循环左移_rotl的字符串哈希算法public static int ComputeNameHashCode(string src) { int hash1 0x6DA3B944; int hash2 0; for (int i 0; i src.Length; i 2) { hash1 (hash1 _rotl(hash1, 5)) ^ src[i]; if ((i 1) src.Length) hash2 (hash2 _rotl(hash2, 5)) ^ src[i 1]; } hash1 _rotl(hash1, 8); hash2 _rotl(hash2, 8); return hash1 ^ hash2; }该文件还提供了ComputeMethodSignatureHashCode、ComputeSignatureVariableHashCode等方法。相关的 VersionResilientHashCode.TypeSystem.cs 提供对构造类型数组、泛型实例等的版本弹性哈希计算——所谓版本弹性version resilient意味着哈希值在程序集版本演化时保持稳定这使编译产物在不同版本间保持兼容。从类型系统抛出异常在类型系统内部抛出异常比简单的throw语句要复杂得多。原因在于类型系统被设计为可在多种场景下使用而每种场景对异常抛出的要求可能不同当类型系统被运行时runtime包含时类型加载失败应抛出System.TypeLoadException而当类型加载错误发生在编译器或 IL 验证器中时System.TypeLoadException会与编译器自身托管程序集的实际问题难以区分因此应抛出不同的异常。类型系统内部的异常抛出被封装在ThrowHelper类中。类型系统的使用者需要提供该类的定义及其方法由这些方法控制将抛出何种异常类型。类型系统提供了一个默认的ThrowHelper实现它会抛出派生自TypeSystemException异常基类的异常——这个默认实现适用于非运行时场景。在 ThrowHelper.cs 中可以看到默认实现确实抛出一族TypeSystemException的子类型public static void ThrowMissingMethodException(TypeDesc owningType, string methodName, MethodSignature signature) { throw new TypeSystemException.MissingMethodException(ExceptionStringID.MissingMethod, Format.Method(owningType, methodName, signature)); }TypeSystemExceptionTypeSystemException.cs携带字符串资源标识符ExceptionStringID与格式化参数Arguments消息通过GetExceptionString按 ID 查询格式化字符串生成public ExceptionStringID StringID { get; } public IReadOnlyListstring Arguments { get; } public override string Message GetExceptionString(StringID, _arguments);字符串 ID 间接层支持编译器场景异常消息被赋予字符串 ID并由 throw helper 消费。这一间接层是支持编译器场景所必需的当 AOT 编译期间发生类型加载异常时AOT 编译器有两项任务——发出警告告知用户该问题已发生潜在地生成一个方法体让其在运行时访问到问题类型时抛出该异常。编译器的本地化localization可能与编译器输出所链接的类库的本地化不一致。通过字符串 ID 间接传递实际异常消息就可以包装这一差异。类型系统的使用者也可以在类型系统之外、任何需要此功能的地方复用 throw helper。相关的 ID 枚举定义在 ExceptionStringID.cs错误消息的默认文本则在 TypeSystemException.Resources.cs 中。物理架构源码布局类型系统的实现位于仓库的以下位置src/coreclr/tools/Common/TypeSystem/Common大部分公共类型系统代码都在这里——包括TypeDesc、MethodDesc、FieldDesc、DefType、ArrayType、ByRefType、PointerType、GenericParameterDesc、SignatureVariable、TypeSystemContext、ThrowHelper、TypeHashingAlgorithms以及FieldLayoutAlgorithm、RuntimeInterfacesAlgorithm、VirtualMethodAlgorithm等算法抽象和MetadataFieldLayoutAlgorithm、MetadataRuntimeInterfacesAlgorithm、MetadataVirtualMethodAlgorithm、BaseTypeRuntimeInterfacesAlgorithm等元数据驱动实现src/coreclr/tools/Common/TypeSystem/EcmaMetadataType、MethodDesc、FieldDesc等从 ECMA-335 模块文件读取元数据的具体实现——包括EcmaType、EcmaMethod、EcmaField、EcmaModule、EcmaAssembly、EcmaGenericParameter以及EcmaSignatureParser等签名解析组件src/coreclr/tools/aot/ILCompiler.TypeSystem.Tests类型系统单元测试注意原文档所述测试目录在仓库中的实际路径为该目录是理解类型系统运作与特性的绝佳起点。测试覆盖了字段布局ArchitectureSpecificFieldLayoutTests.cs、DefType.FieldLayoutTests.cs、泛型与实例化GenericTypeAndMethodTests.cs、CanonicalizationTests.cs、可赋值性判定CastingTests.cs、GC 指针映射GCPointerMapTests.cs、约束验证ConstraintsValidationTest.cs以及异常消息ExceptionStringTests.cs等主题非常值得通读。此外src/coreclr/tools/Common/TypeSystem 下还有若干扩展目录Canon泛型共享/规范化相关、CodeGen代码生成辅助、ILIL 相关工具、Interop互操作相关、Mangling名称修饰、MetadataEmitter元数据发射、RuntimeDetermined运行时确定类型、Sorting确定性排序、TypesDebugInfoWriter类型调试信息写出等它们通过源码包含与部分类扩展的方式为不同工具提供定制能力。与 CoreCLR 类型系统的显著差异托管类型系统与 CoreCLR 原生类型系统之间存在一个值得注意的差异MethodDesc在托管类型系统中尽可能携带精确的泛型实例化exact generic instantiations。托管类型系统中的代码共享策略code sharing policy是可插拔算法之一它不影响MethodDesc的同一性而在 CoreCLR 类型系统中代码共享策略与MethodDesc的同一性耦合在一起。这一差异的具体表现可参考 dotnet/runtime 仓库 PR #45744 的讨论——它展示了同一性语义分离如何带来更简洁的表示与更灵活的代码共享决策。对 AOT 编译器而言这意味着同一泛型方法的共享与非共享形态可以被独立建模而不会污染方法对象的身份。结语托管类型系统是 .NET 新一代 AOT 编译NativeAOT、crossgen2/ReadyToRun与 IL 验证工具的共同基石。它以用 C# 重写 CoreCLR 类型系统为出发点通过懒加载、保守缓存、无锁读者哈希表与源码级可扩展性三大设计支柱在 ECMA-335 元数据之上构建出具备字段布局、接口计算、VTable 布局、可赋值性判定、类型哈希与可插拔异常策略等完整语义能力的类型宇宙。无论是想深入理解 AOT 编译器的类型推导逻辑还是想为 .NET 工具链贡献新的算法扩展managed-type-system.md 与 src/coreclr/tools/Common/TypeSystem 下的源码、ILCompiler.TypeSystem.Tests 下的测试都是最值得从零读起的材料。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价