Чи можливо динамічно компілювати та виконувати фрагменти коду C #?


177

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

Як мінімум, я міг би визначити інтерфейс, який їм потрібно буде реалізувати, і тоді вони нададуть кодовий "розділ", який реалізував цей інтерфейс.


11
Я знаю, що цій посаді є кілька років, але я подумав, що варто згадати при впровадженні Project Roslyn , можливість компілювати сирий 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

ось так стаття , яка показує CodeDOM в дії: stackoverflow.com/questions/865052 / ...
Cheeso

61

Ви можете скласти фрагмент коду C # в пам'ять і генерувати байти збірки за допомогою 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 #, який є найбільшою перевагою. Комплекс - відносний термін, але складання коду під час виконання - це все-таки складна робота. Однак наведений код взагалі не є складним.
тугберк

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 #

EDIT : Або ще краще, використовуйте CodeDOM, як запропонував Нолдорін ...


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

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

@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.