How to do .NET runtime method patch on generic class's static non-generic method? (Harmony or MonoMod)

1.3k Views Asked by At

In this example, I want to patch PatchTarget.QSingleton\<T\>.get_Instance().
How to get it done with Harmony or MonoMod?

Harmony:

"Unhandled exception. System.NotSupportedException: Specified method is not supported."

MonoMod:

"Unhandled exception. System.ArgumentException: The given generic instantiation was invalid."

Code snippet: (runnable with dotnetfiddle.net)

using System; 
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;

namespace PatchTarget {
  public abstract class QSingleton<T> where T : QSingleton<T>, new() {
    protected static T instance = null; protected QSingleton() { } 

    public static T Instance { get {
      if (instance == null) {
        instance = new T();
        Console.Write($"{typeof(T).Name}.Instance: impl=QSingleton");
      }
      return instance; 
    } }
  } 
}

namespace Patch {
  public class TypeHelper<T> where T : PatchTarget.QSingleton<T>, new() {
    public static T InstanceHack() {
      Console.Write($"{typeof(T).Name}.Instance: impl=InstanceHack");
      return null;
    }
  }
    
  public static class HarmonyPatch {
    public static Harmony harmony = new Harmony("Try");

    public static void init() {
      var miOriginal = AccessTools.Property(typeof(PatchTarget.QSingleton<>), "Instance").GetMethod;
      var miHack = AccessTools.Method(typeof(TypeHelper<>), "InstanceHack");
      harmony.Patch(miOriginal, prefix: new HarmonyMethod(miHack));
    }
  }

  public static class MonoModPatch {
    public static MonoMod.RuntimeDetour.Detour sHook;

    public static void init() {
      var miOriginal = AccessTools.Property(typeof(PatchTarget.QSingleton<>), "Instance").GetMethod;
      var miHack = AccessTools.Method(typeof(TypeHelper<>), "InstanceHack");
      sHook = new MonoMod.RuntimeDetour.Detour(miOriginal, miHack);
    }
  }
}

class Program {
  public static void Main() {
    Patch.HarmonyPatch.init();
    // Patch.MonoModPatch.init();
    Console.WriteLine($"done");
  }
}
1

There are 1 best solutions below

0
On

After some trial and error, I got something working, but not the reason behind it.

  • Both Harmony and MonoMod.RuntimeDetour can hook with the typeof(QSingleton<SampleA>).GetMethod(), but not typeof(QSingleton<>).GetMethod().
  • Harmony output is unexpected.
  • Harmony attribute annotation doesn't seem to work.
  • Generating IL seems useless due to the potential lack of TypeSpec for generic.

Questions:

  • What is the difference between QSingleton<>.Instance and QSingleton<SampleA>.Instance in the sample?
    I would guess that <>.Instance is MethodDef, while <SampleA>.Instance is TypeSpec.MemberRef.
  • Why does Harmony/MonoMod.RuntimeDetour need TypeSpec.MemberRef? For generating redirection stub?
  • Is it possible to fix the hook under Harmony?
  • Can Harmony/MonoMod generates "ldtoken <TypeSpec>" if TypeSpec already exists?
  • Can Harmony/MonoMod dynamically generates necessary TypeSpec for generics?

Code snippet: (runnable with dotnetfiddle.net)

using System; 
using HarmonyLib;

namespace PatchTarget {
  public abstract class QSingleton<T> where T : QSingleton<T>, new() {
    protected static T instance = null; protected QSingleton() { } 

    public static T Instance { get {
      if (instance == null) {
        instance = new T();
        Console.WriteLine($"{typeof(T).Name}.Instance: impl=QSingleton");
      }
      return instance; 
    } }
  }

  public class SampleA : QSingleton<SampleA> {
    public SampleA() { Console.WriteLine("SampleA ctor"); }
  }

  public class SampleB : QSingleton<SampleB> {
    public SampleB() { Console.WriteLine("SampleB ctor"); }
  }
}

namespace Patch {
  public class TypeHelper<T> where T : PatchTarget.QSingleton<T>, new() {
    public static T InstanceHack() {
      Console.WriteLine($"{typeof(T).Name}.Instance: impl=InstanceHack");
      return null;
    }

    // For Harmony as Prefix, but attribute does not work.
    public static bool InstanceHackPrefix(T __result) {
      Console.WriteLine($"{typeof(T).Name}.Instance: impl=InstanceHack");
      __result = null;
      return false;
    }
  }

  public static class HarmonyPatch {
    public static Harmony harmony = new Harmony("Try");

    public static void init() {
      // Attribute does not work.
      // Transpiler does not work because the lack of TypeSpec to setup generic parameters.
      var miOriginal = AccessTools.Property(typeof(PatchTarget.QSingleton<PatchTarget.SampleB>), "Instance").GetMethod;
      var miHack = AccessTools.Method(typeof(TypeHelper<PatchTarget.SampleB>), "InstanceHackPrefix");
      harmony.Patch(miOriginal, prefix: new HarmonyMethod(miHack));
    }
  }

  public static class MonoModPatch {
    public static MonoMod.RuntimeDetour.Detour sHook;

    public static void init() {
      var miOriginal = AccessTools.Property(typeof(PatchTarget.QSingleton<PatchTarget.SampleB>), "Instance").GetMethod;
      var miHack = AccessTools.Method(typeof(TypeHelper<PatchTarget.SampleB>), "InstanceHack");
      sHook = new MonoMod.RuntimeDetour.Detour(miOriginal, miHack);
    }
  }
}

class Program {
  public static void Main() {
    _ = PatchTarget.SampleA.Instance;
    // MonoMod works (replaces globally).
    // Harmony hooks, but in an expected way (T becomes SampleB, not 1st generic type parameter).
    // try { Patch.HarmonyPatch.init(); } catch (Exception e) { Console.WriteLine($"Harmony error: {e.ToString()}"); }
    try { Patch.MonoModPatch.init(); } catch (Exception e) { Console.WriteLine($"MonoMod error: {e.ToString()}"); }
    _ = PatchTarget.SampleB.Instance;
    _ = PatchTarget.SampleA.Instance;
    Console.WriteLine($"done");
  }
}

MonoMod.RuntimeDetour Output:(Work as intended)

SampleA.Instance: impl=QSingleton
SampleB.Instance: impl=InstanceHack
SampleA.Instance: impl=InstanceHack

Harmony Output:(Broken <T>)

SampleA.Instance: impl=QSingleton
SampleB.Instance: impl=InstanceHack
SampleB.Instance: impl=InstanceHack