运行时 IL 生成(Wrappers)机制解析:.NET runtime 中 Mono 的运行时动态生成 IL 技术
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
导读:.NET 是面向云、移动、桌面与 IoT 应用的跨平台运行时,其 Mono 运行时(src/mono)在运行期间会大量动态生成 IL 方法,这类方法被称为 "wrappers"(包装器)。本文以仓库设计文档 runtime-ilgen.md 为骨架,结合 wrapper-types.h、marshal.c、method-builder-ilgen.c 等源码实现,系统讲解 wrapper 的定义、WrapperInfo 描述结构、缓存唯一性保证、泛型膨胀路径与 AOT 预编译支持,并逐一剖析 managed-to-native、runtime-invoke、delegate-invoke 等各类 wrapper 的用途。读完本文,你将理解 Mono 如何在运行时"就地"合成方法来完成互操作编组、反射调用、委托派发、同步加锁、数组访问等底层机制。
为什么运行时需要动态生成 IL
在 IL(中间语言)执行模型中,许多高层语言特性并不直接对应一条机器指令,而是需要一段额外的"胶水代码"。Mono 选择在运行时生成这些代码,而不是在 JIT 编译阶段内联展开,原因在于:
- 可复用性:同一段编组逻辑可以被成千上万个方法共享,运行时只生成一次并缓存;
- 与 JIT 解耦:生成的是标准 IL 方法,JIT 只需按常规方式编译它,无需为每种场景定制编译路径;
- AOT 友好:这些 IL 方法也可以在编译期(full-aot)被收集并预编译为原生代码。
在 Mono 运行时中,这类运行时生成的 IL 方法统一被称为wrapper(包装器)——因为它们中的一部分"包裹"着其他方法:例如 managed-to-native wrapper 会包裹被调用的原生函数,负责完成参数/返回值编组、异常处理(EH)结构搭建等职责。
每个 wrapper 方法都会在MonoMethod结构上设置wrapper_type字段,用于标识其 wrapper 类型。
源码结构总览
根据设计文档,与运行时 IL 生成相关的源码分为三组(均位于 src/mono/mono/metadata):
| 文件组 | 职责 |
|---|---|
| wrapper-types.h | 所有 wrapper 类型的枚举定义(WRAPPER(...)宏表) |
| marshal*(marshal.c、marshal.h、marshal-shared.c、marshal-lightweight.c) | 生成各类 wrapper 的函数族 |
| method-builder*(method-builder.c、method-builder-ilgen.c 等) | 运行时创建新 IL 方法/代码的低层设施 |
wrapper 类型枚举
wrapper-types.h 是定义 wrapper 类型的唯一权威来源,其开头的注释耐人寻味:
/* * NOTE NOTE NOTE * No additional wrapper types should be added. * If a new wrapper is absolutely necessary, an existing one needs * to be removed first (with all the change that implies). */该文件通过WRAPPER(name, string)宏定义枚举成员,完整清单如下(对应 wrapper-types.h):
WRAPPER(NONE, "none") WRAPPER(DELEGATE_INVOKE, "delegate-invoke") WRAPPER(DELEGATE_BEGIN_INVOKE, "delegate-begin-invoke") WRAPPER(DELEGATE_END_INVOKE, "delegate-end-invoke") WRAPPER(RUNTIME_INVOKE, "runtime-invoke") WRAPPER(NATIVE_TO_MANAGED, "native-to-managed") WRAPPER(MANAGED_TO_NATIVE, "managed-to-native") WRAPPER(MANAGED_TO_MANAGED, "managed-to-managed") WRAPPER(SYNCHRONIZED, "synchronized") WRAPPER(DYNAMIC_METHOD, "dynamic-method") WRAPPER(CASTCLASS, "castclass") WRAPPER(STELEMREF, "stelemref") WRAPPER(UNBOX, "unbox") WRAPPER(WRITE_BARRIER, "write-barrier") WRAPPER(OTHER, "other") WRAPPER(ALLOC, "alloc")可见 wrapper 类型是一个受控的、封闭的集合,新增类型需要先移除既有类型,以保证运行时与 AOT 编译器、调试器之间协议稳定。
method-builder:低层 IL 构造设施
MonoMethodBuilder 是运行时构建 IL 方法的核心抽象,它通过回调表MonoMethodBuilderCallbacks与具体实现解耦:
typedef struct _MonoMethodBuilder MonoMethodBuilder; typedef struct { MonoMethodBuilder* (*new_base) (MonoClass *klass, MonoWrapperType type, gboolean dynamic); void (*free) (MonoMethodBuilder *mb); MonoMethod* (*create_method) (MonoMethodBuilder *mb, MonoMethodSignature *signature, int max_stack); } MonoMethodBuilderCallbacks;new_base:在指定类上新建一个 method builder,并标明 wrapper 类型与是否动态;free:释放 builder;create_method:根据签名与max_stack(最大栈深度)把已生成的 IL 固化为一个MonoMethod。
IL 版本的实现位于 method-builder-ilgen.c,其中有一个值得注意的约定(见 method-builder-ilgen.c 与 method-builder-ilgen-internals.h):
// note: idx 1 is the wrapper info (see mono_marshal_get_wrapper_info) /* placeholder for the wrapper always at index 1, see mono_marshal_set_wrapper_info */即方法数据(method_data)数组的index 1 固定存放 wrapper 的WrapperInfo,这是下文mono_marshal_get_wrapper_info能直接读取的前提。
WrapperInfo:wrapper 的身份描述
每个 wrapper 都关联一个WrapperInfo结构,用于描述该 wrapper 的附加信息。该结构的定义位于 marshal.h:
/* * This structure contains additional information to uniquely identify a given wrapper * method. It can be retrieved by mono_marshal_get_wrapper_info () for certain types * of wrappers, i.e. ones which do not have a 1-1 association with a method/class. */ typedef struct { WrapperSubtype subtype; union { RuntimeInvokeWrapperInfo runtime_invoke; StringCtorWrapperInfo string_ctor; ElementAddrWrapperInfo element_addr; VirtualStelemrefWrapperInfo virtual_stelemref; NativeToManagedWrapperInfo native_to_managed; ManagedToNativeWrapperInfo managed_to_native; SynchronizedWrapperInfo synchronized; SynchronizedInnerWrapperInfo synchronized_inner; GenericArrayHelperWrapperInfo generic_array_helper; ICallWrapperInfo icall; ArrayAccessorWrapperInfo array_accessor; AllocatorWrapperInfo alloc; UnboxWrapperInfo unbox; GsharedvtWrapperInfo gsharedvt; DelegateInvokeWrapperInfo delegate_invoke; InterpInWrapperInfo interp_in; AOTInitWrapperInfo aot_init; LLVMFuncWrapperInfo llvm_func; NativeFuncWrapperInfo native_func; UnsafeAccessorWrapperInfo unsafe_accessor; } d; } WrapperInfo;结构由两部分构成:
WrapperSubtype subtype:wrapper 的子类型。一些 wrapper 存在多种变体,需要通过子类型进一步区分。子类型枚举同样在 marshal.h 定义,例如WRAPPER_SUBTYPE_RUNTIME_INVOKE_NORMAL(普通反射调用)、WRAPPER_SUBTYPE_DELEGATE_INVOKE_VIRTUAL/BOUND(虚拟/绑定委托调用)、WRAPPER_SUBTYPE_GSHAREDVT_IN_SIG/OUT_SIG(共享泛型值类型签名)、WRAPPER_SUBTYPE_INTERP_IN、WRAPPER_SUBTYPE_AOT_INIT、WRAPPER_SUBTYPE_LLVM_FUNC、WRAPPER_SUBTYPE_UNSAFE_ACCESSOR等。union d:按子类型存放的具体信息,例如:NativeToManagedWrapperInfo记录被调用的method与klass;ElementAddrWrapperInfo记录多维数组的rank(维数)与elem_size(元素大小);AllocatorWrapperInfo记录 GC 名称gc_name与分配类型alloc_type;GenericArrayHelperWrapperInfo记录目标klass、helper 名称与method;UnsafeAccessorWrapperInfo记录UnsafeAccessor的kind、被访问成员名member_name与方法。
写入与读取
WrapperInfo的读写由 marshal.c 中的一对函数完成:
- mono_marshal_set_wrapper_info:把
WrapperInfo写入方法的method_data[1]槽位。值得注意的是它对MONO_WRAPPER_NONE与MONO_WRAPPER_DYNAMIC_METHOD两种类型直接返回——这两类方法不携带WrapperInfo(详见下文 Dynamic-method 一节)。 - mono_marshal_get_wrapper_info:以
mono_method_get_wrapper_data (wrapper, 1)读取该结构,并断言wrapper_type非零:
WrapperInfo* mono_marshal_get_wrapper_info (MonoMethod *wrapper) { g_assert (wrapper->wrapper_type); return (WrapperInfo *)mono_method_get_wrapper_data (wrapper, 1); }- mono_wrapper_info_create:从方法所在 image 的内存分配器中分配
WrapperInfo并初始化subtype,wrapper 元数据与被包装方法同生命周期,随 image 一起卸载。
wrapper 的宿主类
wrapper 方法需要"挂靠"在某个类上。为什么不能统一放进 mscorlib 的某个类?get_wrapper_target_class 的注释给出了三条设计考量:
- wrapper 引用了元数据(签名),因此必须放入与被包装方法相同的 image,保证两者一起卸载;
- 放入带类型初始化器(type initializer)的类可能触发初始化副作用,且 wrapper 是共享的,应避免;
- 放入膨胀类(inflated class)可能因 image 卸载导致类被删除而出问题。
最终结论:wrapper 被放入 image 的<Module>类(动态 image 则放入wrappers_type)。这一细节解释了 wrapper 与宿主程序集生命周期强绑定的原因。
缓存 wrapper:保证唯一性
设计文档强调:"wrappers should be unique, i.e. there should be only one instance of every wrapper"——同一 wrapper 只能存在一个实例。这是通过按 wrapper 类型划分的哈希表缓存实现的,缓存存放在MonoMemoryManager.wrapper_caches中。
在源码中,MonoWrapperCaches结构定义于 loader-internals.h,挂载在内存管理器上(同文件 loader-internals.h):
GHashTable *managed_wrapper_cache; GHashTable *native_wrapper_cache; GHashTable *native_wrapper_aot_cache; GHashTable *native_wrapper_aot_check_cache; GHashTable *unbox_wrapper_cache; ... MonoWrapperCaches wrapper_caches;marshal.c 中到处可以看到按类型取缓存、查缓存、未命中则生成并回填的模式,例如:
- delegate 相关缓存:
delegate_invoke_cache、delegate_invoke_virtual_cache、delegate_begin_invoke_cache、delegate_end_invoke_cache、delegate_bound_static_invoke_cache、delegate_abstract_invoke_cache; - runtime-invoke 缓存:
runtime_invoke_method_cache(以"方法"为键,哈希/比较函数为 wrapper_cache_method_key_hash 系列)与runtime_invoke_signature_cache(以"签名"为键,见 wrapper_cache_signature_key_hash); - native wrapper 缓存:
native_wrapper_cache、native_wrapper_aot_cache、native_wrapper_aot_check_cache; - icall 缓存:
icall_wrapper_cache。
这种"先查缓存、后生成"的两段式结构(典型的如mono_marshal_get_runtime_invoke_full中先查runtime_invoke_method_cache,再在签名缓存中查找/创建)保证了同一参数的 wrapper 全局唯一,也让 AOT 场景下能够命中编译期预生成的版本。
泛型与 wrapper 的膨胀路径
泛型实例的 wrapper 不能直接为每个实例化单独生成——那样既浪费又无法 AOT。设计文档给出了明确的膨胀(inflation)路径:
instance method -> generic method definition -> generic wrapper -> inflated wrapper即:先从泛型实例方法回溯到泛型方法定义,为定义生成通用 wrapper,再基于具体泛型上下文(MonoGenericContext)膨胀出实例 wrapper。
源码中check_generic_wrapper_cache(marshal.c)与check_generic_delegate_wrapper_cache(marshal.c)正是这条路径的实现——它们先查定义级 wrapper 缓存,再判断能否复用/膨胀。marshal.c 第 1841 行附近的注释还给出了一个具体例子:为Sort<T>生成的 wrapper 需要按泛型定义生成、再为具体实例膨胀,这是 full-aot 能正常工作的前提。
AOT 支持:编译期收集与反序列化
在 full-aot 模式下,运行时无法在运行期即时生成 IL,因此AOT 编译器会在编译期收集应用所需的全部 wrapper 并预编译,运行期直接复用。整个过程涉及WrapperInfo结构的序列化/反序列化。
源码中的对应证据包括:
use_aot_wrappers开关与 mono_marshal_use_aot_wrappers:控制是否优先使用 AOT 预生成的 wrapper;- mono_marshal_get_native_wrapper 带有
aot参数,且会先查native_wrapper_aot_cache/native_wrapper_aot_check_cache两个 AOT 缓存(marshal.c),命中即返回 AOT 版本,未命中再走 IL 生成路径; - mono_marshal_get_native_func_wrapper_aot 与
native_func_wrapper_aot_cache:为委托原生调用入口(wrapper_aot_native)提供 AOT 版本; - mono_marshal_get_aot_init_wrapper:生成模块初始化时执行的 AOT 初始化 wrapper,其子类型由
MonoAotInitSubtype枚举(marshal.h)定义,包括AOT_INIT_METHOD、AOT_INIT_METHOD_GSHARED_MRGCTX、AOT_INIT_METHOD_GSHARED_THIS、AOT_INIT_METHOD_GSHARED_VTABLE四种; - 缓存释放路径(marshal.c)同时清理
native_wrapper_aot_cache等,印证 AOT 缓存是 wrapper 缓存体系的一等公民。
一句话总结:AOT 场景下,wrapper 的"生成"从运行期前移到编译期,运行期只负责查表命中。
Wrapper 类型逐一解析
设计文档按用途把 wrapper 分为若干大类,下面结合源码逐一展开。
Managed-to-native(托管到原生)
职责:发起对原生代码的调用。它们负责编组参数与返回值(如字符串编码转换、指针/结构体转换)、搭建 EH 异常处理结构等。
典型入口:mono_marshal_get_native_wrapper 系列。marshal.c 中大量mono_marshal_get_*_conv函数(如 mono_marshal_get_string_to_ptr_conv、mono_marshal_get_ptr_to_string_conv)负责把编组策略翻译为具体 IL 发射序列。P/Invoke 调用的完整 IL 生成在 marshal-lightweight.c 的emit_native_icall_wrapper_ilgen中实现,它支持check_exceptions(是否检查异常)与aot两种模式。
Native-to-managed(原生到托管)
职责:让原生代码能够回调托管方法。当委托被传给原生代码时,原生侧拿到的是一个 native-to-managed wrapper。
其描述结构 NativeToManagedWrapperInfo 记录委托目标method与声明类klass。原生代码通过该 wrapper 进入托管世界,由 wrapper 完成从原生调用约定到托管调用约定的转换(包括异常处理、GC 安全点等)。
Delegate-invoke(委托调用)
用途:处理 JIT 快速路径(fastpath)无法覆盖的更复杂的委托调用场景。
JIT 对常见的委托调用会生成快速内联路径;当调用涉及虚拟分发、绑定静态方法首参、抽象方法等复杂情形时,则回退到 delegate-invoke wrapper。源码中由 mono_marshal_get_delegate_invoke、mono_marshal_get_delegate_invoke_internal(支持callvirt、绑定首参、目标方法等参数)与 mono_marshal_get_delegate_invoke_subtype 生成,子类型包括:
WRAPPER_SUBTYPE_DELEGATE_INVOKE_VIRTUAL:虚拟方法委托;WRAPPER_SUBTYPE_DELEGATE_INVOKE_BOUND:绑定静态方法首参的委托。
委托的BeginInvoke/EndInvoke(异步调用模型)也有独立 wrapper:mono_marshal_get_delegate_begin_invoke 与 mono_marshal_get_delegate_end_invoke,对应DELEGATE_BEGIN_INVOKE/DELEGATE_END_INVOKE类型。
Synchronized(同步加锁)
用途:包装同步方法([MethodImpl(MethodImplOptions.Synchronized)]或 JIT 判定需要同步的方法),由 wrapper 负责加锁/解锁。
描述结构 SynchronizedWrapperInfo 与 SynchronizedInnerWrapperInfo 分别对应外层同步 wrapper 与其内层方法 wrapper——外层做锁获取/释放,内层执行真实方法体。
Runtime-invoke(反射调用)
用途:实现 mono_runtime_invoke(含virtual_变体)——即反射场景下按MonoMethod+ 参数数组进行调用。
其描述结构 RuntimeInvokeWrapperInfo 记录目标method与签名sig。由于反射调用需要把void **args参数数组拆箱/装箱到具体签名,wrapper 成为"签名适配器"。源码提供了多种获取途径:
- mono_marshal_get_runtime_invoke:按方法获取;
- mono_marshal_get_runtime_invoke_for_sig:仅按签名获取(用于不需要具体方法的场景);
- mono_marshal_get_runtime_invoke_dynamic:动态方法的反射调用专用。
缓存上区分方法键缓存(runtime_invoke_method_cache)与签名键缓存(runtime_invoke_signature_cache、runtime_invoke_sig_cache),前者优先,后者兜底,兼顾精确性与缓存命中率。
Dynamic-method(动态方法)
说明:这类方法并不是真正的 wrapper,而是用户代码通过DynamicMethod类创建的方法。
关键区别:它们没有关联的WrapperInfo结构。这与mono_marshal_set_wrapper_info中对MONO_WRAPPER_DYNAMIC_METHOD直接 return 的断言逻辑完全一致(marshal.c)——动态方法由用户完全控制 IL,运行时无需(也无法)为其维护 wrapper 描述。
Alloc(分配器)
用途:SGEN 分代 GC 的分配器方法。
描述结构 AllocatorWrapperInfo 记录gc_name与alloc_type。这类 wrapper 在 sgen-mono.c 中生成(同文件第 296 行调用mono_marshal_set_wrapper_info),是 GC 分配快速路径的组成部分。
Write-barrier(写屏障)
用途:SGEN 写屏障方法。
在分代 GC 中,向托管堆写入引用时必须维护卡表(card table)等屏障结构,write-barrier wrapper 封装了这些写入逻辑。
Castclass(类型转换)
用途:实现复杂的类型转换(cast)。
当 JIT 无法用快速路径完成类型检查时(例如接口转换、带可变泛型参数的转换),回退到 castclass wrapper 执行完整的分派逻辑。
Stelemref(数组元素写入)
用途:实现stelem.ref(向引用类型数组写元素)的复杂场景。
marshal.h 定义了精细的 stelemref 子策略枚举,展示了按运行时类型信息做渐进式检查的优化思路:
typedef enum { STELEMREF_OBJECT, /* no check at all */ STELEMREF_SEALED_CLASS, /* check vtable->klass->element_type */ STELEMREF_CLASS, /* only the klass->parents check */ STELEMREF_CLASS_SMALL_IDEPTH, /* like STELEMREF_CLASS but without the idepth check */ STELEMREF_INTERFACE, /* interfaces without variant generic arguments */ STELEMREF_COMPLEX, /* arrays, MBR or types with variant generic args - go straight to icalls */ ... } StelemrefKind;从"完全不检查"(STELEMREF_OBJECT)到"直接走 icall"(STELEMREF_COMPLEX),运行时按目标类型特性选择最省成本的检查策略。另有VirtualStelemrefWrapperInfo(记录kind)支撑虚接口场景的变体。
Unbox(拆箱)
用途:调用值类型方法前对 receiver(this 参数)进行拆箱。
描述结构 UnboxWrapperInfo 记录目标method。当以装箱对象形式调用值类型方法时,wrapper 先完成拆箱再转入真实方法。
Managed-to-managed / other(托管到托管及其他)
其余 wrapper 归入MANAGED_TO_MANAGED或OTHER大类,通过WrapperSubtype子类型进一步区分。设计文档列举了以下子类型:
String-ctor(字符串构造)
用途:实现字符串构造函数。第一个参数被忽略,直接分配一个新字符串。
描述结构 StringCtorWrapperInfo 记录method。字符串构造在运行时被视为"分配 + 填充",首个显式参数(通常是某种指针/引用)不参与常规构造语义。
Element-addr(元素取址)
用途:实现多维数组的ldelem.a(取元素地址)。
描述结构 ElementAddrWrapperInfo 记录rank(维数)与elem_size(元素大小),wrapper 据此计算多维索引偏移。
Generic-array-helper(泛型数组辅助)
用途:实现数组上的隐式接口(如IList<T>等)。wrapper 会委托给Array类上的 helper 方法。
描述结构 GenericArrayHelperWrapperInfo 记录目标klass、helper 名name与method。CLR 规定T[]隐式实现IList<T>、IReadOnlyList<T>等接口,这些接口方法需要按具体元素类型派发到Array类的对应实现。
Structure-to-ptr / Ptr-to-structure(结构体与指针互转)
用途:
- Structure-to-ptr:实现
Marshal.StructureToPtr; - Ptr-to-structure:实现
Marshal.PtrToStructure。
这类 wrapper 负责在托管结构体与原生内存布局之间做按字段编组(blittable 快速复制或逐字段转换),是System.Runtime.InteropServices内存互操作能力的底层支撑。
小结:wrapper 机制的设计闭环
将设计文档与源码对照,可以勾勒出 Mono 运行时 IL 生成的完整设计闭环:
- 描述:每个 wrapper 由
WrapperInfo(marshal.h)唯一描述,通过mono_marshal_set/get_wrapper_info读写,固定在方法数据槽位 1; - 生成:marshal 系列函数基于
MonoMethodBuilder(method-builder.h)发射 IL,由mono_mb_create_method固化为MonoMethod; - 唯一性:所有 wrapper 按类型进入
MonoWrapperCaches(loader-internals.h),先查后建,保证单实例; - 泛型:走"实例方法 → 泛型定义 → 定义级 wrapper → 膨胀实例"路径,兼顾复用与 AOT;
- AOT:编译期收集、序列化
WrapperInfo并预编译,运行期通过*_aot_cache直接命中。
理解了这一套机制,也就理解了 .NET 互操作、反射、委托与数组抽象这些高层能力在 Mono 运行时底层的真实落地方式。如需深入某个环节,建议从 marshal.c 的各类mono_marshal_get_*函数入手,沿着WrapperInfo的读写与缓存命中路径逐步阅读。
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考