È possibile compilare ed eseguire dinamicamente frammenti di codice C #?


177

Mi chiedevo se è possibile salvare frammenti di codice C # in un file di testo (o qualsiasi flusso di input) e quindi eseguirli dinamicamente? Supponendo che cosa mi viene fornito sarebbe compilare bene all'interno di qualsiasi blocco Main (), è possibile compilare e / o eseguire questo codice? Preferirei compilarlo per motivi di prestazioni.

Per lo meno, potrei definire un'interfaccia che dovrebbero essere implementate, quindi fornirebbero una "sezione" di codice che implementasse quell'interfaccia.


11
So che questo post ha qualche anno, ma ho pensato che valesse la pena menzionare con l'introduzione di Project Roslyn , la possibilità di compilare C # raw al volo ed eseguirlo all'interno di un programma .NET è solo un po 'più semplice.
Lawrence,

Risposte:


176

La migliore soluzione in C # / tutti i linguaggi .NET statici è utilizzare CodeDOM per tali cose. (Come nota, l'altro suo scopo principale è quello di costruire dinamicamente bit di codice, o addirittura intere classi.)

Ecco un breve esempio tratto dal blog di LukeH , che utilizza anche LINQ solo per divertimento.

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 di primaria importanza qui è la CSharpCodeProvider che utilizza il compilatore per compilare il codice al volo. Se si desidera quindi eseguire il codice, è sufficiente utilizzare un po 'di riflessione per caricare dinamicamente l'assembly ed eseguirlo.

Ecco un altro esempio in C # che (sebbene leggermente meno conciso) mostra inoltre esattamente come eseguire il codice compilato in fase di esecuzione usando lo System.Reflectionspazio dei nomi.


3
Anche se dubito che tu stia usando Mono, ho pensato che valesse la pena sottolineare che esiste uno spazio dei nomi Mono.CSharp ( mono-project.com/CSharp_Compiler ) che in realtà contiene un compilatore come servizio in modo da poter eseguire dinamicamente codice / valutazione di base espressioni in linea, con fastidio minimo.
Noldorin,

1
quale sarebbe un mondo reale necessario per farlo. Sono abbastanza verde nella programmazione in generale e penso che sia bello, ma non riesco a pensare a un motivo per cui vorresti / questo sarebbe utile. Grazie se puoi spiegare.
Crash893,

1
@ Crash893: un sistema di script per praticamente qualsiasi tipo di applicazione di progettazione potrebbe farne buon uso. Naturalmente, ci sono alternative come IronPython LUA, ma questa è sicuramente una. Si noti che un sistema di plug-in sarebbe sviluppato meglio esponendo le interfacce e caricando DLL compilate che ne contengono implementazioni, piuttosto che caricare direttamente il codice.
Noldorin,

Ho sempre pensato a "CodeDom" come la cosa che mi ha permesso di creare un file di codice usando un DOM - un modello a oggetti di documento. In System.CodeDom, ci sono oggetti per rappresentare tutti gli artefatti inclusi nel codice: un oggetto per una classe, un'interfaccia, un costruttore, un'istruzione, una proprietà, un campo e così via. Posso quindi costruire il codice usando quel modello a oggetti. Ciò che viene mostrato qui in questa risposta è la compilazione di un file di codice, in un programma. Non CodeDom, sebbene come CodeDom, produce dinamicamente un assembly. L'analogia: posso creare una pagina HTML usando il DOM o usando i concatenamenti di stringhe.
Cheeso,

Ecco un articolo di modo che mostra CodeDOM in azione: stackoverflow.com/questions/865052/...
Cheeso

61

È possibile compilare un pezzo C # di codice in memoria e generare byte di assembly con Roslyn. È già stato menzionato, ma varrebbe la pena aggiungere alcuni esempi di Roslyn per questo qui. Di seguito è riportato l'esempio completo:

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

È lo stesso codice utilizzato dal compilatore C # che rappresenta il vantaggio maggiore. Complesso è un termine relativo, ma compilare il codice in fase di esecuzione è comunque un lavoro complesso. Tuttavia, il codice sopra non è affatto complesso.
Tugberk,

41

Altri hanno già dato buone risposte su come generare codice in fase di esecuzione, quindi ho pensato di affrontare il tuo secondo paragrafo. Ho una certa esperienza con questo e voglio solo condividere una lezione che ho imparato da quell'esperienza.

Per lo meno, potrei definire un'interfaccia che dovrebbero essere implementate, quindi fornirebbero una 'sezione' di codice che implementasse quell'interfaccia.

Potresti avere un problema se usi un interfacetipo di base. Se interfacein futuro aggiungi un singolo nuovo metodo a tutte le classi fornite dal client esistenti che implementano l' interfaceormai diventano astratte, il che significa che non sarai in grado di compilare o creare un'istanza della classe fornita dal client in fase di esecuzione.

Ho avuto questo problema quando è arrivato il momento di aggiungere un nuovo metodo dopo circa 1 anno dalla consegna della vecchia interfaccia e dopo aver distribuito una grande quantità di dati "legacy" che dovevano essere supportati. Ho finito per creare una nuova interfaccia ereditata da quella precedente, ma questo approccio ha reso più difficile caricare e creare un'istanza delle classi fornite dal client perché dovevo verificare quale interfaccia era disponibile.

Una soluzione a cui pensai in quel momento era usare invece una classe reale come tipo base come quella qui sotto. La classe stessa può essere contrassegnata come astratta ma tutti i metodi dovrebbero essere metodi virtuali vuoti (non metodi astratti). I clienti possono quindi sovrascrivere i metodi desiderati e posso aggiungere nuovi metodi alla classe base senza invalidare il codice fornito dal cliente esistente.

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

Indipendentemente dal fatto che questo problema si applichi, è necessario considerare come eseguire la versione dell'interfaccia tra la base di codice e il codice fornito dal client.


2
questa è una prospettiva utile e preziosa.
Cheeso,

5

Trovato utile: assicura che l'Assemblea compilata faccia riferimento a tutto ciò a cui hai fatto riferimento, poiché ci sono buone probabilità che tu volessi che il C # che stai compilando usasse alcune classi ecc. Nel codice che emette questo:

        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;

Nel mio caso stavo emettendo una classe, il cui nome era memorizzato in una stringa className, che aveva un singolo metodo statico pubblico chiamato Get(), che restituiva con il tipo StoryDataIds. Ecco come appare questo metodo:

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

Avvertenza: la compilazione può essere sorprendentemente, estremamente lenta. Un piccolo, relativamente semplice pezzo di codice a 10 righe si compila a priorità normale in 2-10 secondi sul nostro server relativamente veloce. Non dovresti mai legare le chiamate a CompileAssemblyFromSource()qualcosa con normali aspettative di prestazione, come una richiesta web. Invece, compila in modo proattivo il codice che ti serve su un thread a bassa priorità e hai un modo di gestire il codice che richiede che quel codice sia pronto, fino a quando non ha avuto la possibilità di terminare la compilazione. Ad esempio, è possibile utilizzarlo in un processo di processo batch.


La tua risposta è unica Altri non risolvono il mio problema.
FindOutIslamNow,

3

Per compilare potresti semplicemente avviare una chiamata shell al compilatore csc. Potresti avere mal di testa nel tentativo di mantenere i tuoi percorsi e i tuoi passaggi rettilinei, ma sicuramente si può fare.

Esempi di gusci d'angolo C #

EDIT : O meglio ancora, usa CodeDOM come Noldorin ha suggerito ...


Sì, la cosa bella di CodeDOM è che può generare l'assembly per te in memoria (oltre a fornire messaggi di errore e altre informazioni in un formato facilmente leggibile).
Noldorin,

3
@Noldorin, L'implementazione CodeDOM di C # in realtà non genera un assembly in memoria. È possibile abilitare il flag per esso, ma viene ignorato. Utilizza invece un file temporaneo.
Matthew Olenik,

@Matt: Sì, buon punto - ho dimenticato questo fatto. Ciò nonostante, semplifica notevolmente il processo (lo fa apparire efficacemente come se l'assembly fosse generato in memoria) e offre un'interfaccia gestita completa, che è molto più piacevole rispetto ai processi.
Noldorin,

Inoltre, CodeDomProvider è solo una classe che chiama comunque csc.exe.
justin.m.chase

1

Di recente ho dovuto generare processi per test unitari. Questo post è stato utile in quanto ho creato una semplice classe per farlo con il codice come stringa o codice del mio progetto. Per creare questa classe, avrai bisogno dei pacchetti ICSharpCode.Decompilere Microsoft.CodeAnalysisNuGet. Ecco la lezione:

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

Per usarlo, chiama i Runmetodi come di seguito:

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"); }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.