.NET运行时之书(Book of the Runtime,简称BotR)是一系列描述.NET运行时的文档,2007年左右在微软内部创建,最初目的是为了帮助其新员工快速上手.NET运行时;随着.NET开源,BotR也被公开了出来,如果想深入理解CLR,这系列文章不可错过。
BotR系列目录:
[1] CLR类型加载器设计(Type Loader Design)
[2] CLR类型系统概述(Type System Overview)
原文:https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/type-system.md
作者: David Wrighton - 2010
翻译:几秋 (https://www.cnblogs.com/netry/)
CLR类型系统是我们在ECMA规范+扩展中描述的类型系统的表示形式。
该类型系统是由一系列数据结构(其中一些在BotR的其它章节有描述)和操作这些数据结构的算法组合而成,它不是通过反射暴露出来的类型系统,尽管反射确实依赖于这个系统。
由类型系统维护的主要数据结构是:
由类型系统包含的主要算法是:
还有很多辅助数据结构和算法为CLR的其余部分提供各种信息,但它们对于整个系统的理解并不那么重要。
类型系统的数据结构通常被各种算法所使用。本文档不会涉及类型系统算法(),但是它会试图描述各种主要数据结构。
类型系统大体上是给CLR中很多部分提供服务,多数核心组件都对类型系统的行为有某种形式的依赖性。下图描述了影响类型系统的通用数据流,它不是很全面,但是指出了只要的信息流。
类型系统的主要依赖关系如下:
类型系统有3个主要组件依赖于它:
核心类型系统数据结构是表示实际加载类型的数据结构(例如,TypeHandle, MethodTable, MethodDesc, TypeDesc, EEClass)和允许在加载类型后找到它们的数据结构(例如,ClassLoader, Assembly, Module, RIDMaps)。
加载类型的数据结构和算法在BotR的Type Loader 和 MethodDesc章节中有讨论。
将这些数据结构绑定在一起是一组功能, 允许JIT/Reflection/TypeLoader/stackwalker去查找现存类型和方法,一般的想法是,这些搜索应该很容易地由ECMA CLI规范中指定的元数据令牌/签名(metadata tokens/signatures)驱动。
最后,当合适的类型系统数据结构被找到,我们有算法从类型中收集信息,有and/or比较两个类型。可以在 Virtual Stub Dispatch找到这种算法的一个特别复杂的例子。
类型转换算法(casting algorithm)是类型系统中的典型算法,在托管代码的执行过程中大量使用这种算法。
这个算法至少有4个独立的入口(entry point),选择每个入口都是为了提供不同的快速路径,希望能够实现最佳的性能。
除了最后一个之外,每个实现都进行了优化,以便在不完全通用的情况下提高性能。例如,“一个类型能否被转换成一个父类?”就是 “一个对象能否被转换成一个非类型等价的非数组类型”的变体,它通过单循环遍历一个单链表实现。这只能搜索可能的转换操作的一个子集,但可以通过检查试图强制转换的类型来确定是否是合适的集合,这个算法在jit helper JIT_ChkCastClass_Portable
中实现。
假设:
在类型系统中很多算法遵循这种常见模式。类型系统通常用于查找类型,这可以通过任意数量的输入触发,如JIT、反射、序列化、远程调用等等。在这些情况下,对类型系统的基本输入是:
这个算法必须首先解码标识符。对于搜索一个类型的场景,令牌可能是typedef令牌、typeref令牌、typespec令牌,或者是一个字符串。这些不同种类的标识符都会将导致不同形式的查询。
从这个设计中可以明显看出一些搜索算法在类型系统中的共同特点:
除了这个总体设计,在此基础上,还有一些额外的需求:
此搜索算法是 JIT期间使用的典型程序。它具有许多共同的特征:
这使我们能够满足性能要求,以及使用基于IL的JIT所必需的特征。
垃圾回收器要有关类型实例分配在GC堆上的信息,这是通过一个指向类型系统数据结构(MethodTable)的指针来完成,该MethodTable位于每一个托管对象的开头。附着到这个MethodTable之上的,是一个描述类型实例GC布局的数据结构。该布局有两种形式(一般类型和对象数组是一种,值类型数组是另一种)。
堆栈遍历器/GC堆栈遍历器在两种情况下要求类型系统输入:
由于各种原因,包括希望延迟加载类型,以及避免生成多个版本的代码(仅通过相关的gc信息不同),CLR当前需要遍历堆栈上的方法签名,这种需求很少得到满足,因为它要求在特定的时刻执行堆栈遍历,但是为了满足我们的现实目标,当遍历堆栈的时候,必须能够遍历签名。
堆栈遍历器以大约 3 种模式执行:
在GC和分析工具遍历堆栈的情况,由于线程被挂起,分配内存或占用大多数锁是不安全的。这导致我们开发了一条通过类型系统的路径,可以依赖它来遵循上述要求。型系统实现此目标所需的规则是:
这是通过一系列广泛而复杂的强制措施来实施的,包括类型加载器、NGEN镜像生成过程和JIT。
类型系统数据结构是保存到NGEN镜像中的核心部分,不幸的是,这些数据结构逻辑内有指向其它NGEN镜像的指针。为了处理这种情况,类型系统数据结构实现了一个称为恢复(restoration)的概念。
在恢复期,当第一次需要类型系统数据结构时,该数据结构用正确的指针固定, 这与类型加载级别有关,请看前篇CLR类型加载器设计
还存在一个预恢复(pre-restored)数据结构的概念,这意味着数据结构在NGEN镜像加载时足够正确(在intra-module指针和预先加载类型修正之后),数据结构可以按原样使用。此优化要求将NGEN镜像“硬绑定”("hard bound")到其依赖程序集,详情请查看NGEN相关文档。
类型系统是实现域中性加载的核心部分,它通过在AppDomain创建时启用LoaderOptimization选项暴露给用户。Mscorlib始终作为域中性加载,此功能的核心要求是类型系统数据结构不能要求指向特定域状态(domain specific state)的指针,这主要表现在围绕静态字段和类构造函数的需求中。特别是,由于这个原因,一个类的构造函数是否已经运行不是核心MethodTable数据结构的一部分。并且有一种机制来存储附加到DomainFile数据结构而不是MethodTable数据结构。
类型系统的主要部分位于:
主要入口函数是BuildMethodTable、 LoadTypeHandleThrowing、CanCastTo*、 GetMethodDescFromMemberDefOrRefOrSpecThrowing、 GetFieldDescFromMemberRefThrowing、 CompareSigs和 VirtualCallStubManager::ResolveWorkerStatic.