Можно ли динамически компилировать и выполнять фрагменты кода C#?

Мне было интересно, можно ли сохранить фрагменты кода C# в текстовый файл (или любой поток ввода), а затем выполнить их динамически? Предполагая, что то, что предоставлено мне, будет нормально компилироваться в любом блоке Main(), возможно ли скомпилировать и / или выполнить этот код? Я бы предпочел скомпилировать его по соображениям производительности.

По крайней мере, я мог бы определить интерфейс, который они должны были бы реализовать, тогда они предоставили бы "раздел" кода, который реализовал этот интерфейс.

8 ответов

Решение

Лучшее решение в C#/ всех статических.NET языках - это использовать CodeDOM для таких целей. (Как примечание, его другое основное назначение - динамическое конструирование битов кода или даже целых классов.)

Вот хороший короткий пример из блога LukeH, в котором также используется LINQ для развлечения.

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

Класс первостепенной важности здесь CSharpCodeProvider который использует компилятор для компиляции кода на лету. Если вы хотите затем запустить код, вам просто нужно немного подумать, чтобы динамически загрузить сборку и выполнить ее.

Вот еще один пример в C#, который (хотя и немного менее лаконично) дополнительно показывает, как именно выполнить код, скомпилированный во время выполнения, используя System.Reflection Пространство имен.

Вы можете скомпилировать кусок C# кода в память и сгенерировать ассемблерные байты с помощью Roslyn. Это уже упоминалось, но здесь стоило бы добавить пример Roslyn. Ниже приведен полный пример:

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)
        {
            SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@"
                using System;

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

            string assemblyName = Path.GetRandomFileName();
            MetadataReference[] references = new MetadataReference[]
            {
                MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
                MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)
            };

            CSharpCompilation compilation = CSharpCompilation.Create(
                assemblyName,
                syntaxTrees: new[] { syntaxTree },
                references: references,
                options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

            using (var ms = new MemoryStream())
            {
                EmitResult result = compilation.Emit(ms);

                if (!result.Success)
                {
                    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
                {
                    ms.Seek(0, SeekOrigin.Begin);
                    Assembly assembly = Assembly.Load(ms.ToArray());

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

Другие уже дали хорошие ответы о том, как генерировать код во время выполнения, поэтому я подумал, что расскажу о вашем втором абзаце. У меня есть некоторый опыт в этом, и я просто хочу поделиться уроком, который я извлек из этого опыта.

По крайней мере, я мог бы определить интерфейс, который они должны были бы реализовать, тогда они предоставили бы "раздел" кода, который реализовал этот интерфейс.

У вас могут возникнуть проблемы, если вы используете interface в качестве базового типа. Если вы добавите один новый метод к interface в будущем все существующие клиентские классы, которые реализуют interface теперь станьте абстрактными, то есть вы не сможете скомпилировать или создать экземпляр класса, предоставленного клиентом, во время выполнения.

У меня возникла эта проблема, когда пришло время добавить новый метод примерно через 1 год после доставки старого интерфейса и после распространения большого количества "устаревших" данных, которые необходимо было поддерживать. Я закончил тем, что создал новый интерфейс, который унаследовал от старого, но этот подход усложнил загрузку и создание экземпляров клиентских классов, потому что я должен был проверить, какой интерфейс был доступен.

Одно из решений, о которых я думал в то время, состояло в том, чтобы вместо этого использовать фактический класс в качестве базового типа, такого как приведенный ниже. Сам класс может быть помечен как абстрактный, но все методы должны быть пустыми виртуальными методами (не абстрактными методами). Клиенты могут затем переопределить методы, которые они хотят, и я могу добавить новые методы в базовый класс без аннулирования существующего предоставленного клиентом кода.

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

Независимо от того, применима ли эта проблема, вы должны подумать о том, как создать версию интерфейса между вашей кодовой базой и предоставленным клиентом кодом.

Нашел это полезным - гарантирует, что скомпилированная сборка ссылается на все, на что вы ссылались в данный момент, поскольку есть большая вероятность, что вы хотите, чтобы компилируемый C# использовал некоторые классы и т. Д. В коде, который выдает это:

        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;

В моем случае я излучал класс, имя которого было сохранено в строке, classNameкоторый имел единственный открытый статический метод с именем Get(), который вернулся с типом StoryDataIds, Вот как выглядит этот метод:

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

Недавно мне нужно было порождать процессы для модульного тестирования. Этот пост был полезен, так как я создал простой класс, чтобы сделать это либо с кодом в виде строки, либо с кодом из моего проекта. Чтобы построить этот класс, вам понадобится ICSharpCode.Decompiler а также Microsoft.CodeAnalysis Пакеты NuGet. Вот класс:

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

Чтобы использовать его, позвоните Run методы, как показано ниже:

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

Для компиляции вы можете просто инициировать вызов оболочки для компилятора csc. Вы можете испытывать головную боль, пытаясь удержать свои пути и переключатели прямо, но это, безусловно, можно сделать.

Примеры использования C# Corner Shell

РЕДАКТИРОВАТЬ: Или еще лучше, используйте CodeDOM, как предложил Noldorin...

      using System.CodeDom.Compiler;
using System.Diagnostics;
using Microsoft.CSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Reflection;

namespace ASL
{
    class Program
    {
        [Obsolete]
        static void Main(string[] args)
        {
            string code = @"
                using System;

             namespace First
             {
                public class Program
                {
                  public static void Main()
                    {
                        " +
                        "Console.WriteLine(\"Hello, world!\");"
                        + @"
                    }
                }
             }";
            Console.WriteLine(code);
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters();
            // Reference to System.Drawing library
            parameters.ReferencedAssemblies.Add("System.Drawing.dll");
            // True - memory generation, false - external file generation
            parameters.GenerateInMemory = true;
            // True - exe file generation, false - dll file generation
            parameters.GenerateExecutable = true;
            CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
            if (results.Errors.HasErrors)
            {
                StringBuilder sb = new StringBuilder();

                foreach (CompilerError error in results.Errors)
                {
                    sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText));
                }

                throw new InvalidOperationException(sb.ToString());
            }
            Assembly assembly = results.CompiledAssembly;
            Type program = assembly.GetType("First.Program");
            MethodInfo main = program.GetMethod("Main");
            main.Invoke(null, null);
            Console.ReadLine();
        }
    }
}

Пространство имен System.CodeDom.Compiler должно помочь. Смотрите эту статью

Другие вопросы по тегам