¿Es posible compilar y ejecutar dinámicamente fragmentos de código C #?


177

Me preguntaba si es posible guardar fragmentos de código C # en un archivo de texto (o cualquier flujo de entrada) y luego ejecutarlos dinámicamente. Suponiendo que lo que se me proporciona compilaría bien dentro de cualquier bloque Main (), ¿es posible compilar y / o ejecutar este código? Preferiría compilarlo por razones de rendimiento.

Como mínimo, podría definir una interfaz que deberían implementar, luego proporcionarían una 'sección' de código que implementara esa interfaz.


11
Sé que esta publicación tiene algunos años, pero pensé que valía la pena mencionarla con la introducción del Proyecto Roslyn , la capacidad de compilar C # sin procesar sobre la marcha y ejecutarla dentro de un programa .NET es un poco más fácil.
Lawrence

Respuestas:


176

La mejor solución en C # / todos los lenguajes .NET estáticos es usar CodeDOM para tales cosas. (Como nota, su otro propósito principal es construir dinámicamente bits de código, o incluso clases completas).

Aquí hay un buen ejemplo breve tomado del blog de LukeH , que también usa LINQ solo por diversión.

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 clase de importancia principal aquí es la CSharpCodeProvider que utiliza el compilador para compilar código sobre la marcha. Si desea ejecutar el código, solo necesita usar un poco de reflexión para cargar dinámicamente el ensamblado y ejecutarlo.

Aquí hay otro ejemplo en C # que (aunque un poco menos conciso) también le muestra con precisión cómo ejecutar el código compilado en tiempo de ejecución utilizando el System.Reflectionespacio de nombres.


3
Aunque dudo que esté usando Mono, pensé que podría valer la pena señalar que existe un espacio de nombres Mono.CSharp ( mono-project.com/CSharp_Compiler ) que en realidad contiene un compilador como servicio para que pueda ejecutar dinámicamente código / evaluación básicos expresiones en línea, con una molestia mínima.
Noldorin

1
lo que sería una necesidad del mundo real para hacer esto. Soy bastante verde en la programación en general y creo que esto es genial, pero no puedo pensar en una razón por la que querrías / esto sería útil. Gracias si puedes explicarlo.
Crash893

1
@ Crash893: Un sistema de secuencias de comandos para prácticamente cualquier tipo de aplicación de diseñador podría hacer un buen uso de esto. Por supuesto, hay alternativas como IronPython LUA, pero esta es ciertamente una. Tenga en cuenta que un sistema de complementos se desarrollaría mejor exponiendo interfaces y cargando archivos DLL compilados que contienen implementaciones de ellos, en lugar de cargar el código directamente.
Noldorin

Siempre pensé en "CodeDom" como la cosa que me permitía crear un archivo de código usando un DOM, un modelo de objeto de documento. En System.CodeDom, hay objetos para representar todos los artefactos que incluye el código: un objeto para una clase, una interfaz, un constructor, una declaración, una propiedad, un campo, etc. Entonces puedo construir código usando ese modelo de objeto. Lo que se muestra aquí en esta respuesta es compilar un archivo de código, en un programa. No CodeDom, aunque como CodeDom, produce dinámicamente un ensamblaje. La analogía: puedo crear una página HTML usando el DOM o usando cadenas de concatenos.
Cheeso

Aquí hay un artículo SO que muestra CodeDom en acción: stackoverflow.com/questions/865052/…
Cheeso

61

Puede compilar un fragmento de código C # en la memoria y generar bytes de ensamblaje con Roslyn. Ya se mencionó, pero valdría la pena agregar un ejemplo de Roslyn para esto aquí. El siguiente es el ejemplo 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();
        }
    }
}

Es el mismo código que utiliza el compilador de C #, que es el mayor beneficio. Complejo es un término relativo, pero compilar código en tiempo de ejecución es un trabajo complejo de todos modos. Sin embargo, el código anterior no es complejo en absoluto.
tugberk

41

Otros ya han dado buenas respuestas sobre cómo generar código en tiempo de ejecución, así que pensé que abordaría su segundo párrafo. Tengo algo de experiencia con esto y solo quiero compartir una lección que aprendí de esa experiencia.

Como mínimo, podría definir una interfaz que deberían implementar, luego proporcionarían una 'sección' de código que implementara esa interfaz.

Puede tener un problema si usa un interfacecomo tipo base. Si agrega un nuevo método nuevo interfaceen el futuro, todas las clases existentes suministradas por el cliente que implementan el interfaceahora se vuelven abstractas, lo que significa que no podrá compilar o instanciar la clase proporcionada por el cliente en tiempo de ejecución.

Tuve este problema cuando llegó el momento de agregar un nuevo método después de aproximadamente 1 año de enviar la interfaz anterior y después de distribuir una gran cantidad de datos "heredados" que debían ser compatibles. Terminé haciendo una nueva interfaz que heredó de la anterior, pero este enfoque hizo que fuera más difícil cargar e instanciar las clases proporcionadas por el cliente porque tenía que verificar qué interfaz estaba disponible.

Una solución que pensé en ese momento era utilizar una clase real como tipo base, como la que se muestra a continuación. La clase en sí misma puede marcarse como abstracta, pero todos los métodos deben ser métodos virtuales vacíos (no métodos abstractos). Los clientes pueden anular los métodos que deseen y puedo agregar nuevos métodos a la clase base sin invalidar el código existente proporcionado por el cliente.

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

Independientemente de si este problema se aplica, debe considerar cómo versionar la interfaz entre su base de código y el código proporcionado por el cliente.


2
Esa es una perspectiva valiosa y útil.
Cheeso

5

Encontró esto útil: garantiza que el ensamblado compilado haga referencia a todo lo que ha referenciado actualmente, ya que existe una buena posibilidad de que desee que el C # que está compilando use algunas clases, etc. en el código que emite esto:

        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;

En mi caso, estaba emitiendo una clase, cuyo nombre estaba almacenado en una cadena className, que tenía un único método estático público llamado Get(), que regresaba con type StoryDataIds. Así es como se llama ese método:

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

Advertencia: la compilación puede ser sorprendente, extremadamente lenta. Una pequeña porción relativamente simple de código de 10 líneas compila con prioridad normal en 2-10 segundos en nuestro servidor relativamente rápido. Nunca debe vincular las llamadas a CompileAssemblyFromSource()nada con expectativas de rendimiento normales, como una solicitud web. En cambio, compile proactivamente el código que necesita en un subproceso de baja prioridad y tenga una forma de tratar con el código que requiere que el código esté listo, hasta que haya tenido la oportunidad de terminar la compilación. Por ejemplo, podría usarlo en un proceso de trabajo por lotes.


Tu respuesta es única. Otros no resuelven mi problema.
FindOutIslamNow

3

Para compilar, simplemente puede iniciar una llamada de shell al compilador csc. Es posible que tenga dolor de cabeza al tratar de mantener sus caminos y cambios rectos, pero ciertamente se puede hacer.

C # Corner Shell Ejemplos

EDITAR : O mejor aún, use el CodeDOM como Noldorin sugirió ...


Sí, lo bueno de CodeDOM es que puede generar el ensamblado en la memoria (además de proporcionar mensajes de error y otra información en un formato fácil de leer).
Noldorin

3
@Noldorin, la implementación de C # CodeDOM en realidad no genera un ensamblado en la memoria. Puede habilitar la bandera para ello, pero se ignora. Utiliza un archivo temporal en su lugar.
Matthew Olenik

@ Matt: Sí, buen punto, olvidé ese hecho. No obstante, todavía simplifica enormemente el proceso (lo hace parecer efectivamente como si el ensamblaje se hubiera generado en la memoria) y ofrece una interfaz administrada completa, que es mucho mejor que tratar con procesos.
Noldorin

Además, CodeDomProvider es solo una clase que llama a csc.exe de todos modos.
justin

1

Recientemente necesité generar procesos para pruebas unitarias. Esta publicación fue útil ya que creé una clase simple para hacer eso con el código como una cadena o el código de mi proyecto. Para construir esta clase, necesitará los paquetes ICSharpCode.Decompilery Microsoft.CodeAnalysisNuGet. Aquí está la clase:

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

Para usarlo, llame a los Runmétodos de la siguiente manera:

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"); }
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.