Czy możliwe jest dynamiczne kompilowanie i wykonywanie fragmentów kodu C#?

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

Co najmniej, mógłbym zdefiniować interfejs, który będą musieli zaimplementować, wtedy dostarczyliby kod "sekcji", która zaimplementowała to interfejs.

Author: esac, 2009-05-05

6 answers

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

Oto krótki przykład z bloga LukeH , który również używa LINQ 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));
    }
}

Klasą o podstawowym znaczeniu jest CSharpCodeProvider, która wykorzystuje kompilator do kompilowania kodu w locie. Jeśli chcesz następnie uruchomić kod, wystarczy, że użyj odrobiny refleksji, aby dynamicznie załadować zespół i wykonać go.

Oto {[5] } kolejny przykład w C#, który (choć nieco mniej zwięzły) dodatkowo pokazuje, jak dokładnie uruchomić skompilowany kod w przestrzeni nazw System.Reflection.

 153
Author: Noldorin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-08 13:10:26

Możesz skompilować fragment kodu C# do pamięci i wygenerować bajty asemblacji za pomocą Roslyn. To już wspomniano, ale warto byłoby dodać jakiś przykład Roslyn do tego tutaj. Poniżej znajduje się 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)
        {
            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();
        }
    }
}
 44
Author: tugberk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-04-02 15:43:51

Inni już dali dobre odpowiedzi na temat generowania kodu w czasie wykonywania, więc pomyślałem, że zajmę się drugim akapitem. Mam z tym pewne doświadczenie i po prostu chcę podzielić się lekcją, której nauczyłem się z tego doświadczenia.

Co najmniej, mógłbym zdefiniować interfejs, który byłby wymagany do realizacji, wówczas zapewniliby kod "sekcja", która zaimplementowała, że interfejs.

Możesz mieć problem, jeśli użyjesz interface jako typu bazowego. Jeśli dodasz jedną nową metodę do interface w przyszłości, wszystkie istniejące klasy dostarczone przez Klienta, które implementują interface, staną się abstrakcyjne, co oznacza, że nie będziesz w stanie skompilować ani utworzyć instancji klasy dostarczonej przez Klienta w czasie wykonywania.

Miałem ten problem, gdy przyszedł czas, aby dodać nową metodę po około 1 roku wysyłania starego interfejsu i po dystrybucji dużej ilości "starszych" danych, które musiały być obsługiwane. Skończyło się na stworzeniu nowego interfejsu, który odziedziczył po starym ale takie podejście utrudniało ładowanie i tworzenie instancji klas dostarczanych przez klienta, ponieważ musiałem sprawdzić, który interfejs jest dostępny.

Jednym z rozwiązań, o którym myślałem w tym czasie, było użycie rzeczywistej klasy jako typu bazowego, takiego jak ta poniżej. Sama klasa może być oznaczona jako abstrakcyjna, ale wszystkie metody powinny być pustymi metodami wirtualnymi (nie abstrakcyjnymi). Klienci mogą następnie nadpisać metody, które chcą, a ja mogę dodać nowe metody do klasy bazowej bez unieważniania istniejących kod dostarczony przez Klienta.

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

Niezależnie od tego, czy ten problem ma zastosowanie, powinieneś zastanowić się nad wersją interfejsu pomiędzy bazą kodu a kodem dostarczonym przez Klienta.

 36
Author: Brian Ensink,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-05 19:47:45

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

C# Corner Shell Examples

EDIT: albo jeszcze lepiej, Użyj CodeDOM, jak zasugerował Noldorin...

 3
Author: Gary.Ray,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-05 19:10:42

System.CodeDom.Przestrzeń nazw kompilatora powinna pomóc. Zobacz Ten artykuł

 1
Author: FryGuy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-05 19:06:47

Uznało to za przydatne - zapewnia, że skompilowany Assembly odwołuje się do wszystkiego, do czego obecnie się odwołujesz, ponieważ istnieje duża szansa, że chcesz, aby C#, które kompilujesz, używało 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 znaków className, który miał jedną publiczną statyczną metodę o nazwie Get(), która zwracała Typ StoryDataIds. Oto jak wygląda wywołanie tej metody:

        var tempType = asm.GetType(className);
        var ids = (StoryDataIds)tempType.GetMethod("Get").Invoke(null, null);
 1
Author: Chris Moschini,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-09 19:00:14