Harmony Library 補丁方式的最終章,Reverse Patch。
什麼是 Reverse Patch (反向補丁)
Reverse patch 核心功能是將外部原始方法(或其部分程式碼)「複製」到你自己在程式碼中定義的存根方法(stub method),使該存根方法在運行時「變成」那個原始方法。
功能
- 呼叫私有方法:可以輕鬆地呼叫其他類別中的私有方法(private methods),無需透過反射機制(reflection)。(註1)
- 取得原始邏輯:它能提供未經其他補丁(如 Prefix 或 Postfix)修改過的純淨原始方法。
- 性能優勢:與反射相比,它接近原生的執行效能。
- 擷取部分代碼:透過配合 Transpiler,你可以只從一個長方法中「挖出」某一部分的邏輯(例如某段複雜的校驗算法)並將其存入你的存根方法中獨立調用,而不需要手動複製原始碼。
- 凍結實作:它會在你執行反向補丁的那一刻「凍結」該方法的邏輯。這意味著即使之後原始方法被其他補丁修改,你的存根方法依然保持當時的版本。
註1: 如果只是要呼叫私有成員,如果使用的框架是 .NET 8 或以上,建議用 UnsafeAccessorAttribute 更方便,參考前面的文章 [UnsafeAccessor 系列] 。
兩種反向補丁
- Original(預設):取得 DLL 中定義的原始方法,完全不受任何補丁或 Transpiler 的影響。
- Snapshot(快照):獲取當時已經應用了所有現有 Transpiler 之後的版本。注意不會包含 Prefix、Postfix 或 Finalizer 的邏輯。
實作範例
照例先從 Original Class 開始,我們的目標是私有的 Calculate 方法,原來是將 baseValue*2:
public class OriginalClass
{
public static string DisplayValue(int baseValue)
{
return $"The calculated value is {Calculate(baseValue)}";
}
private static int Calculate(int baseValue)
{
return baseValue * 2;
}
}在主程式的專案中,透過 Transpiler 將 *2 修改為 *5:
[HarmonyPatch(typeof(OriginalClass), "Calculate")]
public static class PatchTarget
{
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions)
.MatchStartForward(new CodeMatch(OpCodes.Ldc_I4_2))
.SetInstruction(new CodeInstruction(OpCodes.Ldc_I4_5))
.InstructionEnumeration();
}
}這個階段實作 Original / Snapshot Reverse Patch,有一點要特別提醒,只要相關到 Snapshot Reverse,最好採用手動補丁;如果使用 annotation 補丁,極有可能會因為補丁執行順序的問題導致無法取得 Transpiler 後的結果。
補丁方法本身很簡單,只要照著原來的回傳型別與參數列表就可以,程式碼的內容一律是 throw new NotImplementedException,剩下的就交給 Harmony,反正你不論寫甚麼最後都會被 Reverse Patch 蓋掉,寫成 throw new NotImplementedException 還比較容易發現自己忘記補丁。
注意 ApplyReversePatches method,會呼叫 harmony.CreateReversePatcher(..).Patch(),在 Patch 方法中指名使用 Original 或是 Snapshot ,這是一個 optional 參數,如果不明確傳入,預設值是 HarmonyReversePatchType.Original。
public class ReversePatching
{
public static int ReverseOriginalCalculate(int baseValue) => throw new NotImplementedException("This method is a reverse patch and should not be called directly.");
public static int ReverseSnapshotCalculate(int baseValue) => throw new NotImplementedException("This method is a reverse patch and should not be called directly.");
public static void ApplyReversePatches()
{
var harmony = new Harmony("com.example.sample013.reverse");
var originalMethod = AccessTools.Method(typeof(OriginalClass), "Calculate");
var reverseOriginalMethod = AccessTools.Method(typeof(ReversePatching), nameof(ReverseOriginalCalculate));
var reverseSnapshotMethod = AccessTools.Method(typeof(ReversePatching), nameof(ReverseSnapshotCalculate));
harmony.CreateReversePatcher(originalMethod, reverseOriginalMethod).Patch(HarmonyReversePatchType.Original);
harmony.CreateReversePatcher(originalMethod, reverseSnapshotMethod).Patch(HarmonyReversePatchType.Snapshot);
}
} 主程式與執行結果如下 [範例連結Sample013]:
static void Main(string[] args)
{
var harmony = new Harmony("com.example.sample013");
harmony.PatchAll(Assembly.GetExecutingAssembly());
Console.WriteLine($"Transpiler Result: {OriginalClass.DisplayValue(100)}");
ReversePatching.ApplyReversePatches();
var reverseOriginalResult = ReversePatching.ReverseOriginalCalculate(100);
Console.WriteLine($"Reverse Original Calculate Result: {reverseOriginalResult}");
var reverseSnapshotResult = ReversePatching.ReverseSnapshotCalculate(100);
Console.WriteLine($"Reverse Snapshot Calculate Result: {reverseSnapshotResult}");
}Transpiler Result: The calculated value is 500
Reverse Original Calculate Result: 200
Reverse Snapshot Calculate Result: 500