Transpiler 是 Harmony Library 的重量級功能,相較於 Prefix 或 Postfix 是在原始方法前後插入程式碼,Transpiler 直接操作 IL 指令,讓你在程式執行前就重新改寫方法本身,威力驚人。
Transpiler 的核心功能
與一般的 Prefix 或 Postfix不同,Transpiler 的本質不是在目標方法前後執行額外的代碼,而是直接修改目標方法的原始碼。
- 修改層級:它運作在 CIL (Common Intermediate Language) 層級,也就是 .NET 編譯後的「中間語言」或「指令碼」。
- 後編譯階段:你可以將它看作是一個「後編譯器(Post-compiler)」,在程式執行時,將原始方法的 IL 指令取出,經過你的邏輯修改(插入、刪除或替換指令)後,再交由 JIT 編譯器執行。
- 執行時機:它只會在補丁安裝時執行一次。一旦修改完成,之後每次呼叫該方法都會執行修改後的版本,不會有額外的執行時期(Runtime)效能損耗。
Transpiler 在執行階段(JIT 即時編譯發生之前)會修改原始方法的 IL 指令,達到類似 IL Emit 的效果,但以更安全且模組化的方式進行。相較於 Compiler(編譯器),作者選用 Transpiler 這個名稱,確實帶有『轉譯器』的意味,強調它並非重新編譯,而是對既有 IL 進行轉換。
即使 Transpiler 提供了比 IL Emit 更高階的 API,降低了直接操作 IL 的複雜度,但在使用上仍存在挑戰,例如需要熟悉 .NET IL 的語法與結構,並面臨除錯與維護上的困難。不過,相較於直接使用 IL Emit,難度確實有所降低。所幸現在有 AI 工具的輔助,學習與處理 IL 的門檻也逐漸下降。
Transpiler 的基本宣告
以下擷取文件內容 (引用自 https://harmony.pardeike.net/articles/patching-transpiler.html ):
static IEnumerable<CodeInstruction> Transpiler(<arguments>)
// or
[HarmonyTranspiler]
static IEnumerable<CodeInstruction> MyTranspiler(<arguments>)
// Arguments are identified by their type and can have any name:
IEnumerable<CodeInstruction> instructions // [REQUIRED]
ILGenerator generator // [OPTIONAL]
MethodBase original // [OPTIONAL]- 命名 – 有兩種選擇,一種是直接命名為 Transpiler;另一種是自訂命名,此時須加註 HarmonyTranspiler attribute
- 回傳值型別 – IEnumerable<CodeInstruction>
- 三個參數
| 參數型別與名稱 | 功能說明 |
| IEnumerable<CodeInstruction> instructions | 必要,原始指令流。這是目標方法的「原始碼(IL 版)」,你所有的修改邏輯都必須以此為基礎。 |
| ILGenerator generator | optional,IL 產生器:當你需要控制程式流程(如跳轉標籤)或管理記憶體空間(如宣告變數)時,才需要使用它。 |
| MethodBase original | optional,原始方法資訊。提供目標方法的名稱、所屬類別、參數清單等元數據(Metadata)。 |
實作範例
Transpiler 的實作有兩種方式,一種是 CodeInstruction ,另一種是 CodeMatcher。以下用個簡單的例子來展示這兩種寫法。
先來一個要修改的對象,非常簡單的一個類別,裡面有一個唯讀的欄位 value,要將這個 100 修改成 5;公開的 Value 屬性是為了可以在主程式驗證。
public class OriginalClass
{
private static readonly int value = 100;
public static int Value => value;
}首先,要先了解到 value = 100 這個指派行為事實上會被移動到靜態建構式中,所以要更改的對象是「靜態建構式」。接著需要列出這個靜態建構式的 IL Code (如果你對 IL 熟悉到一個不行就可以略過這一步)。
.method private hidebysig specialname rtspecialname static
void .cctor() cil managed
{
// 程式碼大小 8 (0x8)
.maxstack 8
IL_0000: ldc.i4.s 100
IL_0002: stsfld int32 SampleLibrary011.OriginalClass::'value'
IL_0007: ret
} // end of method OriginalClass::.cctor『IL_0000: ldc.i4.s 100』← 這行就是要修改的指令 (Ldc_I4 系列指令就是將某個整數值塞進評估堆疊的意思)。
Transpiler - CodeInstruction
HarmonyPatch attribute 除了傳入目標型別外,同時傳入 MethodType.StaticConstructor,表明要修改的是靜態建構式。
所以這個 Transpiler 被執行的時候 instructions 上述所列 IL 的每一行指令,然後我們比對要修改的對象是 Opcode.Ldc.i4.s , 值是100,然後修改這個指令為 OpCodes.Ldc_I4_5 (這就是把指令換成『把 5 塞進評估堆疊』)。
這樣就完成了置換靜態建構式的流程 [範例連結Sample011]。
[HarmonyPatch(typeof(OriginalClass), MethodType.StaticConstructor)]
public class PatchOriginal
{
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
foreach (var instruction in instructions)
{
if (instruction.opcode == OpCodes.Ldc_I4_S && instruction.operand is sbyte val && val == 100)
{
yield return new CodeInstruction(OpCodes.Ldc_I4_5);
}
else
{
yield return instruction;
}
}
}
}Transpiler – CodeMatcher
剛才我們看到的是『手動過濾』的方式,這在邏輯簡單時運作良好。想像一下,如果你要尋找的是一段複雜的指令序列,或是要在某個特定的 Call 指令前後插入程式碼,不斷的 if-else 會讓維護 Transpiler 變成一場惡夢。 為了簡化這個過程,Harmony 提供了更高級的抽象工具:CodeMatcher [範例連結Sample012] 。
[HarmonyPatch(typeof(OriginalClass), MethodType.StaticConstructor)]
public static class PatchTarget
{
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions)
.MatchStartForward(new CodeMatch(OpCodes.Ldc_I4_S, (sbyte)100))
.SetOperandAndAdvance((sbyte)5)
.InstructionEnumeration();
}
}這段代碼展示了 CodeMatcher 如何將底層的 IL 處理抽象化,主要可以分為三個階段:
- 初始化與封裝 (new CodeMatcher)
不同於 IEnumerable 需要我們手動用 foreach 迭代,CodeMatcher 像是一個「帶有游標(Cursor)的容器」。它把所有的原始指令讀進來,並準備好一個可以在指令序列中前後移動的指針。 - 宣告式搜尋 (MatchStartForward)
◆ 語意化:不再撰寫 if (instruction.opcode == ...),而是直接告訴程式「我要找什麼」。
◆ 模式比對:new CodeMatch(OpCodes.Ldc_I4_S, (sbyte)100) 就像是在 IL 代碼中執行「尋找」功能。
◆ 游標定位:一旦找到符合的指令,CodeMatcher 的游標會自動停在該指令的位置。 - 就地修改與輸出 (SetInstruction & InstructionEnumeration)
◆ SetInstruction:這是一個「覆蓋」動作。因為游標已經停在正確的位置,我們不需要處理索引(Index),直接呼叫 SetInstruction 就能把當前位置的指令換成 Ldc_I4_5。
◆ 鏈式呼叫:所有的操作都可以連在一起寫,最後透過 InstructionEnumeration() 把修改後的結果轉回 Harmony 需要的 IEnumerable<CodeInstruction>。
簡單聊聊 Transpiler,我們下次見。