Dynamically replace the contents of a C# method?

后端 未结 10 2283
面向向阳花
面向向阳花 2020-11-22 16:09

What I want to do is change how a C# method executes when it is called, so that I can write something like this:

[Distributed]
public DTask Solve         


        
相关标签:
10条回答
  • 2020-11-22 16:18

    I know it is not the exact answer to your question, but the usual way to do it is using factories/proxy approach.

    First we declare a base type.

    public class SimpleClass
    {
        public virtual DTask<bool> Solve(int n, DEvent<bool> callback)
        {
            for (int m = 2; m < n - 1; m += 1)
                if (m % n == 0)
                    return false;
            return true;
        }
    }
    

    Then we can declare a derived type (call it proxy).

    public class DistributedClass
    {
        public override DTask<bool> Solve(int n, DEvent<bool> callback)
        {
            CodeToExecuteBefore();
            return base.Slove(n, callback);
        }
    }
    
    // At runtime
    
    MyClass myInstance;
    
    if (distributed)
        myInstance = new DistributedClass();
    else
        myInstance = new SimpleClass();
    

    The derived type can be also generated at runtime.

    public static class Distributeds
    {
        private static readonly ConcurrentDictionary<Type, Type> pDistributedTypes = new ConcurrentDictionary<Type, Type>();
    
        public Type MakeDistributedType(Type type)
        {
            Type result;
            if (!pDistributedTypes.TryGetValue(type, out result))
            {
                if (there is at least one method that have [Distributed] attribute)
                {
                    result = create a new dynamic type that inherits the specified type;
                }
                else
                {
                    result = type;
                }
    
                pDistributedTypes[type] = result;
            }
            return result;
        }
    
        public T MakeDistributedInstance<T>()
            where T : class
        {
            Type type = MakeDistributedType(typeof(T));
            if (type != null)
            {
                // Instead of activator you can also register a constructor delegate generated at runtime if performances are important.
                return Activator.CreateInstance(type);
            }
            return null;
        }
    }
    
    // In your code...
    
    MyClass myclass = Distributeds.MakeDistributedInstance<MyClass>();
    myclass.Solve(...);
    

    The only performance loss is during construction of the derived object, the first time is quite slow because it will use a lot of reflection and reflection emit. All other times, it is the cost of a concurrent table lookup and a constructor. As said, you can optimize construction using

    ConcurrentDictionary<Type, Func<object>>.
    
    0 讨论(0)
  • 2020-11-22 16:20

    You CAN modify a method's content at runtime. But you're not supposed to, and it's strongly recommended to keep that for test purposes.

    Just have a look at:

    http://www.codeproject.com/Articles/463508/NET-CLR-Injection-Modify-IL-Code-during-Run-time

    Basically, you can:

    1. Get IL method content via MethodInfo.GetMethodBody().GetILAsByteArray()
    2. Mess with these bytes.

      If you just wish to prepend or append some code, then just preprend/append opcodes you want (be careful about leaving stack clean, though)

      Here are some tips to "uncompile" existing IL:

      • Bytes returned are a sequence of IL instructions, followed by their arguments (if they have some - for instance, '.call' has one argument: the called method token, and '.pop' has none)
      • Correspondence between IL codes and bytes you find in the returned array may be found using OpCodes.YourOpCode.Value (which is the real opcode byte value as saved in your assembly)
      • Arguments appended after IL codes may have different sizes (from one to several bytes), depending on opcode called
      • You may find tokens that theses arguments are referring to via appropriate methods. For instance, if your IL contains ".call 354354" (coded as 28 00 05 68 32 in hexa, 28h=40 being '.call' opcode and 56832h=354354), corresponding called method can be found using MethodBase.GetMethodFromHandle(354354)
    3. Once modified, you IL byte array can be reinjected via InjectionHelper.UpdateILCodes(MethodInfo method, byte[] ilCodes) - see link mentioned above

      This is the "unsafe" part... It works well, but this consists in hacking internal CLR mechanisms...

    0 讨论(0)
  • 2020-11-22 16:28

    Disclosure: Harmony is a library that was written and is maintained by me, the author of this post.

    Harmony 2 is an open source library (MIT license) designed to replace, decorate or modify existing C# methods of any kind during runtime. It main focus is games and plugins written in Mono or .NET. It takes care of multiple changes to the same method - they accumulate instead of overwrite each other.

    It creates dynamic replacement methods for every original method and emits code to them that calls custom methods at the start and end. It also allows you to write filters to process the original IL code and custom exception handlers which allows for more detailed manipulation of the original method.

    To complete the process, it writes a simple assembler jump into the trampoline of the original method that points to the assembler generated from compiling the dynamic method. This works for 32/64Bit on Windows, macOS and any Linux that Mono supports.

    Documentation can be found here.

    Example

    (Source)

    Original Code

    public class SomeGameClass
    {
        private bool isRunning;
        private int counter;
    
        private int DoSomething()
        {
            if (isRunning)
            {
                counter++;
                return counter * 10;
            }
        }
    }
    

    Patching with Harmony annotations

    using SomeGame;
    using HarmonyLib;
    
    public class MyPatcher
    {
        // make sure DoPatching() is called at start either by
        // the mod loader or by your injector
    
        public static void DoPatching()
        {
            var harmony = new Harmony("com.example.patch");
            harmony.PatchAll();
        }
    }
    
    [HarmonyPatch(typeof(SomeGameClass))]
    [HarmonyPatch("DoSomething")]
    class Patch01
    {
        static FieldRef<SomeGameClass,bool> isRunningRef =
            AccessTools.FieldRefAccess<SomeGameClass, bool>("isRunning");
    
        static bool Prefix(SomeGameClass __instance, ref int ___counter)
        {
            isRunningRef(__instance) = true;
            if (___counter > 100)
                return false;
            ___counter = 0;
            return true;
        }
    
        static void Postfix(ref int __result)
        {
            __result *= 2;
        }
    }
    

    Alternatively, manual patching with reflection

    using SomeGame;
    using HarmonyLib;
    
    public class MyPatcher
    {
        // make sure DoPatching() is called at start either by
        // the mod loader or by your injector
    
        public static void DoPatching()
        {
            var harmony = new Harmony("com.example.patch");
    
            var mOriginal = typeof(SomeGameClass).GetMethod("DoSomething", BindingFlags.Instance | BindingFlags.NonPublic);
            var mPrefix = typeof(MyPatcher).GetMethod("MyPrefix", BindingFlags.Static | BindingFlags.Public);
            var mPostfix = typeof(MyPatcher).GetMethod("MyPostfix", BindingFlags.Static | BindingFlags.Public);
            // add null checks here
    
            harmony.Patch(mOriginal, new HarmonyMethod(mPrefix), new HarmonyMethod(mPostfix));
        }
    
        public static void MyPrefix()
        {
            // ...
        }
    
        public static void MyPostfix()
        {
            // ...
        }
    }
    
    0 讨论(0)
  • 2020-11-22 16:28

    Logman's solution, but with an interface for swapping method bodies. Also, a simpler example.

    using System;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.CompilerServices;
    
    namespace DynamicMojo
    {
        class Program
        {
            static void Main(string[] args)
            {
                Animal kitty = new HouseCat();
                Animal lion = new Lion();
                var meow = typeof(HouseCat).GetMethod("Meow", BindingFlags.Instance | BindingFlags.NonPublic);
                var roar = typeof(Lion).GetMethod("Roar", BindingFlags.Instance | BindingFlags.NonPublic);
    
                Console.WriteLine("<==(Normal Run)==>");
                kitty.MakeNoise(); //HouseCat: Meow.
                lion.MakeNoise(); //Lion: Roar!
    
                Console.WriteLine("<==(Dynamic Mojo!)==>");
                DynamicMojo.SwapMethodBodies(meow, roar);
                kitty.MakeNoise(); //HouseCat: Roar!
                lion.MakeNoise(); //Lion: Meow.
    
                Console.WriteLine("<==(Normality Restored)==>");
                DynamicMojo.SwapMethodBodies(meow, roar);
                kitty.MakeNoise(); //HouseCat: Meow.
                lion.MakeNoise(); //Lion: Roar!
    
                Console.Read();
            }
        }
    
        public abstract class Animal
        {
            public void MakeNoise() => Console.WriteLine($"{this.GetType().Name}: {GetSound()}");
    
            protected abstract string GetSound();
        }
    
        public sealed class HouseCat : Animal
        {
            protected override string GetSound() => Meow();
    
            private string Meow() => "Meow.";
        }
    
        public sealed class Lion : Animal
        {
            protected override string GetSound() => Roar();
    
            private string Roar() => "Roar!";
        }
    
        public static class DynamicMojo
        {
            /// <summary>
            /// Swaps the function pointers for a and b, effectively swapping the method bodies.
            /// </summary>
            /// <exception cref="ArgumentException">
            /// a and b must have same signature
            /// </exception>
            /// <param name="a">Method to swap</param>
            /// <param name="b">Method to swap</param>
            public static void SwapMethodBodies(MethodInfo a, MethodInfo b)
            {
                if (!HasSameSignature(a, b))
                {
                    throw new ArgumentException("a and b must have have same signature");
                }
    
                RuntimeHelpers.PrepareMethod(a.MethodHandle);
                RuntimeHelpers.PrepareMethod(b.MethodHandle);
    
                unsafe
                {
                    if (IntPtr.Size == 4)
                    {
                        int* inj = (int*)b.MethodHandle.Value.ToPointer() + 2;
                        int* tar = (int*)a.MethodHandle.Value.ToPointer() + 2;
    
                        byte* injInst = (byte*)*inj;
                        byte* tarInst = (byte*)*tar;
    
                        int* injSrc = (int*)(injInst + 1);
                        int* tarSrc = (int*)(tarInst + 1);
    
                        int tmp = *tarSrc;
                        *tarSrc = (((int)injInst + 5) + *injSrc) - ((int)tarInst + 5);
                        *injSrc = (((int)tarInst + 5) + tmp) - ((int)injInst + 5);
                    }
                    else
                    {
                        throw new NotImplementedException($"{nameof(SwapMethodBodies)} doesn't yet handle IntPtr size of {IntPtr.Size}");
                    }
                }
            }
    
            private static bool HasSameSignature(MethodInfo a, MethodInfo b)
            {
                bool sameParams = !a.GetParameters().Any(x => !b.GetParameters().Any(y => x == y));
                bool sameReturnType = a.ReturnType == b.ReturnType;
                return sameParams && sameReturnType;
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-22 16:31

    have a look into Mono.Cecil:

    using Mono.Cecil;
    using Mono.Cecil.Inject;
    
    public class Patcher
    {    
       public void Patch()
       {
        // Load the assembly that contains the hook method
        AssemblyDefinition hookAssembly = AssemblyLoader.LoadAssembly("MyHookAssembly.dll");
        // Load the assembly
        AssemblyDefinition targetAssembly = AssemblyLoader.LoadAssembly("TargetAssembly.dll");
    
        // Get the method definition for the injection definition
        MethodDefinition myHook = hookAssembly.MainModule.GetType("HookNamespace.MyHookClass").GetMethod("MyHook");
        // Get the method definition for the injection target. 
        // Note that in this example class Bar is in the global namespace (no namespace), which is why we don't specify the namespace.
        MethodDefinition foo = targetAssembly.MainModule.GetType("Bar").GetMethod("Foo");
    
        // Create the injector
        InjectionDefinition injector = new InjectionDefinition(foo, myHook, InjectFlags.PassInvokingInstance | InjectFlags.passParametersVal);
    
        // Perform the injection with default settings (inject into the beginning before the first instruction)
        injector.Inject();
    
        // More injections or saving the target assembly...
       }
    }
    
    0 讨论(0)
  • 2020-11-22 16:34

    There exists a couple of frameworks that allows you to dynamically change any method at runtime (they use the ICLRProfiling interface mentioned by user152949):

    • Prig: Free and Open Source!
    • Microsoft Fakes: Commercial, included in Visual Studio Premium and Ultimate but not Community and Professional
    • Telerik JustMock: Commercial, a "lite" version is available
    • Typemock Isolator: Commercial

    There are also a few frameworks that mocks around with the internals of .NET, these are likely more fragile, and probably can't change inlined code, but on the other hand they are fully self-contained and does not require you to use a custom launcher.

    • Harmony: MIT licensed. Seems to actually have been used sucessfully in a few game mods, supports both .NET and Mono.
    • Deviare In Process Instrumentation Engine: GPLv3 and Commercial. .NET support currently marked as experimental, but on the other hand has the benefit of being commercially backed.
    0 讨论(0)
提交回复
热议问题