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


177

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

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


11
Я знаю, что этому посту несколько лет, но я подумал, что стоит упомянуть, что с появлением Project Roslyn возможность компилировать raw C # на лету и запускать его в .NET-программе немного проще.
Лоуренс

Ответы:


176

Лучшее решение в 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пространство имен.


3
Хотя я сомневаюсь, что вы используете Mono, я подумал, что стоит указать, что существует пространство имен Mono.CSharp ( mono-project.com/CSharp_Compiler ), которое на самом деле содержит компилятор в качестве службы, так что вы можете динамически запускать базовый код / ​​оценку. выражения встроенные, с минимальными хлопотами.
Нолдорин

1
что было бы в действительности нужно для этого. Я довольно зеленый в программировании в целом, и я думаю, что это круто, но я не могу придумать причину, почему вы захотите / это было бы полезно. Спасибо, если вы можете объяснить.
Crash893

1
@ Crash893: система сценариев для практически любого дизайнерского приложения могла бы хорошо использовать это. Конечно, есть альтернативы, такие как IronPython LUA, но это, безусловно, один. Обратите внимание, что система плагинов была бы лучше разработана, предоставляя интерфейсы и загружая скомпилированные библиотеки DLL, которые содержат их реализации, а не загружая код напрямую.
Нолдорин

Я всегда думал о «CodeDom» как о том, что позволяет мне создавать файл кода, используя DOM - объектную модель документа. В System.CodeDom есть объекты для представления всех артефактов, которые включает код - объект для класса, для интерфейса, для конструктора, оператор, свойство, поле и т. Д. Затем я могу построить код, используя эту объектную модель. В этом ответе показано, как компилировать файл кода в программу. Не CodeDom, хотя, как и CodeDom, он динамически создает сборку. Аналогия: я могу создать HTML-страницу, используя DOM или конкататы строк.
Cheeso

вот SO статья, которая показывает CodeDom в действии: stackoverflow.com/questions/865052/…
Cheeso

61

Вы можете скомпилировать кусок 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)
        {
            // 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();
        }
    }
}

Это тот же код, который использует компилятор C #, что является самым большим преимуществом. Сложный - это относительный термин, но в любом случае компиляция кода во время выполнения - сложная задача. Однако приведенный выше код совсем не сложен.
Tugberk

41

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

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

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

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

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

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

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


2
это ценная, полезная перспектива.
Cheeso

5

Нашел это полезным - гарантирует, что скомпилированная сборка ссылается на все, на что вы ссылались в данный момент, поскольку есть большая вероятность, что вы хотите, чтобы компилируемый 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);

Предупреждение: сборка может быть на удивление, очень медленной. Небольшой, относительно простой 10-строчный фрагмент кода компилируется с обычным приоритетом за 2-10 секунд на нашем относительно быстром сервере. Никогда не следует привязывать вызовы к CompileAssemblyFromSource()чему-либо с обычными ожиданиями производительности, например к веб-запросу. Вместо этого, предварительно скомпилируйте код, который вам нужен, в потоке с низким приоритетом и получите способ работать с кодом, который требует, чтобы этот код был готов, пока у него не будет возможности завершить компиляцию. Например, вы можете использовать его в процессе пакетной работы.


Ваш ответ уникален. Другие не решают мою проблему.
FindOutIslamNow

3

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

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

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


Да, с CodeDOM приятно то, что он может генерировать сборку для вас в памяти (а также предоставлять сообщения об ошибках и другую информацию в легко читаемом формате).
Нолдорин

3
@ Noldorin, Реализация CodeDOM на C # фактически не генерирует сборку в памяти. Вы можете включить флаг для него, но он игнорируется. Вместо этого он использует временный файл.
Мэтью Оленик

@Matt: Да, хорошая мысль - я забыл этот факт. Тем не менее, он по- прежнему значительно упрощает процесс (позволяет эффективно появляться , как если сборка были произведены в памяти), а также предлагает полный управляемый интерфейс, который гораздо лучше , чем иметь дело с процессами.
Нолдорин

Кроме того, CodeDomProvider - это просто класс, который в любом случае вызывает csc.exe.
justin.m.chase

1

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

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"); }
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.