-
Notifications
You must be signed in to change notification settings - Fork 44
Transpiler helpers
In addition to common Harmony transpiler helpers like CodeInstructionExtensions
and Transpilers
, HarmonyX provides some additional helpers to make writing transpilers easier.
This guide assumes you are already familiar with the basics of writing Harmony transpilers and IL manipulation in general. If not, check out the following resources:
- Transpilers - Harmony Documentation
- CodeInstruction - Harmony Documentation
- Working with IL - R2Wiki
In some cases transpilers are used to insert some complicated logic in the middle of the original method. Especially additional control logic -- conditionals and loops -- are difficult to write out with pure transpilers. In these cases you'd usually make a helper method that contains the desired code and then inject a call
to your method in the correct position. This can be quite annoying to do for multiple methods.
Borrowing from MonoMod, HarmonyX introduces a helper method Transpilers.EmitDelegate
that emits a call
to an arbitrary delegate.
Example:
static void SomeOtherMethod();
static void Original()
{
// ldc.i4.1
// stdloc.0
int foo = 1;
// ldloc.0
// ldc.i4.s 10
// add
// stloc.0
foo += 10;
// ldloc.0
// call SomeOtherMethod(int32)
SomeOtherMethod(foo);
}
IEnumerable<CodeInstruction> Prefix(IEnumerable<CodeInstruction> instructions)
{
foreach(CodeInstruction instruction in instructions)
{
// Match against call SomeOtherMethod(int32)
// At that point the value of `foo` is on the stack, in which case our delegate will consume it
// This is why our delegate returns the new value that's pushed onto the stack
if (instruction.opcode == OpCodes.Call)
yield return Transpilers.EmitDelegate<Func<int, int>>(foo => foo + 5); // Emit a `call Delegate`
yield return instruction;
}
}
CodeMatcher allows to write transpilers as a steam of predicates and filters. If you ever used Unix sed
, you can think of CodeMatcher as a stream editor for IL instructions -- except CodeMatcher supports moving into multiple directions in the IL stream. If you ever used MonoMod, you will find that CodeMatcher is remarkably similar to ILCursor
.
To use CodeMatcher
, simply instantiate it passing a collection of instructions to manipulate. At the end of the manipulation, simply call InstructionEnumeration
to output the manipulated instructions as a IEnumerable<CodeInstruction>
collection.
Thus, the simplest CodeMatcher is
IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions)
.InstructionEnumeration();
}
This matcher takes the instructions are returns the instructions back unmodified.
The core feature of CodeMatcher is the ability to flexibly move inside the method body. You can think of CodeMatcher as a Notepad: you have a cursor which you can move within the method body. You can either move the cursor by a set amount (e.g. move to the start/end or several instructions) or you can use the search feature to move the cursor to some specific place.
To move within the instructions, you can use the following methods:
-
SearchForward
andSearchBack
-- match against a predicate (Func<CodeInstruction, bool>
) and move to the first match from the current position -
MatchForward
andMatchBack
-- match against aCodeMatch
instance and move to the first match from the current position -
Start
,End
andAdvance
-- move to start, end or by some number of instructions relative to current position
*Forward
will find a match from the start of the method, while *Back
will match from the end of the method.
SearchForward
and SearchBack
simply check each a code instruction for a predicate and stop matching once the predicate returns true
. On the other hand, MatchForward
and MatchBack
are of more interest and thus shall be discussed in more detail.
Match*
methods take an array of CodeMatch
instances that represent full or partial matches of an IL instruction. You can think of CodeMatch
as a glob pattern for an instruction. With CodeMatch
, you can match an instruction as follows:
- match by just the opcode: for example
new CodeMatch(OpCodes.Ldfld)
matches anyldfld
instruction not caring about the operand; - match by just operand: for example
new CodeMatch(null, 10)
matches any instruction that has integer10
as the operand; - by both the opcode and operand: for example
new CodeMatch(OpCodes.Ldstr, "Test")
will matchldstr "Test"
- by an arbitrary predicate: for example
new CodeMatch(i => i.opcode == OpCodes.Ldfld)
will match if the opcode of the instruction isldfld
. You can use any of theCodeInstructionExtensions
helpers here and more complex logic
With Match*
methods, you can combine multiple CodeMatch
es to match a string of instuctions.
Examples:
Example 1
new CodeMatcher(instructions)
.MatchForward(false, // false = move at the start of the match, true = move at the end of the match
new CodeMatch(OpCodes.Ldstr),
new CodeMatch(OpCodes.Call, AccessTools.Method(typeof(Foo), "Foo")),
new CodeMatch(OpCodes.Ret))
will match the following code:
ldstr ? ;any string
call Foo.Foo
ret
Example 2
new CodeMatcher(instructions)
.MatchForward(false, // false = move at the start of the match, true = move at the end of the match
new CodeMatch(OpCodes.Stfld),
new CodeMatch(OpCodes.Ldarg_0),
new CodeMatch(i => i.opcode == OpCodes.Ldfld && ((FieldInfo)i.operand).Name == "test"))
will match
stfld ? ;any field
ldarg.0
ldfld ?.test ;any field the name of which is `test`
Once you moved the "cursor" to a place you want to edit, you can use CodeMatcher to manipulate the matched instructions. Some of the common manipulation methods are:
-
SetInstruction
andSetInstructionAndAdvance
-- replaces an instruction with the new one and optionally moves the cursor to the next instruction, this removes any labels and exception blocks attached -
Set
andSetAndAdvance
-- replaces the opcode and the operand of the current instruction while keeping original labels and exception blocks-
SetOpcodeAndAdvance
andSetOperandAndAdvance
are similar toSetAndAdvance
except they update just a single method
-
-
Insert
andInsertAndAdvance
-- inserts one or more instructions at the current cursor position and optionally advances the cursor -
RemoveInstruction
andRemoveInstructions
-- removes instructions, including their labels and branches
Examples:
Example 1
new CodeMatcher(instructions)
.MatchForward(false, // false = move at the start of the match, true = move at the end of the match
new CodeMatch(OpCodes.Ldstr),
new CodeMatch(OpCodes.Call, AccessTools.Method(typeof(Foo), "Foo")),
new CodeMatch(OpCodes.Ret))
.SetOperandAndAdvance("Woo!")
will match
ldstr ? ;any string
call Foo.Foo
ret
and replace the operand of ldstr
to "Woo!"
. The final IL will thus be
ldstr "Woo!"
call Foo.Foo
ret
Example 2
new CodeMatcher(instructions)
.MatchForward(false, // false = move at the start of the match, true = move at the end of the match
new CodeMatch(OpCodes.Stfld),
new CodeMatch(OpCodes.Ldarg_0),
new CodeMatch(i => i.opcode == OpCodes.Ldfld && ((FieldInfo)i.operand).Name == "test"))
.Advance(2) // Move cursor to before ldfld
.InsertAndAdvance(
new CodeInstruction(OpCodes.Dup),
new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Foo), "Foo"))
)
will match
stfld ? ;any field
ldarg.0
ldfld ?.test ;any field the name of which is `test`
and manipulate it as follows:
stfld ? ;any field
ldarg.0
dup
call Foo.Foo(object)
ldfld ?.test ;any field the name of which is `test`
Finally, in many cases you want to repeat the instruction match as much as possible. In that case you can use Repeat
to run the previous Match*
matcher multiple times and run an action.
Full example:
new CodeMatcher(instructions)
.MatchForward(false, // false = move at the start of the match, true = move at the end of the match
new CodeMatch(OpCodes.Stfld),
new CodeMatch(OpCodes.Ldarg_0),
new CodeMatch(i => i.opcode == OpCodes.Ldfld && ((FieldInfo)i.operand).Name == "test"))
.Repeat( matcher => // Do the following for each match
matcher
.Advance(2) // Move cursor to before ldfld
.InsertAndAdvance(
new CodeInstruction(OpCodes.Dup),
new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Foo), "Foo"))
)
)
.InstructionEnumerable(); // Finally, return the manipulated method instructions
- Basic usage
-
HarmonyX extensions
1.1. Patching and unpatching
1.2. Prefixes are flowthrough
1.3. Targeting multiple methods with one patch
1.4. Patching enumerators
1.5. Transpiler helpers
1.6. ILManipulators
1.7. Extended patch targets
1.8. New patch attributes -
Extending HarmonyX
2.1. Custom patcher backends -
Misc
4.1. Patch parameters - Implementation differences from Harmony