Est-il possible de compiler et d'exécuter dynamiquement des fragments de code C #?


177

Je me demandais s'il est possible d'enregistrer des fragments de code C # dans un fichier texte (ou n'importe quel flux d'entrée), puis de les exécuter dynamiquement? En supposant que ce qui m'est fourni serait bien compilé dans n'importe quel bloc Main (), est-il possible de compiler et / ou d'exécuter ce code? Je préférerais le compiler pour des raisons de performances.

À tout le moins, je pourrais définir une interface qu'ils seraient tenus de mettre en œuvre, puis ils fourniraient une «section» de code qui implémentait cette interface.


11
Je sais que cet article date de quelques années, mais j'ai pensé qu'il valait la peine d'être mentionné avec l'introduction de Project Roslyn , la possibilité de compiler du C # brut à la volée et de l'exécuter dans un programme .NET est juste un peu plus facile.
Lawrence

Réponses:


176

La meilleure solution en C # / tous les langages .NET statiques consiste à utiliser le CodeDOM pour de telles choses. (À noter, son autre objectif principal est de construire dynamiquement des bits de code, voire des classes entières.)

Voici un joli petit exemple tiré du blog de LukeH , qui utilise également du LINQ pour le plaisir.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CSharp;
using System.CodeDom.Compiler;

class Program
{
    static void Main(string[] args)
    {
        var csc = new CSharpCodeProvider(new Dictionary<string, string>() { { "CompilerVersion", "v3.5" } });
        var parameters = new CompilerParameters(new[] { "mscorlib.dll", "System.Core.dll" }, "foo.exe", true);
        parameters.GenerateExecutable = true;
        CompilerResults results = csc.CompileAssemblyFromSource(parameters,
        @"using System.Linq;
            class Program {
              public static void Main(string[] args) {
                var q = from i in Enumerable.Range(1,100)
                          where i % 2 == 0
                          select i;
              }
            }");
        results.Errors.Cast<CompilerError>().ToList().ForEach(error => Console.WriteLine(error.ErrorText));
    }
}

La classe de première importance ici est celle CSharpCodeProviderqui utilise le compilateur pour compiler du code à la volée. Si vous souhaitez ensuite exécuter le code, il vous suffit d'utiliser un peu de réflexion pour charger dynamiquement l'assembly et l'exécuter.

Voici un autre exemple en C # qui (bien que légèrement moins concis) vous montre en outre précisément comment exécuter le code compilé à l'exécution en utilisant l' System.Reflectionespace de noms.


3
Bien que je doute de votre utilisation de Mono, j'ai pensé qu'il pourrait être utile de souligner qu'il existe un espace de noms Mono.CSharp ( mono-project.com/CSharp_Compiler ) qui contient en fait un compilateur en tant que service afin que vous puissiez exécuter dynamiquement du code de base / évaluer expressions en ligne, avec un minimum de tracas.
Noldorin

1
quel serait un réel besoin dans le monde pour ce faire. Je suis assez vert pour la programmation en général et je pense que c'est cool, mais je ne peux pas penser à une raison pour laquelle vous voudriez / ce serait utile. Merci si vous pouvez expliquer.
Crash893

1
@ Crash893: Un système de script pour à peu près n'importe quel type d'application de concepteur pourrait en faire bon usage. Bien sûr, il existe des alternatives telles que IronPython LUA, mais c'est certainement une. Notez qu'un système de plugins serait mieux développé en exposant des interfaces et en chargeant des DLL compilées qui en contiennent des implémentations, plutôt que de charger directement du code.
Noldorin

J'ai toujours pensé à "CodeDom" comme la chose qui me permettait de créer un fichier de code en utilisant un DOM - un modèle objet de document. Dans System.CodeDom, il existe des objets pour représenter tous les artefacts inclus dans le code - un objet pour une classe, pour une interface, pour un constructeur, une instruction, une propriété, un champ, etc. Je peux ensuite construire du code en utilisant ce modèle objet. Ce qui est montré ici dans cette réponse est la compilation d'un fichier de code, dans un programme. Pas CodeDom, bien que comme CodeDom, il produit dynamiquement un assembly. L'analogie: je peux créer une page HTML en utilisant le DOM, ou en utilisant des chaînes concat.
Cheeso

voici un article SO qui montre CodeDom en action: stackoverflow.com/questions/865052/…
Cheeso

61

Vous pouvez compiler un morceau de code C # en mémoire et générer des octets d'assembly avec Roslyn. C'est déjà mentionné mais cela vaudrait la peine d'ajouter un exemple de Roslyn pour cela ici. Voici l'exemple complet:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;

namespace RoslynCompileSample
{
    class Program
    {
        static void Main(string[] args)
        {
            // define source code, then parse it (to the type used for compilation)
            SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@"
                using System;

                namespace RoslynCompileSample
                {
                    public class Writer
                    {
                        public void Write(string message)
                        {
                            Console.WriteLine(message);
                        }
                    }
                }");

            // define other necessary objects for compilation
            string assemblyName = Path.GetRandomFileName();
            MetadataReference[] references = new MetadataReference[]
            {
                MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
                MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)
            };

            // analyse and generate IL code from syntax tree
            CSharpCompilation compilation = CSharpCompilation.Create(
                assemblyName,
                syntaxTrees: new[] { syntaxTree },
                references: references,
                options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

            using (var ms = new MemoryStream())
            {
                // write IL code into memory
                EmitResult result = compilation.Emit(ms);

                if (!result.Success)
                {
                    // handle exceptions
                    IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic => 
                        diagnostic.IsWarningAsError || 
                        diagnostic.Severity == DiagnosticSeverity.Error);

                    foreach (Diagnostic diagnostic in failures)
                    {
                        Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
                    }
                }
                else
                {
                    // load this 'virtual' DLL so that we can use
                    ms.Seek(0, SeekOrigin.Begin);
                    Assembly assembly = Assembly.Load(ms.ToArray());

                    // create instance of the desired class and call the desired function
                    Type type = assembly.GetType("RoslynCompileSample.Writer");
                    object obj = Activator.CreateInstance(type);
                    type.InvokeMember("Write",
                        BindingFlags.Default | BindingFlags.InvokeMethod,
                        null,
                        obj,
                        new object[] { "Hello World" });
                }
            }

            Console.ReadLine();
        }
    }
}

C'est le même code que le compilateur C # utilise, ce qui est le plus grand avantage. Complex est un terme relatif, mais la compilation de code à l'exécution est un travail complexe à faire de toute façon. Cependant, le code ci-dessus n'est pas du tout complexe.
tugberk

41

D'autres ont déjà donné de bonnes réponses sur la façon de générer du code au moment de l'exécution, alors j'ai pensé que j'aborderais votre deuxième paragraphe. J'ai une certaine expérience dans ce domaine et je veux juste partager une leçon que j'ai tirée de cette expérience.

À tout le moins, je pourrais définir une interface qu'ils seraient tenus de mettre en œuvre, puis ils fourniraient une «section» de code qui implémentait cette interface.

Vous pouvez rencontrer un problème si vous utilisez un interfacecomme type de base. Si vous ajoutez une seule nouvelle méthode à l' interfaceavenir, toutes les classes existantes fournies par le client qui implémentent le interfacenow deviennent abstraites, ce qui signifie que vous ne pourrez pas compiler ou instancier la classe fournie par le client au moment de l'exécution.

J'ai eu ce problème quand est venu le temps d'ajouter une nouvelle méthode après environ 1 an d'expédition de l'ancienne interface et après avoir distribué une grande quantité de données "héritées" qui devaient être prises en charge. J'ai fini par créer une nouvelle interface héritée de l'ancienne, mais cette approche a rendu plus difficile le chargement et l'instanciation des classes fournies par le client car je devais vérifier quelle interface était disponible.

Une solution à laquelle j'ai pensé à l'époque était d'utiliser à la place une classe réelle comme type de base tel que celui ci-dessous. La classe elle-même peut être marquée comme abstraite mais toutes les méthodes doivent être des méthodes virtuelles vides (pas des méthodes abstraites). Les clients peuvent alors remplacer les méthodes qu'ils souhaitent et je peux ajouter de nouvelles méthodes à la classe de base sans invalider le code existant fourni par le client.

public abstract class BaseClass
{
    public virtual void Foo1() { }
    public virtual bool Foo2() { return false; }
    ...
}

Indépendamment de la question de savoir si ce problème s'applique, vous devez considérer comment versionner l'interface entre votre base de code et le code fourni par le client.


2
c'est une perspective précieuse et utile.
Cheeso

5

J'ai trouvé cela utile - garantit que l'assembly compilé fait référence à tout ce que vous avez actuellement référencé, car il y a de fortes chances que vous vouliez que le C # que vous compilez utilise certaines classes, etc. dans le code qui émet ceci:

        var refs = AppDomain.CurrentDomain.GetAssemblies();
        var refFiles = refs.Where(a => !a.IsDynamic).Select(a => a.Location).ToArray();
        var cSharp = (new Microsoft.CSharp.CSharpCodeProvider()).CreateCompiler();
        var compileParams = new System.CodeDom.Compiler.CompilerParameters(refFiles);
        compileParams.GenerateInMemory = true;
        compileParams.GenerateExecutable = false;

        var compilerResult = cSharp.CompileAssemblyFromSource(compileParams, code);
        var asm = compilerResult.CompiledAssembly;

Dans mon cas, j'émettais une classe, dont le nom était stocké dans une chaîne className, qui avait une seule méthode statique publique nommée Get(), qui retournait avec type StoryDataIds. Voici à quoi ressemble l'appel de cette méthode:

        var tempType = asm.GetType(className);
        var ids = (StoryDataIds)tempType.GetMethod("Get").Invoke(null, null);

Attention: la compilation peut être étonnamment, extrêmement lente. Un petit morceau de code de 10 lignes relativement simple se compile avec une priorité normale en 2 à 10 secondes sur notre serveur relativement rapide. Vous ne devez jamais lier les appels à CompileAssemblyFromSource()quoi que ce soit avec des attentes de performances normales, comme une demande Web. Au lieu de cela, compilez de manière proactive le code dont vous avez besoin sur un thread de faible priorité et disposez d'un moyen de gérer le code qui nécessite que ce code soit prêt, jusqu'à ce qu'il ait une chance de terminer la compilation. Par exemple, vous pouvez l'utiliser dans un processus de traitement par lots.


Votre réponse est unique. D'autres ne résolvent pas mon problème.
FindOutIslamNow

3

Pour compiler, vous pouvez simplement lancer un appel shell au compilateur csc. Vous pouvez avoir mal à la tête en essayant de garder vos chemins et vos commutateurs droits, mais cela peut certainement être fait.

Exemples de coques d'angle C #

EDIT : Ou mieux encore, utilisez le CodeDOM comme Noldorin l'a suggéré ...


Oui, ce qui est bien avec CodeDOM, c'est qu'il peut générer l'assemblage pour vous en mémoire (ainsi que fournir des messages d'erreur et d'autres informations dans un format facilement lisible).
Noldorin

3
@Noldorin, l'implémentation C # CodeDOM ne génère pas réellement d'assembly en mémoire. Vous pouvez activer l'indicateur pour cela, mais il est ignoré. Il utilise à la place un fichier temporaire.
Matthew Olenik

@Matt: Oui, bon point - j'ai oublié ce fait. Néanmoins, cela simplifie encore grandement le processus (le fait effectivement apparaître comme si l'assemblage était généré en mémoire), et offre une interface gérée complète, ce qui est beaucoup plus agréable que de traiter des processus.
Noldorin

En outre, le CodeDomProvider est simplement une classe qui appelle de toute façon csc.exe.
justin.m.chase

1

J'ai récemment eu besoin de générer des processus pour les tests unitaires. Cet article a été utile car j'ai créé une classe simple pour le faire avec du code sous forme de chaîne ou du code de mon projet. Pour créer cette classe, vous aurez besoin des packages ICSharpCode.Decompileret Microsoft.CodeAnalysisNuGet. Voici la classe:

using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.TypeSystem;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

public static class CSharpRunner
{
   public static object Run(string snippet, IEnumerable<Assembly> references, string typeName, string methodName, params object[] args) =>
      Invoke(Compile(Parse(snippet), references), typeName, methodName, args);

   public static object Run(MethodInfo methodInfo, params object[] args)
   {
      var refs = methodInfo.DeclaringType.Assembly.GetReferencedAssemblies().Select(n => Assembly.Load(n));
      return Invoke(Compile(Decompile(methodInfo), refs), methodInfo.DeclaringType.FullName, methodInfo.Name, args);
   }

   private static Assembly Compile(SyntaxTree syntaxTree, IEnumerable<Assembly> references = null)
   {
      if (references is null) references = new[] { typeof(object).Assembly, typeof(Enumerable).Assembly };
      var mrefs = references.Select(a => MetadataReference.CreateFromFile(a.Location));
      var compilation = CSharpCompilation.Create(Path.GetRandomFileName(), new[] { syntaxTree }, mrefs, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

      using (var ms = new MemoryStream())
      {
         var result = compilation.Emit(ms);
         if (result.Success)
         {
            ms.Seek(0, SeekOrigin.Begin);
            return Assembly.Load(ms.ToArray());
         }
         else
         {
            throw new InvalidOperationException(string.Join("\n", result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error).Select(d => $"{d.Id}: {d.GetMessage()}")));
         }
      }
   }

   private static SyntaxTree Decompile(MethodInfo methodInfo)
   {
      var decompiler = new CSharpDecompiler(methodInfo.DeclaringType.Assembly.Location, new DecompilerSettings());
      var typeInfo = decompiler.TypeSystem.MainModule.Compilation.FindType(methodInfo.DeclaringType).GetDefinition();
      return Parse(decompiler.DecompileTypeAsString(typeInfo.FullTypeName));
   }

   private static object Invoke(Assembly assembly, string typeName, string methodName, object[] args)
   {
      var type = assembly.GetType(typeName);
      var obj = Activator.CreateInstance(type);
      return type.InvokeMember(methodName, BindingFlags.Default | BindingFlags.InvokeMethod, null, obj, args);
   }

   private static SyntaxTree Parse(string snippet) => CSharpSyntaxTree.ParseText(snippet);
}

Pour l'utiliser, appelez les Runméthodes ci-dessous:

void Demo1()
{
   const string code = @"
   public class Runner
   {
      public void Run() { System.IO.File.AppendAllText(@""C:\Temp\NUnitTest.txt"", System.DateTime.Now.ToString(""o"") + ""\n""); }
   }";

   CSharpRunner.Run(code, null, "Runner", "Run");
}

void Demo2()
{
   CSharpRunner.Run(typeof(Runner).GetMethod("Run"));
}

public class Runner
{
   public void Run() { System.IO.File.AppendAllText(@"C:\Temp\NUnitTest.txt", System.DateTime.Now.ToString("o") + "\n"); }
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.