Apakah mungkin untuk secara dinamis mengkompilasi dan mengeksekusi fragmen kode C #?


177

Saya bertanya-tanya apakah mungkin untuk menyimpan fragmen kode C # ke file teks (atau aliran input apa pun), dan kemudian menjalankannya secara dinamis? Dengan asumsi apa yang diberikan kepada saya akan dikompilasi dengan baik dalam setiap blok Main (), apakah mungkin untuk mengkompilasi dan / atau menjalankan kode ini? Saya lebih suka mengkompilasinya untuk alasan kinerja.

Paling tidak, saya bisa mendefinisikan sebuah antarmuka yang harus mereka implementasikan, kemudian mereka akan memberikan kode 'bagian' yang mengimplementasikan antarmuka itu.


11
Saya tahu posting ini berumur beberapa tahun, tapi saya pikir layak disebutkan dengan diperkenalkannya Proyek Roslyn , kemampuan untuk mengkompilasi C # mentah dengan cepat dan menjalankannya dalam program .NET hanya sedikit lebih mudah.
Lawrence

Jawaban:


176

Solusi terbaik dalam C # / semua bahasa .NET statis adalah dengan menggunakan CodeDOM untuk hal-hal seperti itu. (Sebagai catatan, tujuan utamanya adalah untuk membangun bit kode secara dinamis, atau bahkan seluruh kelas.)

Berikut ini adalah contoh singkat yang bagus dari blog LukeH , yang menggunakan beberapa LINQ juga hanya untuk bersenang-senang.

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

Kelas yang paling penting di sini adalah CSharpCodeProvideryang menggunakan kompiler untuk mengkompilasi kode dengan cepat. Jika Anda ingin menjalankan kodenya, Anda hanya perlu menggunakan sedikit refleksi untuk secara dinamis memuat rakitan dan menjalankannya.

Berikut adalah contoh lain dalam C # yang (walaupun sedikit kurang ringkas) juga menunjukkan kepada Anda bagaimana menjalankan kode yang dikompilasi runtime menggunakan System.Reflectionnamespace.


3
Meskipun saya ragu Anda menggunakan Mono, saya pikir mungkin ada gunanya menunjukkan bahwa ada ruang nama Mono.CSharp ( mono-project.com/CSharp_Compiler ) yang sebenarnya berisi kompiler sebagai layanan sehingga Anda dapat secara dinamis menjalankan kode dasar / evaluasi ekspresi sebaris, dengan kerumitan minimal.
Noldorin

1
apa yang akan menjadi kebutuhan dunia nyata untuk melakukan ini. Saya cukup hijau di pemrograman secara umum dan saya pikir ini keren tapi saya tidak bisa memikirkan alasan mengapa Anda ingin / ini akan berguna. Terima kasih jika Anda bisa menjelaskannya.
Crash893

1
@ Crash893: Sistem scripting untuk hampir semua jenis aplikasi desainer bisa memanfaatkan ini. Tentu saja, ada alternatif seperti IronPython LUA, tetapi ini tentu saja salah satunya. Perhatikan bahwa sistem plugin akan lebih baik dikembangkan dengan mengekspos antarmuka dan memuat DLL yang dikompilasi yang berisi implementasi dari mereka, daripada memuat kode secara langsung.
Noldorin

Saya selalu menganggap "CodeDom" sebagai hal yang membuat saya membuat file kode menggunakan DOM - model objek dokumen. Di System.CodeDom, ada objek untuk mewakili semua artefak yang termasuk kode - objek untuk kelas, untuk antarmuka, untuk konstruktor, pernyataan, properti, bidang, dan sebagainya. Saya kemudian dapat membuat kode menggunakan model objek itu. Apa yang ditampilkan di sini dalam jawaban ini adalah mengkompilasi file kode, dalam sebuah program. Bukan CodeDom, meskipun seperti CodeDom, itu secara dinamis menghasilkan perakitan. Analogi: Saya dapat membuat halaman HTML menggunakan DOM, atau menggunakan string string.
Cheeso

inilah artikel SO yang menunjukkan CodeDom beraksi: stackoverflow.com/questions/865052/…
Cheeso

61

Anda dapat mengkompilasi sepotong kode C # ke dalam memori dan menghasilkan byte perakitan dengan Roslyn. Sudah disebutkan tetapi akan layak menambahkan beberapa contoh Roslyn untuk ini di sini. Berikut ini adalah contoh lengkapnya:

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

Ini kode yang sama yang digunakan kompiler C # yang merupakan manfaat terbesar. Kompleks adalah istilah relatif tetapi mengkompilasi kode pada saat runtime adalah pekerjaan yang kompleks untuk dilakukan. Namun, kode di atas tidak rumit sama sekali.
tugberk

41

Orang lain telah memberikan jawaban yang baik tentang cara menghasilkan kode saat runtime jadi saya pikir saya akan membahas paragraf kedua Anda. Saya memiliki beberapa pengalaman dengan ini dan hanya ingin berbagi pelajaran yang saya pelajari dari pengalaman itu.

Paling tidak, saya bisa mendefinisikan sebuah antarmuka yang harus mereka implementasikan, kemudian mereka akan memberikan kode 'bagian' yang mengimplementasikan antarmuka itu.

Anda mungkin memiliki masalah jika menggunakan interfacetipe dasar. Jika Anda menambahkan satu metode baru ke interfacemasa depan, semua kelas yang disediakan klien yang menerapkaninterface sekarang menjadi abstrak, artinya Anda tidak akan dapat mengkompilasi atau membuat instance kelas yang disediakan klien saat runtime.

Saya memiliki masalah ini ketika tiba saatnya untuk menambahkan metode baru setelah sekitar 1 tahun pengiriman antarmuka lama dan setelah mendistribusikan sejumlah besar data "warisan" yang perlu didukung. Saya akhirnya membuat antarmuka baru yang diwarisi dari yang lama tapi pendekatan ini membuatnya lebih sulit untuk memuat dan membuat instance kelas yang disediakan klien karena saya harus memeriksa antarmuka mana yang tersedia.

Salah satu solusi yang saya pikirkan saat itu adalah dengan menggunakan kelas aktual sebagai tipe dasar seperti yang di bawah ini. Kelas itu sendiri dapat ditandai abstrak tetapi semua metode harus berupa metode virtual kosong (bukan metode abstrak). Klien kemudian dapat mengganti metode yang mereka inginkan dan saya dapat menambahkan metode baru ke kelas dasar tanpa membatalkan kode yang disediakan oleh klien.

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

Terlepas dari apakah masalah ini berlaku, Anda harus mempertimbangkan bagaimana membuat versi antarmuka antara basis kode Anda dan kode yang disediakan klien.


2
itu adalah perspektif yang berharga dan bermanfaat.
Cheeso

5

Menemukan ini berguna - memastikan Majelis yang dikompilasi mereferensikan semua yang saat ini telah Anda referensikan, karena ada kemungkinan Anda menginginkan C # yang Anda kompilasi untuk menggunakan beberapa kelas dll dalam kode yang memancarkan ini:

        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;

Dalam kasus saya, saya memancarkan kelas, yang namanya disimpan dalam sebuah string className,, yang memiliki nama metode public static tunggal Get(), yang dikembalikan dengan tipe StoryDataIds. Begini tampilannya metode itu:

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

Peringatan: Kompilasi bisa mengejutkan, sangat lambat. Sebagian kecil, 10-baris kode mengkompilasi pada prioritas normal dalam 2-10 detik pada server kami yang relatif cepat. Anda tidak boleh mengikat panggilan ke CompileAssemblyFromSource()apa pun dengan harapan kinerja normal, seperti permintaan web. Alih-alih, kompilasi secara proaktif kode yang Anda butuhkan di utas prioritas rendah dan miliki cara untuk menangani kode yang mengharuskan kode tersebut siap, sampai ada peluang untuk menyelesaikan kompilasi. Misalnya Anda bisa menggunakannya dalam proses pekerjaan batch.


Jawaban Anda unik. Orang lain tidak menyelesaikan masalah saya.
FindOutIslamNow

3

Untuk mengkompilasi Anda hanya bisa memulai panggilan shell ke kompilator csc. Anda mungkin sakit kepala mencoba untuk menjaga jalur Anda dan beralih lurus tetapi pasti bisa dilakukan.

Contoh C # Corner Shell

Sunting : Atau lebih baik lagi, gunakan CodeDOM seperti yang disarankan Noldorin ...


Ya, hal yang menyenangkan dari CodeDOM adalah ia dapat menghasilkan perakitan untuk Anda dalam memori (serta memberikan pesan kesalahan dan info lainnya dalam format yang mudah dibaca).
Noldorin

3
@Noldorin, Implementasi C # CodeDOM sebenarnya tidak menghasilkan perakitan dalam memori. Anda dapat mengaktifkan bendera untuk itu, tetapi diabaikan. Sebagai gantinya, ia menggunakan file sementara.
Matthew Olenik

@ Matt: Ya, poin bagus - Saya lupa fakta itu. Meskipun demikian, masih sangat menyederhanakan proses (membuatnya secara efektif tampak seolah-olah perakitan dihasilkan dalam memori), dan menawarkan antarmuka yang dikelola lengkap, yang jauh lebih baik daripada berurusan dengan proses.
Noldorin

Juga, CodeDomProvider hanyalah kelas yang memanggil csc.exe.
justin.m.chase

1

Baru-baru ini saya perlu menelurkan proses untuk pengujian unit. Posting ini berguna ketika saya membuat kelas sederhana untuk melakukannya dengan kode sebagai string atau kode dari proyek saya. Untuk membangun kelas ini, Anda memerlukan paket ICSharpCode.Decompilerdan Microsoft.CodeAnalysisNuGet. Inilah kelasnya:

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

Untuk menggunakannya, panggil Runmetode seperti di bawah ini:

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"); }
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.