
开发工具【免费下载链接】HarmonyA library for patching, replacing and decorating .NET and Mono methods during runtime项目地址https://gitcode.com/gh_mirrors/ha/Harmony点击查看免费下载Harmony 是一套面向 .NET/Mono 的运行时补丁库而CodeMatcher是它提供给 TranspilerIL 改写器最强大的工具之一它以游标的方式在 IL 指令序列中移动通过匹配方法定位指令片段再执行插入、移除或替换操作。本文以游戏 Mod 场景中替换Kill()调用为事件 API的真实案例为主线完整讲解CodeMatcher、CodeMatch与Code的匹配机制、失败处理策略以及Repeat循环改写方法并结合 CodeMatcher 源码 与单元测试深入其底层实现帮助你写出能够在游戏/应用更新后依然稳健运行的 Transpiler。为什么需要 CodeMatcher传统 Transpiler 直接接收一个IEnumerableCodeInstruction通过手动遍历for/foreach循环逐个检查指令来完成改写。这种方式在面对找到某个方法调用并替换这类需求时非常繁琐你需要自行维护索引、比较操作码与操作数、小心处理位置偏移。CodeMatcher正是为解决这类问题而设计。正如其在 源码 中的注释所示它是一个 A CodeInstruction matcher。它内部持有一份指令列表codes构造时从传入的instructions深拷贝而来一个当前游标位置Pos初始值为-1越界状态上一次匹配的结果状态与错误信息。你可以把它想象成一个可前进、可后退、可回卷的光标先用Match...()/Search...()系列方法找到目标指令再用Insert...()/Remove...()/Set...()等方法就地改写最后通过Instructions()把修改后的指令序列交还给 Harmony 完成补丁注入。如果你还不熟悉 Transpiler 的基础形态参数约定、执行时机、链式执行顺序建议先阅读仓库中的 Transpiler 入门文档。匹配的定义CodeMatch 与 Code要定位 IL 中的特定片段仅靠CodeMatcher还不够你还需要描述要找什么。Harmony 提供两类描述工具CodeMatch一条匹配规则用于匹配单个或连续多个指令Code一组预定义的CodeMatch静态属性可视为CodeMatch的语法糖。CodeMatch规则的结构从 CodeMatch 源码 可以看出一条匹配规则由以下可选约束组合而成字段含义说明name匹配名称匹配成功后可通过CodeMatcher.NamedMatch(name)反查该位置的指令opcodeSet允许的操作码集合命中指令的opcode必须属于该集合operands允许的操作数集合命中指令的operand必须等于其中任意一个predicate自定义谓词直接对CodeInstruction求值返回bool优先于其他约束labels/blocks标签 / 异常块约束指令必须携带对应标签或异常块jumpsFrom/jumpsTo跳转关系约束基于指令索引描述跳转来源/目标匹配判定逻辑位于 CodeMatch.Matches()先看predicate若有则直接返回谓词结果否则依次校验opcodeSet、operands、labels、blocks、跳转约束全部通过才算命中。常用构造方式// 1. 直接指定操作码与操作数 new CodeMatch(OpCodes.Call, myMethodInfo) // 2. 通过表达式定位调用某方法的指令推荐最直观 CodeMatch.Calls(() default(DamageHandler).Kill(default)) // 3. 常用语义化工厂方法 CodeMatch.LoadsConstant(42); // 加载常量 42 CodeMatch.LoadsConstant(someStr); // 加载字符串 CodeMatch.LoadsField(myField); // 加载字段 CodeMatch.StoresField(myField); // 存储字段 CodeMatch.IsLdarg(0); // 任何形式的 Ldarg*参数 0 CodeMatch.IsLdloc(); // 任何形式的 Ldloc* CodeMatch.Branches(); // 任何分支指令其中Calls(...)的实现很有趣见 CodeMatch.cs它把CodeInstructionExtensions.opcodesCalling即Call/Callvirt等调用类操作码的集合合并进opcodeSet再通过SymbolExtensions.GetMethodInfo(expression)从 lambda 表达式中解析出目标MethodInfo作为操作数——这意味着编译器会对操作码与操作数做双重校验。Code更简洁的匹配书写Code.cs 的设计目的是让匹配代码更接近真实 IL 的写法。加入using static HarmonyLib.Code;之后// 等价于 new CodeMatch(OpCodes.Ldarg_1) Ldarg_1 // 等价于 new CodeMatch(OpCodes.Call, myMethodInfo) Call[myMethodInfo] // 还可以附加名称供 NamedMatch 反查 Call[myMethodInfo, killCall]这类预定义属性覆盖了绝大多数常用 IL 指令Ldarg_*、Ldloc_*、Ldc_I4_*、Call、Ret、各类分支指令等在需要编写指令序列模板时能大幅压缩代码量。仓库测试 TestCodeMatcher.cs 验证了Ldc_I4_0与Call[method]两种写法与手写CodeMatch的等价性。实战场景替换 Kill() 调用为事件 API原文档给出了一个典型的多层 API 场景一个为 Mod 提供事件的游戏 API 中基类DamageHandler负责伤害与死亡动画其虚方法Apply()内部会调用Kill()。Mod 想在角色死亡时触发OnDeath事件但不能直接补丁Kill()——因为这个方法还被其他 API 方法复用直接打补丁会导致无关流程也触发事件。正确做法在Apply()的 Transpiler 中找到Kill()的调用点把它替换为 Mod 自己的MyDeathHandler()方法调用。这样事件只会在Apply()路径上触发其他调用方不受影响。以下完整代码来自仓库示例 patching-transpiler-codematcher.cs其中TargetMethods()用于覆盖所有DamageHandler.Apply的派生实现可参见文档辅助方法一节。用法一MatchStartForward ThrowIfInvalid 的硬校验写法[HarmonyPatch] public static class DamageHandler_Apply_Patch { static IEnumerableMethodBase TargetMethods() { var result new ListMethodBase(); // ... (targeting all DamageHandler.Apply derived) return result; } static void MyDeathHandler(DamageHandler handler, Player player) { // ... } static IEnumerableCodeInstruction Transpiler(IEnumerableCodeInstruction instructions /*, ILGenerator generator*/) { // 注意如需在改写中创建 Label请把 ILGenerator generator 传入构造参数 var codeMatcher new CodeMatcher(instructions /*, ILGenerator generator*/); codeMatcher.MatchStartForward( CodeMatch.Calls(() default(DamageHandler).Kill(default)) ) .ThrowIfInvalid(Could not find call to DamageHandler.Kill) .RemoveInstruction() .InsertAndAdvance( CodeInstruction.Call(() MyDeathHandler(default, default)) ); return codeMatcher.Instructions(); } }逐段解读new CodeMatcher(instructions)从 Transpiler 参数构造匹配器。源码 CodeMatcher 构造函数 会对每条指令做浅拷贝确保后续修改不影响原始列表。generator可选若后续需要DefineLabel/CreateLabel/DeclareLocal等生成器能力必须传入。MatchStartForward(...)从当前游标位置向前搜索匹配序列命中后把游标停在序列起始处。其底层是Match(matches, 1, false)见 CodeMatcher.cs内部通过MatchSequence从每个位置尝试连续匹配全部CodeMatch。ThrowIfInvalid(...)若匹配失败游标越界即Pos处于-1或 Length抛出InvalidOperationException异常消息为解释文字 - Current state is invalid见 源码。这是快速失败策略游戏更新导致 IL 结构变化时第一时间用异常暴露问题。RemoveInstruction()删除游标当前指向的指令即Kill()调用见 源码。InsertAndAdvance(...)在当前位置插入指令并把游标向后移动见 源码。这里插入CodeInstruction.Call(() MyDeathHandler(default, default))与CodeMatch.Calls一样通过 lambda 表达式解析出方法引用保持参数个数、顺序一致的栈平衡。return codeMatcher.Instructions()返回改写后的指令列表由 Harmony 写入最终补丁方法。注意栈平衡Kill(player)与MyDeathHandler(handler, player)的入参顺序需要一致这里都假设调用前栈上已有DamageHandler实例与Player参数否则生成的 IL 会因栈不匹配而验证失败。用法二ThrowIfNotMatchForward——把匹配 校验合并成一步原文档特别指出ThrowIfNotMatchForward本质上是MatchStartForward与ThrowIfInvalid的连续调用合并。看 源码实现private void ThrowIfNotMatch(string explanation, int direction, CodeMatch[] matches) { _ ThrowIfInvalid(explanation); var tempPos Pos; try { if (Match(matches, direction, false).IsInvalid) throw new InvalidOperationException(explanation - Match failed); } finally { Pos tempPos; } }它先检查当前状态是否有效然后尝试匹配无论成败都会恢复游标位置失败时抛出解释文字 - Match failed的异常。配套的还有反向的ThrowIfNotMatchBack向起点方向校验和针对当前指令的ThrowIfNotMatch。对应改写后的示例codeMatcher.ThrowIfNotMatchForward(Could not find call to DamageHandler.Kill, CodeMatch.Calls(() default(DamageHandler).Kill(default)) ) .RemoveInstruction() .InsertAndAdvance( CodeInstruction.Call(() MyDeathHandler(default, default)) );相比用法一这段代码更紧凑且失败异常消息区分状态无效与匹配失败两种情形便于定位问题。测试用例 Test_MatchStartForward_Code 展示了MatchStartForwardThrowIfNotMatch的惯用组合并断言游标最终落在Call mBar指令上。用法三IsValid Start()——宽容的可选项改写ThrowIf...系列适合匹配必须存在的场景。但原文档指出在真实项目中并非所有DamageHandler.Apply的派生实现都会调用Kill()。此时用抛异常的策略会导致部分合法方法补丁失败。匹配失败时游标Pos会被推进到列表末尾越界Pos Length此时IsValid为false、IsInvalid为true见 源码 与SetOutOfBounds逻辑。因此可以这样软检查var codeMatcher new CodeMatcher(instructions); codeMatcher.MatchStartForward( CodeMatch.Calls(() default(DamageHandler).Kill(default)) ); if (codeMatcher.IsValid) { codeMatcher.RemoveInstruction() .InsertAndAdvance( CodeInstruction.Call(() MyDeathHandler(default, default)) ); } codeMatcher.Start(); // Other match... return codeMatcher.Instructions();要点IsValid判断当前游标是否在[0, Length)内匹配失败即为false因此if块内的改写会被跳过跳过改写后必须调用Start()见 源码Pos 0把游标回卷到指令开头否则后续的Match.../Search...会从列表末尾这个越界位置继续导致无法命中任何内容这种模式适合匹配到就增强、匹配不到就原样返回的防御式补丁能显著提升 Mod 在游戏更新后的健壮性。此外ReportFailure(MethodBase method, Actionstring logger)见 源码提供了非异常的失败上报通道IsValid为假时把错误信息优先使用上次匹配的错误否则 Unexpected code写入日志并返回true。如果你不想让补丁因异常中断、只想记录警告用它比ThrowIfInvalid更合适。原文档也提醒ThrowIfInvalid与ReportFailure这类显式校验能够帮助你在上游代码升级后准确定位哪个位置、哪条补丁需要修订。用法四Repeat——批量改写所有匹配点如果Kill()在方法里可能出现多次例如循环、多个死亡分支就需要Repeat。它会把上一次成功的匹配操作lastMatchCall反复执行直到游标越界为止codeMatcher.MatchStartForward( CodeMatch.Calls(() default(DamageHandler).Kill(default)) ) // Only take the last Matching condition. .Repeat(matchAction: cm { cm.RemoveInstruction(); cm.InsertAndAdvance( CodeInstruction.Call(() MyDeathHandler(default, default)) ); });看 Repeat 的实现public CodeMatcher Repeat(ActionCodeMatcher matchAction, Actionstring notFoundAction null) { var count 0; if (lastMatchCall null) throw new InvalidOperationException(No previous Match operation - cannot repeat); while (IsValid) { matchAction(this); _ lastMatchCall(); count; } lastMatchCall null; if (count 0 notFoundAction ! null) notFoundAction(lastError); return this; }运行机制与使用要点必须紧跟在一次Match...()之后调用lastMatchCall只在Match(...)方法内被赋值见 源码。如果之前没有匹配操作Repeat会直接抛出InvalidOperationException(No previous Match operation - cannot repeat)。循环体执行matchAction完成改写然后再次调用上次的匹配方法寻找下一个命中点直到游标越界、IsValid变假。notFoundAction可选当一次匹配都没成功时count 0以错误消息字符串为参数调用该委托。可用于集中上报方法里根本没有 Kill 调用这类情况。注意lastError中存放的是Match方法写入的Cannot find {匹配描述}之类的文本见 CodeMatcher.cs。重要限制原文档特别强调Repeat不会重复Search...()类方法只有Match...()系列MatchStartForward/MatchEndForward/MatchStartBackwards/MatchEndBackwards可被重复。克隆陷阱如果在matchAction内部又调用了其他Match...()方法会覆盖lastMatchCall导致Repeat后续循环换了一个匹配条件。正确的做法是在 match action 里先CodeMatcher.Clone()出一个副本见 Clone 实现副本会复制游标位置与匹配状态在副本上执行额外的匹配从而保护Repeat依赖的原匹配条件。匹配失败机制与错误处理全景理解失败机制是写出稳健 Transpiler 的前提。当MatchStartForward找不到目标时Pos被置为Length越界IsValid false若此时直接调用RemoveInstruction()之类的改写方法会因codes[Pos]索引越界而抛出运行时异常——这正是要先用ThrowIfInvalid或IsValid把关的原因失败信息记录在lastError可供ReportFailure与Repeat的notFoundAction使用。CodeMatcher 还提供ThrowIfFalse(explanation, FuncCodeMatcher, bool)见 源码允许你用任意自定义谓词检查当前状态并抛出异常适合表达复杂的先决条件例如当前位置之后的第 3 条必须是 Ret。完整的改写工具箱除了匹配与校验CodeMatcher围绕游标提供了一整套操作均在 CodeMatcher.cs 中可按需查阅类别方法说明读取Instruction/InstructionAt(offset)/Instructions()/Instructions(count)/InstructionsWithOffsets(a, b)读取当前/相对偏移/区间的指令游标Advance(offset)/Start()/End()移动游标Start()回卷到 0End()移到最后一个指令搜索SearchForward(predicate)/SearchBackwards(predicate)按谓词搜索单条指令注意不参与Repeat改写SetInstruction(...)/Set(...)/SetAndAdvance(...)/SetInstructionAndAdvance(...)原地替换操作码/操作数插入Insert(...)/InsertAndAdvance(...)/InsertBranch(...)在游标处插入指令InsertBranch自动创建目标标签移除RemoveInstruction()/RemoveInstructions(count)/RemoveInstructionsInRange(a, b)/RemoveInstructionsWithOffsets(a, b)移除单条/多条/区间指令标签DefineLabel/CreateLabel/CreateLabelAt/AddLabels/SetJumpTo配合ILGenerator处理跳转局部变量DeclareLocal(Type, out LocalBuilder)配合ILGenerator声明局部变量命名匹配NamedMatch(name)通过CodeMatch的name字段反查已命中的指令改写指令时请始终牢记 IL 的栈语义RemoveInstruction()会把调用Kill()的整条指令删掉因此必须同步插入一个参数签名兼容的调用指令如MyDeathHandler保证栈上出入参数量一致、类型匹配否则补丁方法在 JIT 验证阶段会报InvalidProgramException类错误。测试验证仓库如何保证匹配行为仓库在 HarmonyTests/Tools/TestCodeMatcher.cs 中对匹配器行为做了针对性验证Test_CodeMatch验证new CodeMatch(OpCodes.Call, method)的opcode、opcodeSet、operand、operands字段均正确填充Test_Code_Without_Argument/Test_Code_With_Argument验证Code语法糖Ldc_I4_0与Call[method]与手写CodeMatch完全等价Test_MatchStartForward_Code/Test_MatchStartForward_CodeMatch以 CodeMatcherClass.cs 中的Method()内部先调用Foo()再调用Bar(hello)为被补丁方法用MatchStartForward(Call[mBar])定位Bar调用断言游标停在该Call指令上、操作数等于mBar的MethodInfo。这些测试印证了本文用法一、用法二中的核心调用链new CodeMatcher(instructions)→MatchStartForward(...)→ThrowIfNotMatch(...)并把游标落在匹配序列起始处这一语义固化成了可回归的契约。实践建议总结能改一处就不改多处Transpiler 是链式执行的多个 Mod 的 Transpiler 依次叠加改动越少、越局部与其他补丁共存的概率越高。用CodeMatch.Calls(...)这类按方法引用匹配的方式比按指令序号硬编码InstructionsAt固定偏移对上游代码升级更友好。区分硬校验与软校验ThrowIfInvalid/ThrowIfNotMatchForward适合匹配必须存在、缺失即报错的场景IsValidStart()组合适合匹配可选、缺失就原样通过的场景。ReportFailure则适合只记录不中断的降级策略。用好Repeat的三个前提紧跟Match...()调用不要在matchAction里直接嵌套其他Match...()如需额外匹配先用Clone()用notFoundAction处理零命中的情形。匹配失败后记得回卷Match...失败会让游标停在列表末尾继续后续匹配前调用Start()回到开头。保持栈平衡插入的方法调用必须与移除/替换的调用保持一致的参数入栈顺序与类型这是 IL 改写合法性的底线。如需进一步探索可查阅仓库中的 CodeMatcher 源码、CodeMatch 源码、Code 源码 以及配套示例 patching-transpiler-codematcher.cs并结合 Transpiler 文档 与 CodeMatcher API 文档 形成完整的知识闭环。赞分享开发工具【免费下载链接】HarmonyA library for patching, replacing and decorating .NET and Mono methods during runtime项目地址https://gitcode.com/gh_mirrors/ha/Harmony点击查看免费下载相关推荐使用 Sublime Merge 完成首次开源贡献first-contributions 图形化实战指南使用 Sublime Merge 完成首次开源贡献first contributions 图形化实战指南 导读 本文基于 first contribution开发工具Harmony项目中的代码匹配器(CodeMatcher)详解Harmony项目中的代码匹配器 CodeMatcher 详解 什么是CodeMatcher 在Harmony项目中CodeMatcher是处理IL代码的强大开发工具深入KeystoneJS源码它是如何巧妙封装Express与Mongoose的深入KeystoneJS源码它是如何巧妙封装Express与Mongoose的 KeystoneJS 是一个基于 Node.js 的 CMS 与 Web 应开发工具上一篇MusicFree文本按钮TextButton简洁交互设计下一篇揭秘高效智能开发工具一站式开源AI编程解决方案指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考