.NET Mono 运行时 gsharedvt 泛型共享机制深度解析为值类型实现 AOT 友好的泛型代码共享【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime导读本文基于 .NET 运行时仓库中 docs/design/mono/web/gsharedvt.md 设计文档结合src/mono/mono/mini/下的真实源码系统讲解 Mono 运行时如何将泛型共享generic sharing从引用类型扩展到值类型gsharedvtgeneric sharing for valuetypes。gsharedvt 是 iOS、WebAssembly 等禁止运行时生成机器码JIT 不可用环境下 AOT 编译泛型代码的基石。读完本文你将理解为什么值类型泛型无法简单共享、gsharedvt 方法如何用localloc动态分配本地变量、MonoGSharedVtMethodRuntimeInfo与 rgctx 如何驱动运行时信息以及 gsharedvt trampoline / arg trampoline / in-out wrapper 三级调用链的完整工作流程并能在仓库源码中定位每一条关键实现路径。问题背景AOT 环境下的泛型实例化困境在某些环境如 iOS中系统不允许在运行时动态生成原生代码。这意味着应用使用到的所有方法都必须在编译期AOT提前编译完成。对于泛型方法这并不总是可行的。设计文档给出了这样一个例子interface IFace { void fooT (T t); } class Class1 : IFace { public virtual void fooT (T t) { ... } } IFace o new Class1 (); o.fooint ();在这个例子中编译期很难确定Class1:fooint在运行时是否会被需要——泛型实例化可能由反射、依赖注入、序列化等间接路径触发静态分析无法穷举所有可能的T。引用类型的泛型共享对于以引用类型实例化的泛型方法Mono 运行时早已支持泛型共享generic sharing只编译方法的一个版本所有以引用类型进行的实例化都复用它。例如Array.Sortstring与Array.Sortobject在运行时实际是同一个原生方法。之所以可行根本原因在于所有引用类型在底层表示上大小相同——恰好 1 个机器字1 word即一个指针大小。无论T具体是string、object还是某个类传参、返回、赋值都按一个指针宽度处理代码生成无需区分具体类型。值类型为什么不行将泛型共享扩展到值类型会遇到本质性困难。考虑文档中的swap方法void swapT (T[] a, int i, int j) { var t a [i]; a [i] a [j]; a [j] t; }T的大小只有到运行时才知道——int是 4 字节Guid是 16 字节一个包含多个字段的结构体可能是几十字节甚至更大。因此不知道要为局部变量t分配多少栈空间不知道第一个赋值语句a[i] - t需要拷贝多少字节内存。更复杂的是方法签名中包含类型参数的情况public T return_tT (T t) { return t; }此时方法的原生调用签名依赖于类型参数。调用方可能以return_tint (1)调用int放在一个寄存器传入返回值从返回寄存器取也可能以某个结构体实例化参数按值分布在寄存器和/或栈上而结构体返回值通过一个额外的隐藏参数传入内存地址来返回。同一个方法需要适配截然不同的调用约定。两类关键类型术语在深入实现前先明确文档定义的两类类型gsharedvt 类型gsharedvt types类型变量或使用类型变量实例化的泛型实例如T[]、ListT可变类型variable types其大小依赖类型变量、只有运行时才知道的类型。MonoType层面有对应的判定函数全部声明在 mini.hmini_is_gsharedvt_type/mini_is_gsharedvt_klass判断是否为 gsharedvt 类型mini_is_gsharedvt_variable_type/mini_is_gsharedvt_variable_klass判断是否为可变大小未知类型mini_is_gsharedvt_sharable_method判断方法是否可被 gsharedvt 共享mini_is_gsharedvt_signature/mini_is_gsharedvt_variable_signature判断签名中是否包含可变类型。基本实现方法内部如何处理可变大小的局部变量由于可变类型的大小仅在运行时可知编译器无法在编译期为它们分配静态栈槽static stack slot。gsharedvt 的解决方案是在方法入口用localloc动态分配一个本地变量区域locals area需要时动态计算变量地址。MonoGSharedVtMethodRuntimeInfo 与 rgctx运行时所需的信息存放在MonoGSharedVtMethodRuntimeInfo结构中该结构存放在一个 rgctxruntime generic context槽中。其定义位于 mini.h/* This is used by gsharedvt methods to allocate locals and compute local offsets */ typedef struct { int locals_size; /* * The results of resolving the entries in MOonGSharedVtMethodInfo-entries. * We use this instead of rgctx slots since these can be loaded using a load instead * of a call to an rgctx fetch trampoline. */ gpointer entries [MONO_ZERO_LEN_ARRAY]; } MonoGSharedVtMethodRuntimeInfo;其中locals_size是该实例化下所有可变局部变量的总大小字节entries数组则是MonoGSharedVtMethodInfo中各 rgctx 条目解析后的结果——注释特别说明之所以把解析结果直接缓存在这个结构里而不是每次访问 rgctx 槽是因为通过一次内存加载就能取得数据而无需调用 rgctx fetch trampoline这本身就是一项性能优化。方法开头的初始化伪代码如下info_var rgctx_fetch(METHOD GSHAREDVT INFO) locals_var localloc (info_var-locals_size)每次需要某个可变大小局部变量的地址时用基址加偏移计算locals_var info_var-locals_offsets [local idx]对应到真实结构体locals_offsets即由entries数组承载的偏移信息。赋值与拷贝memset memcpy由于大小未知局部变量的初始化统一使用memset拷贝统一使用memcpy拷贝字节数从 rgctx 获取。文档中T a b;被编译为a_addr locals_var info_var-locals_offsets [a idx] b_addr locals_var info_var-locals_offsets [b idx] size rgctx_fetch(T size) memcpy(a_addr, b_addr, size)与此对应源码中 rgctx 信息类型包含了一组专门服务于值拷贝的条目。在 mini-generic-sharing.c 的instantiate_info()中可以看到MONO_RGCTX_INFO_ARRAY_ELEMENT_SIZE数组元素大小、MONO_RGCTX_INFO_VALUE_SIZE值类型大小、MONO_RGCTX_INFO_CLASS_SIZEOF类型实例大小、MONO_RGCTX_INFO_MEMCPY内存拷贝函数指针、MONO_RGCTX_INFO_BZERO清零函数指针等条目——它们正是memcpy/memset运行时参数的来源。用这种方式编译出来的方法被称为gsharedvt 方法。调用 gsharedvt 方法by-ref 参数与 trampoline 链不同的调用约定签名中包含可变类型的 gsharedvt 方法使用不同的调用约定gsharedvt 参数一律按引用by ref传递。文档中的例子foo(int,int,int,T)实际调用时被改写为foo(int,int,int,T)返回值则复用大型结构体返回值的约定通过一个隐藏参数传入一块内存区域的地址方法把返回值写入该区域。这里立即产生一个约定鸿沟普通方法如调用方使用具体类型签名调用即return_tint (1)而 gsharedvt 被调方期望的是return_tint (int)这类签名。当两者相遇就必须在两种调用约定之间做转换。gsharedvt trampoline 与 arg trampoline约定转换过程非常底层且架构相关通常涉及寄存器、栈槽中数值的重排由名为gsharedvt trampoline的跳板完成。trampoline 接收一个 info 结构该结构描述了调用方与被调方的调用约定以及两者之间转换所需的步骤。但调用方并不会主动传入这个 info 结构因此需要**另一个 trampolinegsharedvt arg trampoline**负责把 info 结构传给 gsharedvt trampoline。于是一次调用形成三级链caller - gsharedvt arg trampoline - gsharedvt trampoline - callee反向亦然当调用方是 gsharedvt 方法、被调方是普通方法时同样需要这条链。info 结构包含完成参数搬移与发起调用所需的全部信息被调方地址callee address传给被调方的 rgctx寄存器与栈槽的映射关系a mapping for registers and stack slots当前是 in 还是 out 场景其他辅助信息。ARM 上 return_t 的完整调用序列文档以 ARM 架构为例给出了return_tint的逐步调用序列这是理解 trampoline 机制的最佳案例。先明确约定差异调用方参数放r0期望返回值也在r0gsharedvt 被调方通过r1接收 int 值的地址通过r0接收值类型返回地址。完整序列如下调用方把值1放入r0发起调用控制流进入 trampoline 代码trampoline 基础设施检测到该调用需要 gsharedvt trampoline计算携带调用约定信息的 info 结构并为其创建 gsharedvt arg trampolinegsharedvt arg trampoline 被调用它调用 gsharedvt trampoline并把 info 结构作为参数传入trampoline 分配一个新的栈帧以及一块 1 字1 word大小的区域用于存放返回值它从r0接收参数值存入自己的某个栈槽并把该栈槽的地址放入r1把返回值区域的地址放入r0调用 gsharedvt 方法方法把r1指向的内存拷贝到r0指向的内存然后返回 trampolinetrampoline 从返回值区域加载返回值到r0返回调用方调用方在r0中收到返回值。in wrapper 与 out wrapper出于异常处理exception handling的考虑运行时为 gsharedvt trampoline 创建了包装方法wrapper method这样 trampoline 会出现在堆栈跟踪stack trace中展开unwind代码也能正常穿过它。包装分两种in wrapper处理调用方使用可变签名调用 gsharedvt 方法的场景即外部世界进入 gsharedvt 世界out wrapper处理调用方使用可变签名调用普通方法的场景即从 gsharedvt 世界出去。文档后续统一用 wrapper 一词指代 gsharedvt arg trampoline。从 gsharedvt 方法中发出调用使用非可变签名的普通调用这类调用没有约定鸿沟按常规方式处理即可。使用可变签名的直接调用这类调用有两个问题被调方最终可能是 gsharedvt 方法也可能是非 gsharedvt 方法——前者不需要 wrapper后者需要wrapper 对不同实例化要做不同的事情这意味着调用点无法被 patch 成直接跳转到某个 wrapper因为 wrapper 与单一实例化绑定。解决方案是通过 rgctx 条目发起间接调用。rgctx 条目的解析器代码resolver确定需要哪种 wrapper并把 rgctx 条目 patch 为 wrapper 的地址——这样同一 gsharedvt 方法内后续相同实例化的调用就能直接跳转到 wrapper无需再次解析。这就是 mini-generic-sharing.c 中instantiate_info()的核心职责之一文档明确标注该函数包含处理从 gsharedvt 方法经由 rgctx 条目发起调用的代码配合 mini-trampolines.c 中的mini_add_method_trampolines()负责处理从普通方法调用 gsharedvt 方法。使用可变签名的虚调用虚方法virtual method额外复杂每个方法在 vtable 中只有一个槽位而这个槽位可能同时被普通代码和 gsharedvt 代码调用。解决方案是当虚方法以 gsharedvt 方式编译时为它包一个in wrapper并把wrapper 的地址放入 vtable 槽位而不是方法本体代码虚调用方gsharedvt 侧会加一个out wrapper。因此虚调用序列为caller - out wrapper - in wrapper - calleeAOT 支持把 gsharedvt 与 trampoline 固化进镜像在纯 AOT 场景下运行时机制必须提前固化。文档明确了 AOT 的处理策略对每一个泛型方法都 AOT 编译一个 gsharedvt 版本运行时若找不到某个具体实例化就回退使用该 gsharedvt 版本gsharedvt trampoline 以及一批 gsharedvt arg trampoline 会被保存进 mscorlib 的 AOT 镜像中。源码印证了这一设计。在 aot-compiler.c 中add_gsharedvt_wrappers()L511 附近负责为 gsharedvt 签名批量生成 in/out wrapper编译产物中包含名为%sgsharedvt_arg_trampolines_pageL1524-L1526与%sgsharedvt_trampolines_pageL2320-L2327的符号页面正是trampoline 固化进镜像的实现arch_emit_gsharedvt_arg_trampoline()L2950-L2960说明各架构负责在自己的 AOT 输出中生成 arg trampoline 代码。在 LLVM-only 运行时侧llvmonly-runtime.c 的mini_llvmonly_add_method_wrappers()L139 起集中体现了运行时加载方法时按caller_gsharedvt/callee_gsharedvt两种布尔组合决策是否需要包 wrapper 的逻辑——通过MONO_AOT_METHOD_FLAG_GSHAREDVT_VARIABLE标志L178从 AOT 编译产物的方法标志中判断被调方是否为可变签名的 gsharedvt 方法。实现细节源码中的关键路径共享方法的表示gshared_constraint 标志gsharedvt 版本的方法与普通 gshared 版本一样通过用类型参数膨胀方法inflating the method with type parameters来表示。两者的区别在于gsharedvt 使用匿名的泛型参数且其gshared_constraint字段被设置为指向某个值类型。运行时正是靠这个约束字段区分 gshared 与 gsharedvt 两种共享形式。关键文件与函数清单文档给出了完整的实现地图以下路径均已在本仓库 src/mono/mono/mini/ 下得到确认文件关键函数职责method-to-ir.c—gsharedvt 方法的 IR 生成主体mini-generic-sharing.cinstantiate_info()处理从 gsharedvt 方法经由 rgctx 条目发起的调用、rgctx 条目实例化mini-trampolines.cmini_add_method_trampolines()处理从普通方法到 gsharedvt 方法的调用mini-ARCH-gsharedvt.cmono_arch_get_gsharedvt_call_info()返回传给 gsharedvt trampoline 的架构相关 info 结构tramp-ARCH-gsharedvt.cmono_arch_get_gsharedvt_trampoline()、mono_aot_get_gsharedvt_arg_trampoline()创建 gsharedvt trampoline返回以架构相关方式向 gsharedvt trampoline 传入 info 结构的 arg trampoline在 CMakeLists.txt 中可以看到这些架构相关文件的实际清单mini-amd64-gsharedvt.c/mini-amd64-gsharedvt.h/tramp-amd64-gsharedvt.cmini-x86-gsharedvt.c/tramp-x86-gsharedvt.cmini-arm64-gsharedvt.c/mini-arm64-gsharedvt.h/tramp-arm64-gsharedvt.cmini-arm-gsharedvt.c/tramp-arm-gsharedvt.c对应的函数声明集中在 mini.hgboolean mono_arch_gsharedvt_sig_supported (MonoMethodSignature *sig); gpointer mono_arch_get_gsharedvt_trampoline (MonoTrampInfo **info, gboolean aot); gpointer mono_arch_get_gsharedvt_call_info (MonoMemoryManager *mem_manager, gpointer addr, MonoMethodSignature *normal_sig, MonoMethodSignature *gsharedvt_sig, gboolean gsharedvt_in, gint32 vcall_offset, gboolean calli);注意mono_arch_get_gsharedvt_call_info的参数设计同时传入normal_sig调用方的普通签名与gsharedvt_sig被调方的 gsharedvt 签名、gsharedvt_inin/out 方向、vcall_offset虚调用偏移与calli间接调用标志——这正是文档所述 info 结构需要描述两种调用约定及转换步骤的接口化表达。另外调试器侧也深度集成了 gsharedvt调试器代理在 debugger-agent.c 中处理 gsharedvt 方法的单步与断点逻辑debug-mini.c 负责 gsharedvt 方法的调试信息映射保证在包装链与动态 locals 区域存在时仍能正确调试。可能的未来工作设计文档同时列出了若干优化方向其中部分已在当前源码中有所体现寄存器分配优化将info_var和locals_var分配进寄存器减少访存减少 rgctx fetch 调用往 info 结构中放入更多信息避免频繁的 rgctx 抓取MonoGSharedVtMethodRuntimeInfo.entries缓存解析结果的设计即是这一方向的具体落地wrapper 融合gsharedvt 方法之间互相调用时当前会同时添加 out 与 in 两个 wrapper未来希望更多场景只用单个 wrapper或创建一种同时兼具 in/out 功能的通用 wrapper减少 AOT 实例化数量AOT 编译器会尝试编译运行时可能用到的每一种实例化导致大量实际不会使用的实例化占用空间。未来希望跳过部分实例化、改用其 gsharedvt 版本——尤其当 gsharedvt 版本带来的开销很小甚至没有时。总结gsharedvt 是 Mono/ .NET 运行时在无法 JIT环境iOS、WebAssembly、NativeAOT 类场景下支撑泛型代码的关键机制。它的核心思路可以概括为三点可变大小的局部变量通过localloc在方法入口动态分配 locals 区域由MonoGSharedVtMethodRuntimeInfo存放于 rgctx 槽提供大小与偏移统一用memcpy/memset操作可变签名的参数按引用传递返回值走大结构体约定普通代码与 gsharedvt 代码之间通过gsharedvt arg trampoline - gsharedvt trampoline链完成调用约定转换in/out wrapper 保证异常展开与堆栈跟踪的完整性AOT 场景下每个泛型方法预编译 gsharedvt 版本trampoline 与 arg trampoline 固化进镜像运行时按需选择具体实例化或回退到共享版本。理解这条调用链与数据结构是深入阅读mini-generic-sharing.c、mini-ARCH-gsharedvt.c与tramp-ARCH-gsharedvt.c等 JIT/LLVM 后端代码的最佳切入点也是理解现代 .NET 在 iOS/WebAssembly 等受限平台上运行时设计哲学的钥匙。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考