动态替换C#方法的内容?

155
我想要做的是更改 C# 方法被调用时的执行方式,以便我可以编写类似于以下代码的内容:
[Distributed]
public 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;
}

在运行时,我需要能够分析具有分布式属性的方法(这一点我已经可以做到),然后在函数体执行之前和函数返回之后插入代码。更重要的是,我需要在不修改调用 Solve 的代码或在函数开始处(在编译时这样做;在运行时完成是目标)的情况下实现它。
目前,我尝试了这段代码(假设 t 是存储 Solve 的类型,m 是 Solve 的 MethodInfo):
private void WrapMethod(Type t, MethodInfo m)
{
    // Generate ILasm for delegate.
    byte[] il = typeof(Dpm).GetMethod("ReplacedSolve").GetMethodBody().GetILAsByteArray();

    // Pin the bytes in the garbage collection.
    GCHandle h = GCHandle.Alloc((object)il, GCHandleType.Pinned);
    IntPtr addr = h.AddrOfPinnedObject();
    int size = il.Length;

    // Swap the method.
    MethodRental.SwapMethodBody(t, m.MetadataToken, addr, size, MethodRental.JitImmediate);
}

public DTask<bool> ReplacedSolve(int n, DEvent<bool> callback)
{
    Console.WriteLine("This was executed instead!");
    return true;
}

然而,MethodRental.SwapMethodBody仅适用于动态模块,不能使用在已编译并存储在程序集中的模块。
因此,我正在寻找一种有效的方法来对一个已存储在已加载和执行的程序集中的方法进行SwapMethodBody操作。
请注意,如果我必须将该方法完全复制到动态模块中,这也不是问题,但在这种情况下,我需要找到一种方法来复制IL,并更新所有调用Solve()的地方,使它们指向新的副本。

3
无法交换已加载的方法。否则,Spring.Net就不必使用代理和接口来实现奇怪的操作了 :-) 请阅读此问题,它与您的问题有关:https://dev59.com/KnVD5IYBdhLWcg3wTJvF(如果您可以拦截它,则可以类似于交换它... 如果您不能1,那么显然您不能2)。 - xanatos
在这种情况下,是否有一种方法可以将一个方法复制到动态模块中,并更新其余程序集,以便对该方法的调用指向新的副本? - June Rhodes
老生常谈。如果可以轻松完成,所有不同的IoC容器可能都会这样做。他们没有这样做->99%不能完成 :-)(没有可怕和无名的黑客)。有一个希望:他们承诺在C#5.0中进行元编程和异步操作。我们已经看到了异步操作...元编程还没有...但它可能是! - xanatos
1
你确实没有解释为什么想要让自己陷入这么痛苦的事情。 - DanielOfTaebl
9
请看下面我的回答。这是完全可行的。在你不拥有代码和运行时期间都可以实现。我不明白为什么会有那么多人认为这是不可能的。 - Andreas Pardeike
显示剩余4条评论
11个回答

426

声明:Harmony是一个由我撰写并维护的库,我也是这篇文章的作者。

Harmony 2 是一个开源库(MIT许可证),旨在在运行时替换、装饰或修改任何类型的C#方法。它主要关注于使用Mono或.NET编写的游戏和插件。它处理对同一方法的多个更改-它们积累而不是相互覆盖。

它为每个原始方法创建动态替换方法,并向它们发出调用自定义方法的代码以进行处理。它还允许您编写过滤器以处理原始IL代码以及自定义异常处理程序,从而允许更详细地操纵原始方法。

为了完成此过程,它会将简单的汇编跳转写入原始方法的跳板中,指向从编译动态方法生成的汇编。这适用于Windows、macOS上的32/64位以及任何Mono支持的Linux。

文档可以在这里找到。

示例

(来源)

原始代码

public class SomeGameClass
{
    private bool isRunning;
    private int counter;

    private int DoSomething()
    {
        if (isRunning)
        {
            counter++;
            return counter * 10;
        }
    }
}

使用Harmony注释进行修补

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;
    }
}

或者,使用反射手动修补

using SomeGame;
using System.Reflection;
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()
    {
        // ...
    }
}

2
据我所知,它目前还不支持.NET Core 2,在使用AppDomain.CurrentDomain.DefineDynamicAssembly时会出现一些异常。 - Max
是的,马克斯,我还没有时间使用.NET Core 2来构建和测试Harmony。 - Andreas Pardeike
2
我的一位朋友0x0ade提到了一个不太成熟的替代方案,它可以在.NET Core上运行,即NuGet上的MonoMod.RuntimeDetour。 - Andreas Pardeike
2
更新:通过包含对 System.Reflection.Emit 的引用,Harmony 现在可以与 .NET Core 3 编译和测试。 - Andreas Pardeike
2
@rraallvv 要替换一个方法,你需要创建一个前缀,其中包含原始方法的全部参数加上 __instance(如果不是静态方法)和 ref __result,并返回 false 来跳过原始方法。在该方法中,使用__instance并将结果分配给__result,然后返回false。 - Andreas Pardeike
显示剩余8条评论

225

适用于 .NET 4 及以上版本

using System;
using System.Reflection;
using System.Runtime.CompilerServices;


namespace InjectionTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Target targetInstance = new Target();

            targetInstance.test();

            Injection.install(1);
            Injection.install(2);
            Injection.install(3);
            Injection.install(4);

            targetInstance.test();

            Console.Read();
        }
    }

    public class Target
    {
        public void test()
        {
            targetMethod1();
            Console.WriteLine(targetMethod2());
            targetMethod3("Test");
            targetMethod4();
        }

        private void targetMethod1()
        {
            Console.WriteLine("Target.targetMethod1()");

        }

        private string targetMethod2()
        {
            Console.WriteLine("Target.targetMethod2()");
            return "Not injected 2";
        }

        public void targetMethod3(string text)
        {
            Console.WriteLine("Target.targetMethod3("+text+")");
        }

        private void targetMethod4()
        {
            Console.WriteLine("Target.targetMethod4()");
        }
    }

    public class Injection
    {        
        public static void install(int funcNum)
        {
            MethodInfo methodToReplace = typeof(Target).GetMethod("targetMethod"+ funcNum, BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
            MethodInfo methodToInject = typeof(Injection).GetMethod("injectionMethod"+ funcNum, BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
            RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
            RuntimeHelpers.PrepareMethod(methodToInject.MethodHandle);

            unsafe
            {
                if (IntPtr.Size == 4)
                {
                    int* inj = (int*)methodToInject.MethodHandle.Value.ToPointer() + 2;
                    int* tar = (int*)methodToReplace.MethodHandle.Value.ToPointer() + 2;
#if DEBUG
                    Console.WriteLine("\nVersion x86 Debug\n");

                    byte* injInst = (byte*)*inj;
                    byte* tarInst = (byte*)*tar;

                    int* injSrc = (int*)(injInst + 1);
                    int* tarSrc = (int*)(tarInst + 1);

                    *tarSrc = (((int)injInst + 5) + *injSrc) - ((int)tarInst + 5);
#else
                    Console.WriteLine("\nVersion x86 Release\n");
                    *tar = *inj;
#endif
                }
                else
                {

                    long* inj = (long*)methodToInject.MethodHandle.Value.ToPointer()+1;
                    long* tar = (long*)methodToReplace.MethodHandle.Value.ToPointer()+1;
#if DEBUG
                    Console.WriteLine("\nVersion x64 Debug\n");
                    byte* injInst = (byte*)*inj;
                    byte* tarInst = (byte*)*tar;


                    int* injSrc = (int*)(injInst + 1);
                    int* tarSrc = (int*)(tarInst + 1);

                    *tarSrc = (((int)injInst + 5) + *injSrc) - ((int)tarInst + 5);
#else
                    Console.WriteLine("\nVersion x64 Release\n");
                    *tar = *inj;
#endif
                }
            }
        }

        private void injectionMethod1()
        {
            Console.WriteLine("Injection.injectionMethod1");
        }

        private string injectionMethod2()
        {
            Console.WriteLine("Injection.injectionMethod2");
            return "Injected 2";
        }

        private void injectionMethod3(string text)
        {
            Console.WriteLine("Injection.injectionMethod3 " + text);
        }

        private void injectionMethod4()
        {
            System.Diagnostics.Process.Start("calc");
        }
    }

}

21
这篇文章应该获得更多的点赞。虽然我的情况与此不同,但这段话正好能帮我找到正确的方向。谢谢。 - S.C.
2
@Logman 很棒的回答。但我的问题是:调试模式下发生了什么?是否可能只替换一条指令?例如,如果我想将条件跳转替换为无条件跳转,是否可以实现?据我所知,您正在替换已编译的方法,因此很难确定应该替换哪个条件... - Alex Zhukovskiy
2
@AlexZhukovskiy 如果你喜欢的话,可以在Stack上发布并发送给我链接。周末之后我会仔细研究并回答你的问题。Machine,周末之后我也会研究你的问题。 - Logman
2
在使用MSTest进行集成测试时,我注意到两件事:(1)当你在injectionMethod*()内部使用this时,在_编译时_它将引用Injection实例,但在_运行时_将引用Target实例(对于你在注入方法内使用的所有实例成员的引用都是如此)。(2)由于某种原因,当你运行一个已经通过调试编译的测试时,#DEBUG部分只能在_调试_测试时工作,而不能在运行测试时工作。我最终选择了_始终_使用#else部分。我不明白为什么这样做有效,但确实有效。 - Good Night Nerd Pride
2
@Logman 当使用Reflection.Emit.MethodBuilder创建的方法进行注入时,在调试模式下会抛出AccessViolationException(在发布模式下可以正常工作)。有什么想法吗? - Mr Anderson
显示剩余33条评论

27

你可以在运行时修改方法的内容。但是你不应该这样做,强烈建议只在测试目的下这样做。

看一下这个链接:

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

基本上,你可以:

  1. 通过MethodInfo.GetMethodBody().GetILAsByteArray()获取IL方法内容
  2. 搞乱这些字节。

    如果你只想添加或附加一些代码,则只需添加/附加所需的操作码(但要小心保持堆栈的干净)

    以下是“反编译”现有IL的一些提示:

    • 返回的字节是一系列IL指令,后跟它们的参数(如果它们有一些 - 例如,“.调用”有一个参数:被调用的方法标记,“.弹出”没有)
    • IL代码和数组中找到的字节之间的对应关系可以使用OpCodes.YourOpCode.Value找到(它是实际的操作码字节值,保存在你的程序集中)
    • 附加在IL代码后面的参数可能具有不同的大小(从一个到多个字节),具体取决于调用的操作码
    • 通过适当的方法可以找到这些参数所引用的标记。例如,如果你的IL包含“.调用354354”(以十六进制28 00 05 68 32编码,28h=40为“.call”操作码,56832h=354354),则可以使用MethodBase.GetMethodFromHandle(354354)查找相应的被调用方法。
  3. 一旦修改完成,你的IL字节数组可以通过InjectionHelper.UpdateILCodes(MethodInfo method, byte[] ilCodes)重新注入 - 参见上述链接

    这是“不安全”的部分...它运作良好,但这涉及到黑客式攻击CLR内部机制...


9
仅仅是严谨一点,354354(0x00056832)不是一个有效的元数据标记,高字节应该是0x06(MethodDef)、0x0A(MemberRef)或者0x2B(MethodSpec)。此外,元数据标记应该按照小端字节序写入。最后,元数据标记是与模块相关的,MethodInfo.MetadataToken会返回定义模块的标记,如果你想调用一个不在修改的方法所在模块中定义的方法,则无法使用该标记。 - Brian Reichle

16

基于这个问题和另一个答案,我整理出了以下版本:

// Note: This method replaces methodToReplace with methodToInject
// Note: methodToInject will still remain pointing to the same location
public static unsafe MethodReplacementState Replace(this MethodInfo methodToReplace, MethodInfo methodToInject)
        {
//#if DEBUG
            RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
            RuntimeHelpers.PrepareMethod(methodToInject.MethodHandle);
//#endif
            MethodReplacementState state;

            IntPtr tar = methodToReplace.MethodHandle.Value;
            if (!methodToReplace.IsVirtual)
                tar += 8;
            else
            {
                var index = (int)(((*(long*)tar) >> 32) & 0xFF);
                var classStart = *(IntPtr*)(methodToReplace.DeclaringType.TypeHandle.Value + (IntPtr.Size == 4 ? 40 : 64));
                tar = classStart + IntPtr.Size * index;
            }
            var inj = methodToInject.MethodHandle.Value + 8;
#if DEBUG
            tar = *(IntPtr*)tar + 1;
            inj = *(IntPtr*)inj + 1;
            state.Location = tar;
            state.OriginalValue = new IntPtr(*(int*)tar);

            *(int*)tar = *(int*)inj + (int)(long)inj - (int)(long)tar;
            return state;

#else
            state.Location = tar;
            state.OriginalValue = *(IntPtr*)tar;
            * (IntPtr*)tar = *(IntPtr*)inj;
            return state;
#endif
        }
    }

    public struct MethodReplacementState : IDisposable
    {
        internal IntPtr Location;
        internal IntPtr OriginalValue;
        public void Dispose()
        {
            this.Restore();
        }

        public unsafe void Restore()
        {
#if DEBUG
            *(int*)Location = (int)OriginalValue;
#else
            *(IntPtr*)Location = OriginalValue;
#endif
        }
    }

目前这个是最佳答案。 - Evgeny Gorbovoy
添加一个使用示例会很有帮助。 - kofifus
1
太棒了!我刚试了一下,它有效0 但我想知道它是如何工作的。你能告诉我一些关于它的信息吗?可以给我一个链接或话题来查找答案吗? - south
不适用于动态生成的methodToInject。会抛出各种异常,如AccessViolationException、内部CLR错误、SEXException等。 - Evgeny Gorbovoy
哦,抱歉。明确一点,当调试器附加时,它不适用于动态生成的methodToInject。 - Evgeny Gorbovoy
2
有人根据这个答案编写了一个项目,其中解释了一些东西并展示了用法:https://github.com/spinico/MethodRedirect - Tooster

14

如果这个方法是非虚拟的,非泛型的,不在泛型类型中,未被内联并且在x86平台上,则可以替换它:

MethodInfo methodToReplace = ...
RuntimeHelpers.PrepareMetod(methodToReplace.MethodHandle);

var getDynamicHandle = Delegate.CreateDelegate(Metadata<Func<DynamicMethod, RuntimeMethodHandle>>.Type, Metadata<DynamicMethod>.Type.GetMethod("GetMethodDescriptor", BindingFlags.Instance | BindingFlags.NonPublic)) as Func<DynamicMethod, RuntimeMethodHandle>;

var newMethod = new DynamicMethod(...);
var body = newMethod.GetILGenerator();
body.Emit(...) // do what you want.
body.Emit(OpCodes.jmp, methodToReplace);
body.Emit(OpCodes.ret);

var handle = getDynamicHandle(newMethod);
RuntimeHelpers.PrepareMethod(handle);

*((int*)new IntPtr(((int*)methodToReplace.MethodHandle.Value.ToPointer() + 2)).ToPointer()) = handle.GetFunctionPointer().ToInt32();

//all call on methodToReplace redirect to newMethod and methodToReplace is called in newMethod and you can continue to debug it, enjoy.

看起来非常危险。我真的希望没有人在生产代码中使用它。 - Brian Reichle
2
这种技术被应用性能监控(APM)工具使用,并且在生产环境中也被广泛采用。 - Martin Kersten
1
谢谢您的回复,我正在开发一个项目,以提供这种能力作为面向切面编程API。我解决了在x86和x64上管理虚拟方法和泛型方法的限制。如果您需要更多细节,请告诉我。 - Teter28
7
类元数据是指描述类的数据,包括类的名称、访问修饰符、父类、实现的接口以及其他注释信息等。它可以在运行时被访问和操作,常用于框架和库中。 - Sebastian
这个答案是伪代码并且已经过时了。其中许多方法已经不存在了。 - N-ate
@Teter28,我正在尝试弄清楚如何替换泛型中的方法。我知道这在泛型中不起作用,但你知道为什么吗? - johnny 5

10

存在一些框架可以让你在运行时动态更改任何方法(它们使用用户152949提到的ICLRProfiling接口):

  • Microsoft Fakes:商业软件,包含在Visual Studio Premium和Ultimate中,但不包括Community和Professional版本
  • Telerik JustMock:商业软件,有“精简版”可用
  • Typemock Isolator:商业软件
  • Prig:免费且开源,但自2017年以来未得到维护

还有一些框架可以模拟.NET的内部操作,这些框架可能更加脆弱,可能无法更改内联代码,但另一方面它们完全自包含,不需要使用自定义启动器。

  • Harmony:采用MIT许可证。似乎已成功地在一些游戏修改中使用,支持.NET和Mono。
  • [Pose][7]:采用MIT许可证,但自2021年以来没有更新。
  • Deviare In Process Instrumentation Engine:采用GPLv3和商业许可证。目前标记为实验性的.NET支持,但另一方面享有商业支持的好处。不幸的是,自2020年以来没有更新。

8

Logman的解决方案,但是具有交换方法体的接口。另外,还有一个更简单的例子。

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;
        }
    }
}

1
这个错误提示是:在 MA.ELCalc.FunctionalTests.dll 中发生了类型为 'System.AccessViolationException' 的异常,但未在用户代码中处理。额外的信息是:尝试读取或写入受保护的内存。这通常表明其他内存已损坏。当替换 getter 时出现此问题。 - N-ate
我收到了异常信息:“wapMethodBodies尚未处理8个字节的IntPtr大小”。 - Phong Dao

8

根据TakeMeAsAGuest的答案,这是一个类似的扩展程序,不需要使用unsafe块。

这是Extensions类:

using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace MethodRedirect
{
    static class Extensions
    { 
        public static void RedirectTo(this MethodInfo origin, MethodInfo target)
        {
            IntPtr ori = GetMethodAddress(origin);
            IntPtr tar = GetMethodAddress(target);
         
            Marshal.Copy(new IntPtr[] { Marshal.ReadIntPtr(tar) }, 0, ori, 1);
        }

        private static IntPtr GetMethodAddress(MethodInfo mi)
        {
            const ushort SLOT_NUMBER_MASK = 0xffff; // 2 bytes mask
            const int MT_OFFSET_32BIT = 0x28;       // 40 bytes offset
            const int MT_OFFSET_64BIT = 0x40;       // 64 bytes offset

            IntPtr address;

            // JIT compilation of the method
            RuntimeHelpers.PrepareMethod(mi.MethodHandle);

            IntPtr md = mi.MethodHandle.Value;             // MethodDescriptor address
            IntPtr mt = mi.DeclaringType.TypeHandle.Value; // MethodTable address

            if (mi.IsVirtual)
            {
                // The fixed-size portion of the MethodTable structure depends on the process type
                int offset = IntPtr.Size == 4 ? MT_OFFSET_32BIT : MT_OFFSET_64BIT;

                // First method slot = MethodTable address + fixed-size offset
                // This is the address of the first method of any type (i.e. ToString)
                IntPtr ms = Marshal.ReadIntPtr(mt + offset);

                // Get the slot number of the virtual method entry from the MethodDesc data structure
                long shift = Marshal.ReadInt64(md) >> 32;
                int slot = (int)(shift & SLOT_NUMBER_MASK);
                
                // Get the virtual method address relative to the first method slot
                address = ms + (slot * IntPtr.Size);                                
            }
            else
            {
                // Bypass default MethodDescriptor padding (8 bytes) 
                // Reach the CodeOrIL field which contains the address of the JIT-compiled code
                address = md + 8;
            }

            return address;
        }
    }
}

下面是一个简单的使用示例:

using System;
using System.Reflection;

namespace MethodRedirect
{
    class Scenario
    {    
      static void Main(string[] args)
      {
          Assembly assembly = Assembly.GetAssembly(typeof(Scenario));
          Type Scenario_Type = assembly.GetType("MethodRedirect.Scenario");

          MethodInfo Scenario_InternalInstanceMethod = Scenario_Type.GetMethod("InternalInstanceMethod", BindingFlags.Instance | BindingFlags.NonPublic);
          MethodInfo Scenario_PrivateInstanceMethod = Scenario_Type.GetMethod("PrivateInstanceMethod", BindingFlags.Instance | BindingFlags.NonPublic);

          Scenario_InternalInstanceMethod.RedirectTo(Scenario_PrivateInstanceMethod);

          // Using dynamic type to prevent method string caching
          dynamic scenario = (Scenario)Activator.CreateInstance(Scenario_Type);

          bool result = scenario.InternalInstanceMethod() == "PrivateInstanceMethod";

          Console.WriteLine("\nRedirection {0}", result ? "SUCCESS" : "FAILED");

          Console.ReadKey();
      }

      internal string InternalInstanceMethod()
      {
          return "InternalInstanceMethod";
      }

      private string PrivateInstanceMethod()
      {
          return "PrivateInstanceMethod";
      }
    }
}

这是从我在Github上提供的一个更详细的项目中提炼出来的 (MethodRedirect)。
备注:该代码是使用.NET Framework 4实现的,尚未在较新版本的.NET上进行测试。

这是否允许用Action或普通方法替换属性setter方法?此外,我们能否用Action替换构造函数?使用我的代码(.NET 6.0),我已经成功交换了构造函数,但似乎无法重新指向setter。谢谢! - N-ate
从这个例子来看,在Net Core中,恢复令牌操作似乎无法正常工作。 - Red Riding Hood
对于属性访问器重定向(Get或Set),您可以使用反射从中获取相应的PropertyInfo,然后根据需要使用GetGetMethod和/或GetSetMethod获取相应的MethodInfo - Spinicoffee

5

3
我知道这不是你问题的确切答案,但通常的做法是使用工厂/代理方法。
首先,我们声明一个基本类型。
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;
    }
}

然后我们可以声明一个派生类型(称之为代理)。
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();

派生类型也可以在运行时生成。
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(...);

唯一的性能损失在派生对象构造时发生,第一次构造速度较慢,因为它将使用大量反射和反射发射。而其他时间只需并行表查找和构造函数代价即可。正如所说,您可以使用优化来进行构建。
ConcurrentDictionary<Type, Func<object>>.

1
嗯...这仍然需要程序员积极意识到分布式处理方面的工作;我正在寻找一种解决方案,只依赖于他们在方法上设置[Distributed]属性(而不是从ContextBoundObject继承或子类化)。看起来我可能需要使用Mono.Cecil之类的工具对程序集进行编译后修改。 - June Rhodes
我不会说这是通常的方法。这种方法在所需技能方面很简单(无需理解CLR),但需要为每个替换的方法/类重复相同的步骤。如果以后您想要更改某些内容(例如,在之前执行一些代码,而不仅仅是之后),则必须进行N次操作(与不安全代码相比,后者只需要进行一次操作)。因此,这是一个需要N小时完成的工作,而不是1小时的工作。 - Evgeny Gorbovoy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接