Runtime edge cases
Some runtime behavior needs a workaround; some cannot be patched. Here are the common cases.
Inlining
When the runtime inlines a method, it copies its code into the caller. That call site no longer calls the method, so patching the method does not affect it.
If you control the host, disabling inlining may help. Otherwise, patch a caller that has not been inlined. You may need TargetMethods() to cover several callers.
Calling Base Methods
base.SomeMethod() in your patch refers to your patch class's base, not the patched class's base:
using HarmonyLib;
using System;
using System.Runtime.CompilerServices;
[HarmonyPatch]
class Patch
{
[HarmonyReversePatch]
[HarmonyPatch(typeof(BaseClass), nameof(BaseClass.Method))]
[MethodImpl(MethodImplOptions.NoInlining)]
static string BaseMethodDummy(SubClass instance) => null;
[HarmonyPatch(typeof(SubClass), nameof(SubClass.Method))]
static void Prefix(SubClass __instance)
{
var str = BaseMethodDummy(__instance);
Console.WriteLine(str);
}
}
public class BaseClass
{
public virtual string Method() => "base";
}
public class SubClass : BaseClass
{
public override string Method() => "subclass";
}
The compiler resolves base.SomeMethod() to a specific base method. To call that implementation from a patch, use a reverse patch to copy it into a stub you control. See this example and comparison.
Runtime async in .NET 11
Harmony does not currently support patching methods compiled with .NET 11 runtime async. Patching or reverse-patching these methods throws NotSupportedException before installing a replacement. Ordinary Task-returning methods and methods that use compiler-generated async state machines remain supported.
Runtime async uses a different calling convention. Copying its method body into an ordinary replacement can fail at execution, and patching its reflected entry point does not cover all native async callers.
Callers compiled with runtime-async, including the .NET 11 runtime libraries, call the runtime-generated async variant of an ordinary Task-returning method. A patch on that method does not reach them. With MonoMod.Core 1.4.0-prerelease.1, a variant compiled after the patch corrupts code and can crash the process, see MonoMod#344. When Harmony.DEBUG is on, Harmony writes a line to the log when it detects such a variant compilation.
Generics
Generics can be difficult to patch. In general, expect generic methods and methods of generic classes to be shared between different types of T during runtime. This means that by patching one method, the method will be patched for all types of T. Depending on the type of generic and your .NET runtime, this can be worked around in a few ways:
- If the generic includes a value type, such as
int, in most (but not all) cases, the method will not be shared. Patching a method which uses a value type parameter will patch only that specific method. Conversely, patching a generic with an object type will not patch the value type method, so both may have to be patched. - If the method is a non-generic non-static method of a generic class, you can check the generic type using
__instance(such as__instance.GetType().DeclaringType.GenericTypeArguments), and adjust your code's behavior depending on the type. - If the method is a generic method of a non-generic class, you may be able to examine the method's arguments, if any argument contains
T. Also, generic type data will be lost (ifMethod<T>is patched usingMethod<string>,Method<object>will becomeMethod<string>) - If the method is a static non-generic method of a generic class, generic type data will be lost (see above).
These limits apply to the detour. If a runtime host registers an installer and accepts the fully open definition, Harmony installs one body for its family. A definition patch reaches every instantiation. A closed-instantiation patch runs only for that instantiation. Declined targets and hosts without an installer retain detour behavior.
An instantiation with a transpiler or an Infix patch gets a closed helper body. The definition's patches also apply inside that helper. Definition transpilers must therefore handle closed IL with substituted types and operands. The helper call adds one stack frame, which affects stack inspection and caller-sensitive code.
The priority and before/after sorter merges definition and instantiation patches within each patch category. Record indices can interleave, so definition patches do not always run first. When priority and index are equal, the definition's patch runs first. __state stays separate per patch class and registration target.
For a generic callback, register its definition with the required declaring-type-then-method parameter order and compatible constraints. Directly constructed callbacks are rejected for both definitions and instantiations installed through a family. Use a patch factory when you need a fixed callback instantiation. Definition-wide bindings must be valid for every permitted type. Instantiation bindings and passthrough results validate closed types. See the callback rules.
Generic family bridges reject function-pointer signatures before installation. Reverse patches into host-owned generic stand-ins are also unsupported. Partially open targets fail if a host with an installer accepts them.
Changing the type returned by a constructor
Constructors initialize an object; they do not choose its type. A C# construction uses IL like:
newobj instance void Test::.ctor();
And the newobj IL code is described by Microsoft as
The newobj instruction allocates a new instance of the class associated with ctor and initializes all the fields in the new instance to 0 (of the proper type) or null references as appropriate. It then calls the constructor ctor with the given arguments along with the newly created instance. After the constructor has been called, the now initialized object reference (type O) is pushed on the stack.
By the time the constructor runs, the object already exists. To change what is created, patch the caller's newobj operation or replace the value it produces, not the constructor body.
Static Constructors
Static constructors of a class will run as soon as you touch or instantiate that class. That unfortunately means that when Harmony asks for some basic required information for that class, it will trigger the static constructor before the patch happens.
As a result, you cannot patch static constructors unless you plan to run them again (which often defeats the purpose). It also has the side effect that your patches to other methods in such a class will run the constructor at the wrong moment - causing errors. In that case, you need to time the patching so it happens when it's ok to run the static constructor or when it already has been triggered.
Native (External) Methods
A native method has no IL body for Harmony to copy, so ordinary prefixes and postfixes do not work. A transpiler-only patch can supply a new body from the empty input. Beware: this replaces the native implementation; it does not give you a way to call it.
Function pointer signatures
A method whose signature contains a function pointer type, written as delegate* in C#, patches on CoreCLR. On Mono, reflection cannot describe such a type: it recurses without end while Harmony prepares the patch, and the process ends with a stack overflow. Do not patch such methods on Mono.
MarshalByRefObject
Patching methods on classes derived from MarshalByRefObject is runtime-dependent and may fail. Verify the specific method on the runtime you intend to support.
Special Classes
Some framework classes, such as HttpRequest, have runtime-dependent patching quirks. An identity patch (patching with no callbacks or transpilers) on one method has sometimes been needed before patching another in the same class. There is no general workaround; results depend on the runtime, architecture, and class.
Methods With Dead Code
In some environments (like Mono) the runtime poses strict rules about creating methods that should not contain dead code. This becomes problematic, when patching methods like the following results in a InvalidProgramException:
public SomeType MyMethod()
{
throw new NotImplementedException()
}
That method has no RET IL code in its body and if you try to patch it, Harmony will generate illegal IL. The only solution to this is to create a Transpiler that transpiles the method to a correct version by creating valid IL. This is also true for adding a Prefix or Postfix to that method. The way Harmony works, the replacement method needs to be valid to add calls to your patches to it.
Patching too early: MissingMethodException in Unity
Patching during early Unity startup can throw MissingMethodException: Attempted to access a missing method when the target directly or indirectly calls an external UnityEngine method.
In the following example code, patching either SomeMethod() or SomeOtherMethod() will cause the exception:
class SomeGameObject
{
GameObject gameObject;
void SomeMethod() => UnityEngine.Object.DontDestroyOnLoad(gameObject);
void SomeOtherMethod() => SomeMethod();
}
UnityEngine.Object.DontDestroyOnLoad() is an external UnityEngine method:
[MethodImpl(MethodImplOptions.InternalCall)]
[GeneratedByOldBindingsGenerator]
public static extern void DontDestroyOnLoad(Object target);
Wait until UnityEngine has linked its external methods. One option is to patch after the first scene loads, using SceneManager.sceneLoaded:
public static class Patcher
{
private static bool patched = false;
public static void Main() =>
//DoPatch(); <-- Do not execute patching on assembly entry point
SceneManager.sceneLoaded += SceneLoaded;
private static void DoPatch()
{
var harmony = new Harmony("test");
harmony.PatchAll(typeof(Patcher).Assembly);
patched = true;
}
private static void SceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode loadSceneMode)
{
// Execute patching after unity has finished it's startup and loaded at least the first game scene
if (!patched)
DoPatch();
}
}