Skip to content

Transpiler helpers

Geoffrey Horsington edited this page Jul 30, 2022 · 6 revisions

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:

Additional transpilers

EmitDelegate

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

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.

Basic usage

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.

Moving inside the instructions

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 and SearchBack -- match against a predicate (Func<CodeInstruction, bool>) and move to the first match from the current position
  • MatchForward and MatchBack -- match against a CodeMatch instance and move to the first match from the current position
  • Start, End and Advance -- 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.

Instruction matching

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 any ldfld instruction not caring about the operand;
  • match by just operand: for example new CodeMatch(null, 10) matches any instruction that has integer 10 as the operand;
  • by both the opcode and operand: for example new CodeMatch(OpCodes.Ldstr, "Test") will match ldstr "Test"
  • by an arbitrary predicate: for example new CodeMatch(i => i.opcode == OpCodes.Ldfld) will match if the opcode of the instruction is ldfld. You can use any of the CodeInstructionExtensions helpers here and more complex logic

With Match* methods, you can combine multiple CodeMatches 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`

Manipulating instructions

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 and SetInstructionAndAdvance -- 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 and SetAndAdvance -- replaces the opcode and the operand of the current instruction while keeping original labels and exception blocks
    • SetOpcodeAndAdvance and SetOperandAndAdvance are similar to SetAndAdvance except they update just a single method
  • Insert and InsertAndAdvance -- inserts one or more instructions at the current cursor position and optionally advances the cursor
  • RemoveInstruction and RemoveInstructions -- 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`

Handling multiple matches

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