C# Harmony Transpiler字段引用空值异常问题排查
Harmony Transpiler字段替换异常问题分析与解决
问题场景
尝试用Harmony的Transpiler修改Boosterpack.Clicked方法,把硬编码的常量5和10替换为OneVillagerChecks类的静态字段,代码如下:
namespace DifficultyModNS { [HarmonyPatch(typeof(Boosterpack),nameof(Boosterpack.Clicked))] public class OneVillagerChecks { public static Int32 frequency = 5; public static Int32 startChecking = 10; static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { foreach (var instruction in instructions) { if (instruction.opcode == OpCodes.Ldc_I4_5) { DifficultyMod.Log("Transpiling const 5 to variable"); yield return new CodeInstruction(OpCodes.Ldfld, operand: AccessTools.Field(typeof(Int32), name: "DifficultyModNS.OneVillagerChecks::frequency")); } else if (instruction.opcode == OpCodes.Ldc_I4_S && Convert.ToInt32(instruction.operand) == 10) { DifficultyMod.Log("Transpiling const 10 to variable"); yield return new CodeInstruction(OpCodes.Ldfld, operand: AccessTools.Field(typeof(Int32), name: "DifficultyModNS.OneVillagerChecks::startChecking")); } else { yield return instruction; } } } } }
错误现象
第一条日志正常打印,但第二条日志输出后立即抛出异常:
[17:18:44] [Log : difficulty_mod] Transpiling const 5 to variable [17:18:44] [Log : difficulty_mod] Transpiling const 10 to variable ArgumentNullException: Invalid argument for ldfld NULL Parameter name: operand at HarmonyLib.Internal.Patching.ILManipulator.WriteTo (Mono.Cecil.Cil.MethodBody body, System.Reflection.MethodBase original) [0x001b4] in <4a2409e6eb214388b8d6ab81d407350d>:0 at HarmonyLib.Public.Patching.HarmonyManipulator.WriteTranspilers () [0x00084] in <4a2409e6eb214388b8d6ab81d407350d>:0 at HarmonyLib.Public.Patching.HarmonyManipulator.WriteImpl () [0x00031] in <4a2409e6eb214388b8d6ab81d407350d>:0 Rethrow as HarmonyException: IL Compile Error (unknown location) at HarmonyLib.Public.Patching.HarmonyManipulator.WriteImpl () [0x00382] in <4a2409e6eb214388b8d6ab81d407350d>:0 at HarmonyLib.Public.Patching.HarmonyManipulator.Process (MonoMod.Cil.ILContext ilContext, System.Reflection.MethodBase originalMethod) [0x00042] in <4a2409e6eb214388b8d6ab81d407350d>:0 at HarmonyLib.Public.Patching.HarmonyManipulator.Manipulate (System.Reflection.MethodBase original, HarmonyLib.PatchInfo patchInfo, MonoMod.Cil.ILContext ctx) [0x00006] in <4a2409e6eb214388b8d6ab81d407350d>:0 at HarmonyLib.Public.Patching.HarmonyManipulator.Manipulate (System.Reflection.MethodBase original, MonoMod.Cil.ILContext ctx) [0x00007] in <4a2409e6eb214388b8d6ab81d407350d>:0 at HarmonyLib.Public.Patching.ManagedMethodPatcher.Manipulator (MonoMod.Cil.ILContext ctx) [0x00012] in <4a2409e6eb214388b8d6ab81d407350d>:0 at MonoMod.Cil.ILContext.Invoke (MonoMod.Cil.ILContext+Manipulator manip) [0x00087] in <92bbbc5a09fc417ca7426fbdd31824c2>:0 at MonoMod.RuntimeDetour.ILHook+Context.InvokeManipulator (Mono.Cecil.MethodDefinition def, MonoMod.Cil.ILContext+Manipulator cb) [0x00012] in <925d74c1e4e3467a888e7df55b8370c5>:0 at (wrapper dynamic-method) MonoMod.RuntimeDetour.ILHook+Context.DMD<MonoMod.RuntimeDetour.ILHook+Context::Refresh>(MonoMod.RuntimeDetour.ILHook/Context) at (wrapper dynamic-method) MonoMod.Utils.DynamicMethodDefinition.Trampoline<MonoMod.RuntimeDetour.ILHook+Context::Refresh>?1194592624(object) at HarmonyLib.Internal.RuntimeFixes.StackTraceFixes.OnILChainRefresh (System.Object self) [0x00000] in <4a2409e6eb214388b8d6ab81d407350d>:0 at MonoMod.RuntimeDetour.ILHook.Apply () [0x00059] in <925d74c1e4e3467a888e7df55b8370c5>:0 at HarmonyLib.Public.Patching.ManagedMethodPatcher.DetourTo (System.Reflection.MethodBase replacement) [0x00047] in <4a2409e6eb214388b8d6ab81d407350d>:0 // 后续为子系统重复抛出的异常
最初疑问:同一类中相邻的两个字段,为什么一个替换成功另一个失败?
后续排查:
- 编辑1:移除失效分支的else if后,异常仍在函数退出后触发,疑问转为:字段名称存在但Ldfld的操作数返回NULL?
- 编辑2:最终找到问题并解决。
问题根源
- IL指令使用错误:要访问的是静态字段,应该用
OpCodes.Ldsfld指令(加载静态字段到栈),但代码中误用了OpCodes.Ldfld(加载实例字段,需要栈上先有类的实例引用),这会直接导致IL验证失败。 - AccessTools.Field调用错误:调用
AccessTools.Field(typeof(Int32), "DifficultyModNS.OneVillagerChecks::frequency")时,第一个参数传了字段的类型Int32,但正确的应该传字段所属的类OneVillagerChecks的Type,导致AccessTools无法找到目标字段,返回NULL,进而触发ArgumentNullException。
修正后的代码
namespace DifficultyModNS { [HarmonyPatch(typeof(Boosterpack),nameof(Boosterpack.Clicked))] public class OneVillagerChecks { public static Int32 frequency = 5; public static Int32 startChecking = 10; static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { // 直接获取字段信息,避免AccessTools的使用误区 var frequencyField = typeof(OneVillagerChecks).GetField(nameof(frequency)); var startCheckingField = typeof(OneVillagerChecks).GetField(nameof(startChecking)); foreach (var instruction in instructions) { if (instruction.opcode == OpCodes.Ldc_I4_5) { DifficultyMod.Log("Transpiling const 5 to variable"); yield return new CodeInstruction(OpCodes.Ldsfld, frequencyField); } else if (instruction.opcode == OpCodes.Ldc_I4_S && Convert.ToInt32(instruction.operand) == 10) { DifficultyMod.Log("Transpiling const 10 to variable"); yield return new CodeInstruction(OpCodes.Ldsfld, startCheckingField); } else { yield return instruction; } } } } }
总结
- 静态字段访问必须用
Ldsfld,实例字段才用Ldfld,二者不可混淆; - 获取字段信息时,要明确字段所属的类类型,而非字段本身的类型,直接用
Type.GetField更直观不易出错。
内容的提问来源于stack exchange,提问作者jmucchiello
相关产品推荐
相关产品推荐

