Ist es möglich, C # -Codefragmente dynamisch zu kompilieren und auszuführen?


177

Ich habe mich gefragt, ob es möglich ist, C # -Codefragmente in einer Textdatei (oder einem beliebigen Eingabestream) zu speichern und diese dann dynamisch auszuführen. Unter der Annahme, dass das, was mir zur Verfügung gestellt wird, in jedem Main () -Block gut kompiliert werden kann, ist es möglich, diesen Code zu kompilieren und / oder auszuführen? Ich würde es aus Leistungsgründen lieber kompilieren.

Zumindest könnte ich eine Schnittstelle definieren, die sie implementieren müssten, dann würden sie einen Code-Abschnitt bereitstellen, der diese Schnittstelle implementiert.


11
Ich weiß, dass dieser Beitrag einige Jahre alt ist, aber ich fand es erwähnenswert, dass mit der Einführung von Project Roslyn die Möglichkeit, rohes C # im laufenden Betrieb zu kompilieren und in einem .NET-Programm auszuführen, nur ein bisschen einfacher ist.
Lawrence

Antworten:


176

Die beste Lösung in C # / allen statischen .NET-Sprachen ist die Verwendung des CodeDOM für solche Dinge zu verwenden. (Als Hinweis besteht sein anderer Hauptzweck darin, dynamisch Codebits oder sogar ganze Klassen zu konstruieren.)

Hier ist ein schönes kurzes Beispiel aus LukeHs Blog , in dem auch etwas LINQ nur zum Spaß verwendet wird.

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

Die Klasse von primärer Bedeutung ist hier die, CSharpCodeProviderdie den Compiler verwendet, um Code im laufenden Betrieb zu kompilieren. Wenn Sie den Code dann ausführen möchten, müssen Sie nur ein wenig Nachdenken verwenden, um die Assembly dynamisch zu laden und auszuführen.

Hier ist ein weiteres Beispiel in C #, das Ihnen (obwohl etwas weniger prägnant) zusätzlich genau zeigt, wie Sie den zur Laufzeit kompilierten Code mithilfe des System.ReflectionNamespace ausführen .


3
Obwohl ich bezweifle, dass Sie Mono verwenden, hielt ich es für sinnvoll, darauf hinzuweisen, dass es einen Mono.CSharp-Namespace ( mono-project.com/CSharp_Compiler ) gibt, der tatsächlich einen Compiler als Dienst enthält, damit Sie grundlegenden Code / Evaluate dynamisch ausführen können Ausdrücke inline, mit minimalem Aufwand.
Noldorin

1
Was wäre ein realer Bedarf dafür? Ich bin ziemlich grün im Programmieren im Allgemeinen und ich denke, das ist cool, aber ich kann mir keinen Grund vorstellen, warum Sie wollen / das wäre nützlich. Danke, wenn du es erklären kannst.
Crash893

1
@ Crash893: Ein Skriptsystem für so ziemlich jede Art von Designeranwendung könnte dies gut nutzen. Natürlich gibt es Alternativen wie IronPython LUA, aber dies ist sicherlich eine. Beachten Sie, dass ein Plugin-System besser entwickelt werden kann, indem Schnittstellen verfügbar gemacht und kompilierte DLLs geladen werden, die Implementierungen davon enthalten, anstatt Code direkt zu laden.
Noldorin

Ich habe immer an "CodeDom" gedacht, mit dem ich eine Codedatei mit einem DOM erstellen konnte - einem Dokumentobjektmodell. In System.CodeDom gibt es Objekte, die alle im Code enthaltenen Artefakte darstellen - ein Objekt für eine Klasse, eine Schnittstelle, einen Konstruktor, eine Anweisung, eine Eigenschaft, ein Feld usw. Mit diesem Objektmodell kann ich dann Code erstellen. In dieser Antwort wird hier gezeigt, wie eine Codedatei in einem Programm kompiliert wird. Nicht CodeDom, obwohl es wie CodeDom dynamisch eine Assembly erstellt. Die Analogie: Ich kann eine HTML-Seite mit dem DOM oder mit String-Concats erstellen.
Cheeso

Hier ist ein SO-Artikel, der CodeDom in Aktion zeigt: stackoverflow.com/questions/865052/…
Cheeso

61

Sie können ein Stück C # -Code in den Speicher kompilieren und mit Roslyn Assembly-Bytes generieren . Es ist bereits erwähnt, aber es lohnt sich, hier ein Beispiel für Roslyn hinzuzufügen. Das folgende ist das vollständige Beispiel:

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

Es ist derselbe Code, den der C # -Compiler verwendet, was der größte Vorteil ist. Komplex ist ein relativer Begriff, aber das Kompilieren von Code zur Laufzeit ist ohnehin eine komplexe Aufgabe. Der obige Code ist jedoch überhaupt nicht komplex.
Tugberk

41

Andere haben bereits gute Antworten zum Generieren von Code zur Laufzeit gegeben, daher dachte ich, ich würde Ihren zweiten Absatz ansprechen. Ich habe einige Erfahrungen damit und möchte nur eine Lektion teilen, die ich aus diesen Erfahrungen gelernt habe.

Zumindest könnte ich eine Schnittstelle definieren, die implementiert werden müsste, und dann einen Code-Abschnitt bereitstellen, der diese Schnittstelle implementiert.

Möglicherweise liegt ein Problem vor, wenn Sie einen interfaceals Basistyp verwenden. Wenn Sie interfacein Zukunft alle vorhandenen vom Client bereitgestellten Klassen, die das interfaceJetzt implementieren, um eine einzige neue Methode erweitern, werden sie abstrakt. Dies bedeutet, dass Sie die vom Client bereitgestellte Klasse zur Laufzeit nicht kompilieren oder instanziieren können.

Ich hatte dieses Problem, als es an der Zeit war, nach etwa einem Jahr Versand der alten Schnittstelle und nach der Verteilung einer großen Menge von "Legacy" -Daten, die unterstützt werden mussten, eine neue Methode hinzuzufügen. Am Ende habe ich eine neue Schnittstelle erstellt, die von der alten geerbt wurde, aber dieser Ansatz machte es schwieriger, die vom Client bereitgestellten Klassen zu laden und zu instanziieren, da ich überprüfen musste, welche Schnittstelle verfügbar war.

Eine Lösung, an die ich damals dachte, bestand darin, stattdessen eine tatsächliche Klasse als Basistyp wie den folgenden zu verwenden. Die Klasse selbst kann als abstrakt markiert werden, aber alle Methoden sollten leere virtuelle Methoden sein (keine abstrakten Methoden). Clients können dann die gewünschten Methoden überschreiben, und ich kann der Basisklasse neue Methoden hinzufügen, ohne den vom Client bereitgestellten Code ungültig zu machen.

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

Unabhängig davon, ob dieses Problem auftritt, sollten Sie überlegen, wie Sie die Schnittstelle zwischen Ihrer Codebasis und dem vom Client bereitgestellten Code versionieren.


2
Das ist eine wertvolle, hilfreiche Perspektive.
Cheeso

5

Fand dies nützlich - stellt sicher, dass die kompilierte Assembly auf alles verweist, auf das Sie derzeit verwiesen haben, da es eine gute Chance gibt, dass das C #, das Sie kompilieren, einige Klassen usw. in dem Code verwendet, der dies ausgibt:

        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;

In meinem Fall habe ich eine Klasse ausgegeben, deren Name in einer Zeichenfolge gespeichert war className, die eine einzige öffentliche statische Methode mit dem Namen hatte Get()und mit type zurückgegeben wurde StoryDataIds. So sieht der Aufruf dieser Methode aus:

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

Warnung: Die Kompilierung kann überraschenderweise extrem langsam sein. Ein kleiner, relativ einfacher 10-Zeilen-Codeblock wird mit normaler Priorität in 2-10 Sekunden auf unserem relativ schnellen Server kompiliert. Sie sollten Anrufe niemals CompileAssemblyFromSource()an etwas mit normalen Leistungserwartungen wie einer Webanforderung binden . Kompilieren Sie stattdessen proaktiv den Code, den Sie in einem Thread mit niedriger Priorität benötigen, und haben Sie eine Möglichkeit, mit Code umzugehen, für den dieser Code bereit sein muss, bis die Kompilierung abgeschlossen werden kann. Sie können es beispielsweise in einem Stapeljobprozess verwenden.


Ihre Antwort ist einzigartig. Andere lösen mein Problem nicht.
FindOutIslamNow

3

Zum Kompilieren können Sie einfach einen Shell-Aufruf an den csc-Compiler initiieren. Möglicherweise haben Sie Kopfschmerzen, wenn Sie versuchen, Ihre Pfade und Schalter gerade zu halten, aber dies kann auf jeden Fall getan werden.

Beispiele für C # -Eckenschalen

EDIT : Oder noch besser, verwenden Sie das CodeDOM, wie Noldorin vorgeschlagen hat ...


Ja, das Schöne an CodeDOM ist, dass es die Assembly für Sie im Speicher generieren kann (sowie Fehlermeldungen und andere Informationen in einem leicht lesbaren Format bereitstellt).
Noldorin

3
@Noldorin, Die C # CodeDOM-Implementierung generiert keine Assembly im Speicher. Sie können das Flag dafür aktivieren, es wird jedoch ignoriert. Stattdessen wird eine temporäre Datei verwendet.
Matthew Olenik

@ Matt: Ja, guter Punkt - ich habe diese Tatsache vergessen. Trotzdem vereinfacht es den Prozess erheblich (lässt ihn effektiv so erscheinen, als ob die Baugruppe im Speicher generiert worden wäre) und bietet eine vollständig verwaltete Schnittstelle, die viel besser ist als der Umgang mit Prozessen.
Noldorin

Außerdem ist der CodeDomProvider nur eine Klasse, die ohnehin csc.exe aufruft.
justin.m.chase

1

Ich musste kürzlich Prozesse für Unit-Tests erzeugen. Dieser Beitrag war nützlich, da ich eine einfache Klasse erstellt habe, um dies entweder mit Code als Zeichenfolge oder mit Code aus meinem Projekt zu tun. Um diese Klasse zu erstellen, benötigen Sie die Pakete ICSharpCode.Decompilerund Microsoft.CodeAnalysisNuGet. Hier ist die Klasse:

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

Um es zu verwenden, rufen Sie die folgenden RunMethoden auf:

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"); }
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.