Remplacer dynamiquement le contenu d'une méthode C #?

110

Ce que je veux faire, c'est changer la façon dont une méthode C # s'exécute lorsqu'elle est appelée, afin que je puisse écrire quelque chose comme ceci:

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

Au moment de l'exécution, je dois pouvoir analyser les méthodes qui ont l'attribut Distributed (ce que je peux déjà faire), puis insérer du code avant que le corps de la fonction ne s'exécute et après le retour de la fonction. Plus important encore, je dois être capable de le faire sans modifier le code où Solve est appelé ou au début de la fonction (au moment de la compilation; l'objectif est de le faire au moment de l'exécution).

Pour le moment, j'ai essayé ce bit de code (supposons que t est le type dans lequel Solve est stocké et m est un MethodInfo de Solve) :

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

Cependant, MethodRental.SwapMethodBody ne fonctionne que sur les modules dynamiques; pas ceux qui ont déjà été compilés et stockés dans l'assembly.

Je cherche donc un moyen de faire efficacement SwapMethodBody sur une méthode qui est déjà stockée dans un assembly chargé et en cours d'exécution .

Notez que ce n'est pas un problème si je dois copier complètement la méthode dans un module dynamique, mais dans ce cas, je dois trouver un moyen de copier à travers l'IL ainsi que de mettre à jour tous les appels à Solve () de sorte qu'ils pointerait vers la nouvelle copie.

Juin Rhodes
la source
3
Impossible d'échanger les méthodes déjà chargées. Sinon Spring.Net n'aurait pas à faire des choses étranges avec des proxies et des interfaces :-) Lisez cette question, c'est tangent à votre problème: stackoverflow.com/questions/25803/... (si vous pouvez l'intercepter, vous pouvez quelque chose comme -swap it ... Si vous ne pouvez pas 1 alors clairement vous ne pouvez pas 2).
xanatos le
Dans ce cas, existe-t-il un moyen de copier une méthode dans un module dynamique et de mettre à jour le reste de l'assembly de sorte que les appels à cette méthode pointent vers la nouvelle copie?
Juin Rhodes
Même vieux. Si cela pouvait être fait facilement, tous les différents conteneurs IoC le feraient probablement. Ils ne le font pas -> 99%, cela ne peut pas être fait :-) (sans hacks terribles et innombrables). Il y a un seul espoir: ils ont promis la métaprogrammation et l'asynchrone en C # 5.0. Async nous l'avons vu ... Metaprogramming rien ... MAIS ça pourrait être ça!
xanatos le
1
Vous n'avez vraiment pas expliqué pourquoi vous voulez vous laisser aller à quelque chose d'aussi douloureux.
DanielOfTaebl
6
Veuillez voir ma réponse ci-dessous. C'est tout à fait possible. Sur du code que vous ne possédez pas et pendant l'exécution. Je ne comprends pas pourquoi tant de gens pensent que ce n'est pas possible.
Andreas Pardeike

Réponses:

203

Divulgation: Harmony est une bibliothèque qui a été écrite et est maintenue par moi, l'auteur de cet article.

Harmony 2 est une bibliothèque open source (licence MIT) conçue pour remplacer, décorer ou modifier les méthodes C # existantes de tout type pendant l'exécution. Il se concentre principalement sur les jeux et les plugins écrits en Mono ou .NET. Il prend en charge plusieurs modifications apportées à la même méthode - elles s'accumulent au lieu de se remplacer.

Il crée des méthodes de remplacement dynamiques pour chaque méthode d'origine et leur émet du code qui appelle des méthodes personnalisées au début et à la fin. Il vous permet également d'écrire des filtres pour traiter le code IL d'origine et des gestionnaires d'exceptions personnalisés, ce qui permet une manipulation plus détaillée de la méthode d'origine.

Pour terminer le processus, il écrit un simple saut d'assembleur dans le trampoline de la méthode d'origine qui pointe vers l'assembleur généré à partir de la compilation de la méthode dynamique. Cela fonctionne pour 32 / 64Bit sur Windows, macOS et tout Linux pris en charge par Mono.

La documentation peut être trouvée ici .

Exemple

( Source )

Code d'origine

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

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

Patcher avec les annotations 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;
    }
}

Alternativement, patching manuel avec réflexion

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()
    {
        // ...
    }
}
Andreas Pardeike
la source
J'ai jeté un œil au code source, très intéressant! Pouvez-vous expliquer (ici et / ou dans la documentation) comment fonctionnent les instructions spécifiques qui sont utilisées pour effectuer le saut (in Memory.WriteJump)?
Tom
Pour répondre partiellement à mon propre commentaire: 48 B8 <QWord>déplace une valeur immédiate QWord vers rax, puis FF E0est jmp rax- tout est clair là-bas! Ma question restante concerne le E9 <DWord>cas (un saut de près): il semble que dans ce cas le saut de près soit conservé et la modification soit sur la cible du saut; Quand Mono génère-t-il un tel code en premier lieu et pourquoi bénéficie-t-il de ce traitement spécial?
Tom
1
Autant que je sache, il ne prend pas encore en charge .NET Core 2, obtenant quelques exceptions avec AppDomain.CurrentDomain.DefineDynamicAssembly
Max
1
Un de mes amis, 0x0ade m'a mentionné qu'il existe une alternative moins mature qui fonctionne sur .NET Core, à savoir MonoMod.RuntimeDetour sur NuGet.
Andreas Pardeike
1
Mise à jour: en incluant une référence à System.Reflection.Emit, Harmony compile et teste désormais OK avec .NET Core 3
Andreas Pardeike
181

Pour .NET 4 et supérieur

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

}
Bûcheron
la source
14
Cela mérite tant d'autres votes positifs. J'ai un scénario complètement différent, mais cet extrait de code est exactement ce dont j'avais besoin pour me mettre dans la bonne direction. Merci.
SC
2
@Logman excellente réponse. Mais ma question est: que se passe-t-il en mode débogage? Et est-il possible de remplacer une seule instruction? Par exemple, si je veux remplacer le saut conditionnel par un saut inconditionnel? AFAIK vous remplacez la méthode compilée, il n'est donc pas facile de déterminer quelle condition nous devrions remplacer ...
Alex Zhukovskiy
2
@AlexZhukovskiy si vous aimez le poster sur la pile et m'envoyer le lien. Je vais l'examiner et vous donner une réponse après le week-end. Machine Je vais aussi examiner votre question après le week-end.
Logman
2
Deux choses que j'ai remarquées en faisant cela pour un test d'intégration avec MSTest: (1) Lorsque vous utilisez à l' thisintérieur, injectionMethod*()il référencera une Injectioninstance pendant la compilation , mais une Targetinstance pendant l' exécution (cela est vrai pour toutes les références aux membres d'instance que vous utilisez dans un injecté méthode). (2) Pour une raison quelconque, la #DEBUGpartie ne fonctionnait que lors du débogage d' un test, mais pas lors de l' exécution d' un test compilé par débogage. J'ai fini par toujours utiliser la #elsepièce. Je ne comprends pas pourquoi cela fonctionne mais c'est le cas.
Good Night Nerd Pride
2
très agréable. il est temps de tout casser! @GoodNightNerdPride utilise Debugger.IsAttachedau lieu du #if préprocesseur
M.kazem Akhgary
25

Vous POUVEZ modifier le contenu d'une méthode lors de l'exécution. Mais vous n'êtes pas censé le faire, et il est fortement recommandé de le conserver à des fins de test.

Jetez un œil à:

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

En gros, vous pouvez:

  1. Récupère le contenu de la méthode IL via MethodInfo.GetMethodBody (). GetILAsByteArray ()
  2. Mess avec ces octets.

    Si vous souhaitez simplement ajouter ou ajouter du code, il vous suffit de préprendre / ajouter les opcodes que vous voulez (faites attention à ne pas laisser la pile propre, cependant)

    Voici quelques conseils pour "décompiler" l'IL existant:

    • Les octets renvoyés sont une séquence d'instructions IL, suivies de leurs arguments (s'ils en ont - par exemple, '.call' a un argument: le jeton de méthode appelée et '.pop' n'en a aucun)
    • La correspondance entre les codes IL et les octets que vous trouvez dans le tableau renvoyé peut être trouvée en utilisant OpCodes.YourOpCode.Value (qui est la valeur réelle de l'octet opcode telle qu'elle est enregistrée dans votre assembly)
    • Les arguments ajoutés après les codes IL peuvent avoir des tailles différentes (de un à plusieurs octets), selon l'opcode appelé
    • Vous pouvez trouver des jetons auxquels ces arguments font référence via des méthodes appropriées. Par exemple, si votre IL contient ".call 354354" (codé comme 28 00 05 68 32 en hexa, 28h = 40 étant l'opcode '.call' et 56832h = 354354), la méthode appelée correspondante peut être trouvée en utilisant MethodBase.GetMethodFromHandle (354354 )
  3. Une fois modifié, votre tableau d'octets IL peut être réinjecté via InjectionHelper.UpdateILCodes (méthode MethodInfo, byte [] ilCodes) - voir lien mentionné ci-dessus

    C'est la partie "non sûre" ... Cela fonctionne bien, mais cela consiste à pirater les mécanismes CLR internes ...

Olivier
la source
7
Juste pour être pédant, 354354 (0x00056832) n'est pas un jeton de métadonnées valide, l'octet de poids fort doit être 0x06 (MethodDef), 0x0A (MemberRef) ou 0x2B (MethodSpec). En outre, le jeton de métadonnées doit être écrit dans l'ordre des octets petit boutiste. Enfin, le jeton de métadonnées est spécifique au module et MethodInfo.MetadataToken renverra le jeton du module de déclaration, le rendant inutilisable si vous souhaitez appeler une méthode non définie dans le même module que la méthode que vous modifiez.
Brian Reichle
13

vous pouvez le remplacer si la méthode est non virtuelle, non générique, pas de type générique, non intégrée et sur plateforme 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.
Teter28
la source
Cela semble fou dangereux. J'espère vraiment que personne ne l'utilise dans le code de production.
Brian Reichle
2
Cela est utilisé par les outils de surveillance des performances des applications (APM) et est également utilisé en production.
Martin Kersten
1
Merci pour la réponse, je travaille sur un projet pour offrir ce type de capacité en tant qu'API de programmation orientée aspect. J'ai résolu ma limitation pour gérer la méthode virtuelle et la méthode générique à la fois sur x86 et x64. Faites-moi savoir si vous avez besoin de plus de détails.
Teter28
6
Qu'est-ce que les métadonnées de classe?
Sebastian le
Cette réponse est un pseudo-code et est obsolète. Beaucoup de méthodes n'existent plus.
N-mangé
9

Il existe quelques frameworks qui vous permettent de changer dynamiquement n'importe quelle méthode au moment de l'exécution (ils utilisent l'interface ICLRProfiling mentionnée par user152949):

Il existe également quelques frameworks qui se moquent des internes de .NET, ceux-ci sont probablement plus fragiles et ne peuvent probablement pas modifier le code intégré, mais d'un autre côté, ils sont entièrement autonomes et ne vous obligent pas à utiliser un lanceur personnalisé.

  • Harmony : sous licence MIT. Semble avoir été utilisé avec succès dans quelques mods de jeu, prend en charge à la fois .NET et Mono.
  • Moteur d'instrumentation Deviare In Process : GPLv3 et commercial. Le support .NET actuellement marqué comme expérimental, mais d'un autre côté a l'avantage d'être soutenu commercialement.
poizan42
la source
8

Solution de Logman , mais avec une interface pour permuter les corps de méthode. Aussi, un exemple plus simple.

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;
        }
    }
}
C. McCoy IV
la source
1
Cela m'a donné: Une exception de type «System.AccessViolationException» s'est produite dans MA.ELCalc.FunctionalTests.dll mais n'a pas été gérée dans le code utilisateur Informations supplémentaires: Tentative de lecture ou d'écriture de la mémoire protégée. Cela indique souvent qu'une autre mémoire est corrompue. ,,, Lors du remplacement d'un getter.
N-mangé
J'ai eu l'exception "wapMethodBodies ne gère pas encore la taille IntPtr de 8"
Phong Dao
6

Sur la base de la réponse à cette question et à une autre, ive a proposé cette version rangée:

// 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
        }
    }
TakeMeAsAGuest
la source
Pour le moment, c'est la meilleure réponse
Eugene Gorbovoy
serait utile d'ajouter un exemple d'utilisation
kofifus
3

Je sais que ce n'est pas la réponse exacte à votre question, mais la façon habituelle de le faire consiste à utiliser l'approche usines / proxy.

Nous déclarons d'abord un type de base.

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

Ensuite, nous pouvons déclarer un type dérivé (appelez-le 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();

Le type dérivé peut également être généré lors de l'exécution.

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(...);

La seule perte de performances est lors de la construction de l'objet dérivé, la première fois est assez lente car elle utilisera beaucoup de réflexion et de réflexion émise. Toutes les autres fois, il s'agit du coût d'une recherche de table simultanée et d'un constructeur. Comme dit, vous pouvez optimiser la construction en utilisant

ConcurrentDictionary<Type, Func<object>>.
Salvatore Previti
la source
1
Hmm .. cela nécessite toujours un travail de la part du programmeur pour être activement conscient du traitement distribué; Je cherchais une solution qui ne repose que sur la définition de l'attribut [Distributed] sur la méthode (et non sur le sous-classement ou l'héritage de ContextBoundObject). On dirait que je pourrais avoir besoin de faire quelques modifications post-compilation sur les assemblys en utilisant Mono.Cecil ou quelque chose comme ça.
Juin Rhodes
Je ne dirais pas que c'est la manière habituelle. Cette méthode est simple en termes de compétences requises (pas besoin de comprendre CLR), mais elle nécessite de répéter les mêmes étapes pour chacune des méthodes / classes remplacées. Si plus tard vous voulez changer quelque chose (par exemple, exécuter du code après, pas seulement avant), vous devrez le faire N fois (contrairement au code non sécurisé qui nécessite de le faire une fois). Donc c'est N heures de travail vs 1 heure de travail)
Eugene Gorbovoy