Czy można dynamicznie kompilować i wykonywać fragmenty kodu C #?


177

Zastanawiałem się, czy można zapisać fragmenty kodu C # do pliku tekstowego (lub dowolnego strumienia wejściowego), a następnie wykonać je dynamicznie? Zakładając, że to, co zostało mi dostarczone, skompilowałoby się dobrze w dowolnym bloku Main (), czy jest możliwe skompilowanie i / lub wykonanie tego kodu? Wolałbym go skompilować ze względu na wydajność.

Mógłbym przynajmniej zdefiniować interfejs, który będą musieli zaimplementować, a następnie dostarczą `` sekcję '' kodu, która implementuje ten interfejs.


11
Wiem, że ten post ma kilka lat, ale pomyślałem, że warto o nim wspomnieć wraz z wprowadzeniem Project Roslyn , możliwość kompilowania surowego C # w locie i uruchamiania go w programie .NET jest po prostu trochę łatwiejsza.
Lawrence

Odpowiedzi:


176

Najlepszym rozwiązaniem w C # / wszystkich statycznych językach .NET jest użycie CodeDOM do takich rzeczy. (Warto zauważyć, że jego drugim głównym celem jest dynamiczne konstruowanie bitów kodu, a nawet całych klas).

Oto ładny, krótki przykład z bloga LukeH , który używa LINQ również dla zabawy.

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

Najważniejsza tutaj jest klasa, CSharpCodeProviderktóra wykorzystuje kompilator do kompilowania kodu w locie. Jeśli chcesz następnie uruchomić kod, wystarczy użyć odrobiny odbicia, aby dynamicznie załadować zestaw i wykonać go.

Oto kolejny przykład w C #, który (choć nieco mniej zwięzły) dodatkowo pokazuje dokładnie, jak uruchomić kod skompilowany w czasie wykonywania przy użyciu System.Reflectionprzestrzeni nazw.


3
Chociaż wątpię, czy używasz Mono, pomyślałem, że warto byłoby wskazać, że istnieje przestrzeń nazw Mono.CSharp ( mono-project.com/CSharp_Compiler ), która w rzeczywistości zawiera kompilator jako usługę, dzięki czemu można dynamicznie uruchamiać podstawowy kod / oceniać wyrażenia w tekście, przy minimalnym wysiłku.
Noldorin

1
jaka byłaby rzeczywista potrzeba, aby to zrobić. Jestem całkiem zielony w programowaniu w ogóle i myślę, że to jest fajne, ale nie mogę wymyślić powodu, dla którego chciałbyś / to byłoby przydatne. Dzięki, jeśli możesz wyjaśnić.
Crash893

1
@ Crash893: System skryptów dla prawie każdego rodzaju aplikacji projektanta mógłby to dobrze wykorzystać. Oczywiście istnieją alternatywy, takie jak IronPython LUA, ale z pewnością jest jedna. Należy zauważyć, że system wtyczek byłby lepiej opracowany przez udostępnianie interfejsów i ładowanie skompilowanych bibliotek DLL, które zawierają ich implementacje, zamiast bezpośredniego ładowania kodu.
Noldorin

Zawsze myślałem o „CodeDom” jako o rzeczy, która pozwala mi stworzyć plik kodu przy użyciu DOM - modelu obiektowego dokumentu. W System.CodeDom istnieją obiekty reprezentujące wszystkie artefakty zawarte w kodzie - obiekt dla klasy, dla interfejsu, dla konstruktora, instrukcja, właściwość, pole i tak dalej. Następnie mogę skonstruować kod przy użyciu tego modelu obiektów. To, co jest pokazane w tej odpowiedzi, to kompilowanie pliku kodu w programie. Nie CodeDom, chociaż podobnie jak CodeDom dynamicznie tworzy zestaw. Analogia: mogę stworzyć stronę HTML za pomocą DOM lub używając konklatów ciągów.
Cheeso

oto artykuł SO, który pokazuje CodeDom w akcji: stackoverflow.com/questions/865052/…
Cheeso

61

Możesz skompilować fragment kodu C # do pamięci i wygenerować bajty zestawu za pomocą Roslyn. Już o tym wspomniano, ale warto dodać tutaj przykład Roslyn. Oto pełny przykład:

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

Jest to ten sam kod, którego używa kompilator C #, co jest największą zaletą. Złożony jest terminem względnym, ale kompilowanie kodu w czasie wykonywania i tak jest złożonym zadaniem. Jednak powyższy kod wcale nie jest skomplikowany.
tugberk

41

Inni już udzielili dobrych odpowiedzi na temat generowania kodu w czasie wykonywania, więc pomyślałem, że odniosę się do twojego drugiego akapitu. Mam z tym pewne doświadczenie i po prostu chcę podzielić się lekcją, której nauczyłem się z tego doświadczenia.

Mógłbym przynajmniej zdefiniować interfejs, który będą musieli zaimplementować, a następnie dostarczą `` sekcję '' kodu, która implementuje ten interfejs.

Możesz mieć problem, jeśli użyjesz interfacetypu podstawowego. Jeśli dodasz jedną nową metodę do interfacew przyszłości, wszystkie istniejące klasy dostarczone przez klienta, które implementują interfaceteraz, staną się abstrakcyjne, co oznacza, że ​​nie będzie można skompilować ani utworzyć wystąpienia klasy dostarczonej przez klienta w czasie wykonywania.

Miałem ten problem, gdy przyszedł czas na dodanie nowej metody po około 1 roku dostarczenia starego interfejsu i po rozprowadzeniu dużej ilości „starszych” danych, które wymagały obsługi. Skończyło się na stworzeniu nowego interfejsu, który odziedziczyłem po starym, ale takie podejście utrudniło ładowanie i tworzenie instancji klas dostarczonych przez klienta, ponieważ musiałem sprawdzić, który interfejs był dostępny.

Jednym z rozwiązań, o których myślałem wtedy, było użycie rzeczywistej klasy jako typu podstawowego, takiego jak poniższy. Samą klasę można oznaczyć jako abstrakcyjną, ale wszystkie metody powinny być pustymi metodami wirtualnymi (nie metodami abstrakcyjnymi). Klienci mogą wtedy przesłonić żądane metody, a ja mogę dodać nowe metody do klasy bazowej bez unieważniania istniejącego kodu dostarczonego przez klienta.

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

Niezależnie od tego, czy ten problem występuje, należy rozważyć, jak zmienić wersję interfejsu między bazą kodu a kodem dostarczonym przez klienta.


2
to cenna, pomocna perspektywa.
Cheeso

5

Okazało się to przydatne - zapewnia, że ​​skompilowany zestaw odwołuje się do wszystkiego, do czego się obecnie odwołujesz, ponieważ istnieje duża szansa, że ​​chcesz, aby C #, który kompilujesz, używał niektórych klas itp. W kodzie, który emituje to:

        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;

W moim przypadku emitowałem klasę, której nazwa była przechowywana w łańcuchu className, która miała jedną publiczną metodę statyczną o nazwie Get(), która zwracała z typem StoryDataIds. Oto jak wygląda wywołanie tej metody:

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

Ostrzeżenie: kompilacja może być zaskakująco, bardzo wolna. Mały, stosunkowo prosty 10-liniowy fragment kodu kompiluje się z normalnym priorytetem w ciągu 2-10 sekund na naszym stosunkowo szybkim serwerze. Nigdy nie należy łączyć wywołań z CompileAssemblyFromSource()niczym, co ma normalne oczekiwania dotyczące wydajności, na przykład z żądaniem internetowym. Zamiast tego proaktywnie kompiluj kod, którego potrzebujesz, w wątku o niskim priorytecie i miej sposób radzenia sobie z kodem, który wymaga, aby kod był gotowy, dopóki nie będzie miał szansy na zakończenie kompilacji. Na przykład można go użyć w procesie zadań wsadowych.


Twoja odpowiedź jest wyjątkowa. Inni nie rozwiązują mojego problemu.
FindOutIslamNow

3

Aby skompilować, możesz po prostu zainicjować wywołanie powłoki do kompilatora csc. Możesz mieć ból głowy, próbując utrzymać ścieżki i przełączniki prosto, ale z pewnością można to zrobić.

Przykłady powłoki narożnej języka C #

EDYCJA : Lub jeszcze lepiej, użyj CodeDOM, jak sugerował Noldorin ...


Tak, fajną rzeczą w CodeDOM jest to, że może generować zestaw w pamięci (a także dostarczać komunikaty o błędach i inne informacje w łatwym do odczytania formacie).
Noldorin

3
@Noldorin, implementacja C # CodeDOM w rzeczywistości nie generuje zestawu w pamięci. Możesz włączyć tę flagę, ale zostanie zignorowana. Zamiast tego używa pliku tymczasowego.
Matthew Olenik

@Matt: Tak, słuszna uwaga - zapomniałem o tym. Niemniej jednak nadal znacznie upraszcza proces (sprawia, że wygląda on efektywnie tak, jakby zestaw był generowany w pamięci) i oferuje kompletny zarządzany interfejs, który jest znacznie przyjemniejszy niż zajmowanie się procesami.
Noldorin

Ponadto CodeDomProvider jest po prostu klasą, która i tak wywołuje csc.exe.
justin.m.chase

1

Niedawno musiałem odrodzić procesy do testów jednostkowych. Ten post był przydatny, ponieważ utworzyłem prostą klasę, aby to zrobić, używając kodu jako ciągu lub kodu z mojego projektu. Aby skompilować tę klasę, będziesz potrzebować pakietów ICSharpCode.Decompileri Microsoft.CodeAnalysisNuGet. Oto klasa:

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

Aby z niego skorzystać, wywołaj Runponiższe metody:

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"); }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.