É possível compilar e executar dinamicamente fragmentos de código C #?


177

Eu queria saber se é possível salvar fragmentos de código C # em um arquivo de texto (ou qualquer fluxo de entrada) e depois executá-los dinamicamente? Supondo que o que é fornecido a mim compilaria bem em qualquer bloco Main (), é possível compilar e / ou executar esse código? Eu preferiria compilá-lo por razões de desempenho.

No mínimo, eu poderia definir uma interface que eles precisariam implementar, então eles forneceriam uma 'seção' de código que implementasse essa interface.


11
Eu sei que este post tem alguns anos, mas achei que vale a pena mencionar com a introdução do Projeto Roslyn , a capacidade de compilar C # em tempo real e executá-lo em um programa .NET é apenas um pouco mais fácil.
Lawrence

Respostas:


176

A melhor solução em C # / todas as linguagens estáticas do .NET é usar o CodeDOM para essas coisas. (Como observação, seu outro objetivo principal é construir dinamicamente bits de código ou mesmo classes inteiras.)

Aqui está um pequeno exemplo do blog de LukeH , que usa algum LINQ também apenas por diversão.

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

A classe de importância primária aqui é a CSharpCodeProviderque utiliza o compilador para compilar código em tempo real. Se você deseja executar o código, basta usar um pouco de reflexão para carregar dinamicamente o assembly e executá-lo.

Aqui está outro exemplo em C # que (embora um pouco menos conciso) mostra adicionalmente exatamente como executar o código compilado em tempo de execução usando o System.Reflectionespaço para nome.


3
Embora eu duvide que você esteja usando o Mono, pensei que poderia valer a pena ressaltar que existe um espaço para nome Mono.CSharp ( mono-project.com/CSharp_Compiler ) que realmente contém um compilador como um serviço, para que você possa executar dinamicamente o código / avaliação básicos expressões em linha, com o mínimo de problemas.
Noldorin 05/05

1
o que seria uma necessidade do mundo real para fazer isso. Eu sou muito verde na programação em geral e acho que isso é legal, mas não consigo pensar em uma razão pela qual você gostaria / isso seria útil. Obrigado se você pode explicar.
Crash893

1
@ Crash893: Um sistema de script para praticamente qualquer tipo de aplicativo de designer pode fazer bom uso disso. Obviamente, existem alternativas como o IronPython LUA, mas essa certamente é uma. Observe que um sistema de plug-in seria melhor desenvolvido expondo interfaces e carregando DLLs compiladas que contêm implementações delas, em vez de carregar o código diretamente.
Noldorin

Eu sempre pensei em "CodeDom" como a coisa que me permite criar um arquivo de código usando um DOM - um modelo de objeto de documento. No System.CodeDom, existem objetos para representar todos os artefatos que o código inclui - um objeto para uma classe, uma interface, um construtor, uma instrução, uma propriedade, um campo e assim por diante. Posso então construir código usando esse modelo de objeto. O que é mostrado aqui nesta resposta é compilar um arquivo de código, em um programa. Não o CodeDom, embora, como o CodeDom, ele produz dinamicamente um assembly. A analogia: eu posso criar uma página HTML usando o DOM ou usando concats de string.
#

aqui está um artigo para que mostra CodeDom em ação: stackoverflow.com/questions/865052/...
Cheeso

61

Você pode compilar um C # de código na memória e gerar bytes de montagem com o Roslyn. Já foi mencionado, mas vale a pena adicionar aqui um exemplo de Roslyn. A seguir está o exemplo 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();
        }
    }
}

É o mesmo código que o compilador C # usa, que é o maior benefício. Complexo é um termo relativo, mas compilar código em tempo de execução é um trabalho complexo a ser feito de qualquer maneira. No entanto, o código acima não é complexo.
tugberk

41

Outros já deram boas respostas sobre como gerar código em tempo de execução, então pensei em abordar seu segundo parágrafo. Eu tenho alguma experiência com isso e só quero compartilhar uma lição que aprendi dessa experiência.

No mínimo, eu poderia definir uma interface que eles precisariam implementar, então eles forneceriam uma 'seção' de código que implementasse essa interface.

Você pode ter um problema se usar um interfacetipo de base. Se você incluir um único método novo interfaceno futuro, todas as classes fornecidas pelo cliente existentes que implementam o interfaceagora se tornam abstratas, o que significa que você não poderá compilar ou instanciar a classe fornecida pelo cliente em tempo de execução.

Eu tive esse problema quando chegou a hora de adicionar um novo método após cerca de 1 ano de envio da interface antiga e depois de distribuir uma grande quantidade de dados "legados" que precisavam ser suportados. Acabei criando uma nova interface herdada da antiga, mas essa abordagem dificultava o carregamento e a instanciação das classes fornecidas pelo cliente, porque eu tinha que verificar qual interface estava disponível.

Uma solução que pensei na época era usar uma classe real como um tipo de base, como a abaixo. A própria classe pode ser marcada como abstrata, mas todos os métodos devem ser métodos virtuais vazios (não métodos abstratos). Os clientes podem substituir os métodos que desejam e eu posso adicionar novos métodos à classe base sem invalidar o código fornecido pelo cliente.

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

Independentemente de se esse problema se aplicar, você deve considerar a versão da interface entre sua base de código e o código fornecido pelo cliente.


2
essa é uma perspectiva valiosa e útil.
Cheeso

5

Achei isso útil - garante que o Assembly compilado faça referência a tudo o que você fez referência atualmente, pois há uma boa chance de que você desejasse que o C # estivesse compilando para usar algumas classes etc. no código que emite isso:

        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;

No meu caso, eu estava emitindo uma classe, cujo nome estava armazenado em uma string className, que tinha um único método estático público chamado Get(), que retornava com o tipo StoryDataIds. Aqui está como é chamado esse método:

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

Aviso: A compilação pode ser surpreendentemente extremamente lenta. Um pequeno pedaço de código de 10 linhas relativamente simples é compilado com prioridade normal em 2 a 10 segundos em nosso servidor relativamente rápido. Você nunca deve vincular chamadas a CompileAssemblyFromSource()qualquer coisa com expectativas normais de desempenho, como uma solicitação da Web. Em vez disso, compile proativamente o código necessário em um encadeamento de baixa prioridade e tenha uma maneira de lidar com o código que exige que ele esteja pronto, até que tenha a chance de concluir a compilação. Por exemplo, você pode usá-lo em um processo de trabalho em lotes.


Sua resposta é única. Outros não resolvem meu problema.
FindOutIslamNow

3

Para compilar, você pode simplesmente iniciar uma chamada de shell para o compilador csc. Você pode ter uma dor de cabeça tentando manter seus caminhos e opções retos, mas isso certamente pode ser feito.

Exemplos de shell de canto em C #

EDIT : Ou melhor ainda, use o CodeDOM como Noldorin sugeriu ...


Sim, o bom do CodeDOM é que ele pode gerar o assembly para você na memória (além de fornecer mensagens de erro e outras informações em um formato facilmente legível).
Noldorin

3
@Oldoldin, A implementação do C # CodeDOM não gera realmente um assembly na memória. Você pode ativar o sinalizador para ele, mas ele é ignorado. Ele usa um arquivo temporário.
Matthew Olenik 5/05/09

@ Matt: Sim, bom ponto - eu esqueci esse fato. No entanto, ele ainda simplifica bastante o processo (faz parecer efetivamente como se o assembly fosse gerado na memória) e oferece uma interface gerenciada completa, que é muito mais agradável do que lidar com processos.
Noldorin 05/05

Além disso, o CodeDomProvider é apenas uma classe que chama o csc.exe de qualquer maneira.
usar o seguinte código

1

Recentemente, eu precisei gerar processos para testes de unidade. Esta publicação foi útil, pois criei uma classe simples para fazer isso com o código como uma sequência ou o código do meu projeto. Para criar essa classe, você precisará dos pacotes ICSharpCode.Decompilere Microsoft.CodeAnalysisNuGet. Aqui está a turma:

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 usá-lo, chame os Runmétodos como abaixo:

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"); }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.