CodeInstruction
A CodeInstruction represents one IL instruction, including its opcode, operand, labels, and exception boundaries.
CodeInstruction wraps the .NET Emit API so transpilers can share and edit instructions. Some details differ: jumps use labels, for example, not numeric offsets such as "four instructions forward".
A transpiler receives and returns IEnumerable<CodeInstruction>. An optional ILGenerator lets you define labels and local variables.
Most arguments from Emit() are used in the same way:
- Emit takes an OpCode and so does CodeInstruction
- Operands are mostly the same:
TypeFieldInfoMethodInfoConstructorInfoInt64,Int32,Int16,Single,Double,String, orByte
The main restrictions are:
- Operands of jumps cannot be numeric, use
Labelinstead SignatureHelpersupport is experimental at best- You should avoid using indices when referring to local variables
Do not call ILGenerator.Emit(): return instructions and let Harmony emit them. Use the generator to declare locals and labels; describe exception boundaries on the instructions themselves.
Prefer reusing existing operands for labels and locals. Find a distinctive instruction and take its operand instead of hard-coding an index.
Indirect calls
For a calli instruction read from a method body, Harmony supplies an InlineSignature operand. Transpilers can inspect its parameter types, return type, calling convention, and instance flags without accessing Harmony internals.
Use its PopCount and PushCount properties when calculating evaluation-stack depth:
if (instruction.opcode == OpCodes.Calli && instruction.operand is InlineSignature signature)
{
stackDepth -= signature.PopCount;
stackDepth += signature.PushCount;
}
PopCount includes the function pointer, every listed parameter, and an implicit instance when HasThis is true. When ExplicitThis is also true, the first listed parameter already represents that instance, so it is not counted twice. PushCount is zero for void and one for any returned value. These are counts of evaluation-stack values, not bytes or native calling-convention registers. See the calli stack behavior and instance-signature flags.
Parameter and return entries are Type objects, nested InlineSignature objects for function pointers, or InlineSignature.ModifierType objects for optional/required type modifiers. Modifiers do not change the stack count; returning a function pointer pushes one value even if that function itself returns void. The counts follow changes to the mutable signature.
The existing calling-convention representation uses CallingConvention.Winapi for the default managed convention; its other named values denote their corresponding unmanaged conventions. Stack counting does not depend on this encoding. The computed counts describe a valid signature; they do not validate whether the runtime can emit an arbitrary signature assembled by a transpiler.
Making this model public does not expand the reader's existing signature support. Generic type/method parameter entries, varargs sentinels, and pointer/by-reference/array wrappers around a nested function-pointer signature are not currently supported by the reader.
Local variables
Existing local operands can be numeric indices or LocalBuilder objects. Handle both. To add a local, call ILGenerator.DeclareLocal; to reuse one, copy its existing operand.
Labels
Jumps use a Label operand. The destination instruction holds that label in its labels list. To add a jump, call ILGenerator.DefineLabel(), attach the label to the destination, and use it as the jump's operand.
If a jump uses a label that no returned instruction carries, patching throws InvalidProgramException and names the transpilers. This happens, for example, when the instruction meant to receive the label is never found because the game's IL changed.
Stack depth
Every path that reaches an instruction must arrive with the same number of items on the evaluation stack. On .NET Core and .NET 5 or later, Harmony checks this when it writes the patched method. If the depths differ, or an instruction pops more than the stack holds, patching throws InvalidProgramException. The message names the instruction, the depths and the transpilers. A common cause is a jump after a dup that leaves the copy on the stack on one path only.
Some runtimes accept such IL. Mono accepts a depth difference at a leave, because leave empties the stack. To leave the decision to the runtime, set Harmony.ValidateStack = false or the environment variable HARMONY_VALIDATE_STACK=0. When set, the environment variable overrides the property. Harmony then sizes the stack for the deepest path, and a runtime that rejects the IL throws its own InvalidProgramException.
Try/catch boundaries
An instruction's blocks list marks exception boundaries, including filters. Harmony builds the exception metadata from these markers. When moving or inserting code, preserve boundary order and keep handler-entry labels on their handler instructions.
Convenience methods
CodeInstruction extension methods help create, find, and compare instructions and their operands.
Pitfalls
Removing an instruction can orphan its labels or exception boundaries. Copying one can duplicate them. Both can produce invalid IL.
When inserting before an instruction, decide whether jumps should reach your new code or the old instruction. Move labels accordingly. Treat exception boundaries separately: moving one changes which code is inside the handler or protected region.
Set Harmony.DEBUG = true or use [HarmonyDebug] to inspect generated instructions, labels, and exception boundaries in the log.