基于C#/Mono.Cecil确定方法所有退出指令的技术问询
在Unity中实现类似PostSharp OnExit的IL织入逻辑
核心问题拆解
你遇到的本质问题是:常规的OpCodes.Ret、OpCodes.Throw/OpCodes.Rethrow只能覆盖显式返回和主动抛异常的场景,但函数还会通过异常传播、协程状态机切换、迭代器遍历结束这些隐式路径退出,这些场景不会触发上述指令,导致OnExit逻辑遗漏。
分场景IL指令处理方案
针对不同的函数退出路径,需要分别处理对应的IL结构:
1. 显式返回路径:处理所有OpCodes.Ret
这是最基础的场景,遍历方法的IL指令序列,在每一条OpCodes.Ret指令前插入OnExit逻辑。注意要保证插入的指令不破坏原栈的状态——比如如果原Ret指令前有返回值在栈上,插入的OnExit方法不能消耗或修改这个值。
2. 异常传播路径:分析Exception Handler块
当函数内部调用的方法抛出异常且未被本地catch捕获时,函数会直接跳转到上层异常处理逻辑,此时需要:
- 遍历方法的
ExceptionHandlers集合,针对每个Catch、Finally、Fault类型的Handler:- 找到Handler块内的所有退出点:包括块内的
Ret、Throw,以及跳转到块外的分支指令(比如OpCodes.Br、OpCodes.Brtrue等跳转到Handler范围外的指令) - 在这些退出点前插入OnExit逻辑
- 找到Handler块内的所有退出点:包括块内的
- 特别注意
Finally块:无论是否发生异常,Finally块都会执行,所以必须在Finally块的所有退出点插入逻辑,保证异常场景下OnExit也能触发。
3. 协程(Yield Return):处理状态机的MoveNext方法
Unity协程本质是编译器生成的状态机类(带[CompilerGenerated]属性,实现IEnumerator),原方法的逻辑会被拆分到状态机的MoveNext方法中。需要:
- 识别目标方法对应的状态机类(可通过方法的
CustomAttributes或查找引用该方法的编译器生成类) - 找到状态机类的
MoveNext方法,对该方法执行和普通方法一样的处理:遍历所有Ret指令、分析异常Handler块,插入OnExit逻辑 - 注意:协程的
yield return会触发状态机暂停,此时不算函数退出,只有当MoveNext返回false(协程结束)时的Ret才是真正的退出点,不过保险起见可以在MoveNext的所有Ret前都插入逻辑,或者判断返回值是否为false后再执行。
4. 迭代器(foreach):处理迭代器状态机
foreach依赖的迭代器同样是编译器生成的状态机(实现IEnumerator/IEnumerable),处理逻辑和协程一致:
- 找到编译器生成的状态机类及其
MoveNext方法 - 在
MoveNext方法的所有Ret和异常Handler块退出点插入OnExit逻辑
基于Mono.Cecil的实现要点
- 保留栈状态:插入OnExit方法调用时,使用
OpCodes.Call(而非Callvirt,如果OnExit是静态方法),且确保调用前后栈的元素数量和类型不变——比如如果原指令栈顶是返回值,调用OnExit后要把这个值重新放回栈顶。 - 避免循环处理:处理状态机方法时,要标记已处理的方法,避免重复织入。
- 兼容性:Unity使用的Mono版本可能有差异,测试时要覆盖不同Unity版本(比如2020+、2022+)的IL生成逻辑。
简化方案:复用现有库扩展
如果不想手动实现完整的IL分析,可以基于你找到的MewWeaver进行扩展:
- 新增对Exception Handler块的遍历逻辑
- 添加状态机方法的自动识别和处理逻辑
- 整合所有退出点的织入逻辑
内容的提问来源于stack exchange,提问作者necrokamios
相关产品推荐
相关产品推荐

