Замена C# метода объявления типа, который реализует интерфейс и наследуется от базы

Я пытаюсь поменять содержимое метода во время выполнения в целях модульного тестирования унаследованного кода. Я работал с этими SO ответами;

  1. Динамически заменить содержимое метода C#?
  2. Как заменить указатель на переопределенный (виртуальный) метод в указателе моего метода? (Выпуск x64 и x86)

Вот полный пример кода того, что я имею до сих пор.

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

namespace Foo.Bar {

    public interface IFoo {
        string Apple();
    }

    public class Bar {

        protected virtual object One() {
            return null;
        }

        protected virtual object Two() {
            return null;
        }

        protected virtual object Three() {
            return null;
        }

        /* Uncomment this to generate a null reference */
        //protected virtual object Four() {
        //    return null;
        //}

    }

    public class Foo : Bar, IFoo {

        public string Apple() {
            return "Apple";
        }

        public string Orange() {
            return "Orange";
        }

        /* Uncommenting this fixes the null reference */
        //public override int GetHashCode() {
        //    throw new NotImplementedException();
        //}

        public void ReplaceMethod(Delegate targetMethod, Delegate replacementMethod) {

            MethodInfo methodToReplace = targetMethod.Method;
            MethodInfo methodToInject = replacementMethod.Method;

            RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
            RuntimeHelpers.PrepareMethod(methodToInject.MethodHandle);

            if (methodToReplace.IsVirtual)
                ReplaceVirtualInner(methodToReplace, methodToInject);
            else
                ReplaceStandard(methodToReplace, methodToInject);

        }

        private void ReplaceStandard(MethodInfo methodToReplace, MethodInfo methodToInject) {

            IntPtr targetPtr = methodToInject.MethodHandle.Value;
            IntPtr replacePtr = methodToReplace.MethodHandle.Value;

            unsafe
            {
                if (IntPtr.Size == 4) {

                    int* inj = (int*)replacePtr.ToPointer() + 2;
                    int* tar = (int*)targetPtr.ToPointer() + 2;

                    if (Debugger.IsAttached) {

                        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 {
                        *tar = *inj;
                    }
                }
                else {

                    long* inj = (long*)replacePtr.ToPointer() + 1;
                    long* tar = (long*)targetPtr.ToPointer() + 1;

                    if (Debugger.IsAttached) {

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

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

                        *tarSrc = (((long)injInst + 5) + *injSrc) - ((long)tarInst + 5);
                    }
                    else {
                        *tar = *inj;
                    }
                }
            }


        }

        private void ReplaceVirtualInner(MethodInfo methodToReplace, MethodInfo methodToInject) {

            unsafe
            {
                UInt64* methodDesc = (UInt64*)(methodToReplace.MethodHandle.Value.ToPointer());
                int index = (int)(((*methodDesc) >> 32) & 0xFF);

                if (IntPtr.Size == 4) {
                    uint* classStart = (uint*)methodToReplace.DeclaringType.TypeHandle.Value.ToPointer();
                    classStart += 10;
                    classStart = (uint*)*classStart;

                    uint* tar = classStart + index;
                    uint* inj = (uint*)methodToInject.MethodHandle.Value.ToPointer() + 2;

                    if (Debugger.IsAttached) {

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

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

                        *tarSrc = (((uint)injInst + 5) + *injSrc) - ((uint)tarInst + 5);
                    }
                    else {
                        *tar = *inj;
                    }

                }
                else {

                    ulong* classStart = (ulong*)methodToReplace.DeclaringType.TypeHandle.Value.ToPointer();
                    classStart += 8;
                    classStart = (ulong*)*classStart;

                    ulong* tar = classStart + index;
                    ulong* inj = (ulong*)methodToInject.MethodHandle.Value.ToPointer() + 1;

                    if (Debugger.IsAttached) {

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

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

                        *tarSrc = (((ulong)injInst + 5) + *injSrc) - ((ulong)tarInst + 5);
                    }
                    else {
                        *tar = *inj;
                    }

                }

            }
        }

    }

}

Использование;

    Foo.Bar.Foo foo = new Foo.Bar.Foo();

    foo.ReplaceMethod(
        ((Func<string>)foo.Apple),
        ((Func<string>)foo.Orange)
    );

    var result = foo.Apple(); // this is "Orange" :)

Я хорошо разбираюсь в приведенном выше коде, по сути, он находит адрес целевого метода и изменяет значение так, чтобы оно указывало на другое место в памяти. Есть также дополнительное вуду для размещения буферов памяти, добавленных отладчиком.

Виртуальные методы обрабатываются по-разному, что я не совсем понимаю, особенно;

uint* classStart = (uint*)methodToReplace.DeclaringType.TypeHandle.Value.ToPointer();
classStart += 10; /* why 10?? */
classStart = (uint*)*classStart;

Код работает, однако, здесь все становится странным; если базовый класс (декларирующего типа целевого метода) имеет более 3 виртуальных методов, реализует интерфейс и не переопределяет никакие методы, тогда генерируется исключение NullReferenceException.

Пожалуйста, кто-нибудь может объяснить, что происходит, и помочь мне глубже понять код?

1 ответ

То, что вы спрашиваете, на самом деле - это особенность в typemock инфраструктуры модульного тестирования, которая позволяет вам изменить реализацию макетируемого метода на другой в простой строке кода, например:

[TestMethod]
public void TestMethod1()
{
    var real = new Foo();

    Isolate.WhenCalled(() => real.Apple()).DoInstead(x => { return real.Orange(); });

    Assert.AreEqual("Orange", real.Apple());
}

Вы можете узнать больше об этой функции здесь.

Другие вопросы по тегам