เป็นไปได้หรือไม่ที่จะรวบรวมและประมวลผลโค้ด C # แบบไดนามิก?


177

ฉันสงสัยว่าเป็นไปได้หรือไม่ที่จะบันทึกเศษรหัส C # ลงในไฟล์ข้อความ (หรือสตรีมอินพุต) แล้วดำเนินการแบบไดนามิกเหล่านั้นหรือไม่ สมมติว่ามีอะไรให้ฉันจะรวบรวมปรับภายในบล็อก Main () ใด ๆ มันเป็นไปได้ที่จะรวบรวมและ / หรือรันรหัสนี้ ฉันต้องการรวบรวมเพื่อเหตุผลด้านประสิทธิภาพ

อย่างน้อยที่สุดฉันสามารถกำหนดอินเทอร์เฟซที่พวกเขาจะต้องดำเนินการจากนั้นพวกเขาก็จะให้รหัส 'ส่วน' ที่ใช้อินเทอร์เฟซนั้น


11
ฉันรู้ว่าโพสต์นี้มีอายุไม่กี่ปี แต่ฉันคิดว่ามันควรค่าแก่การกล่าวถึงด้วยการเปิดตัวProject Roslynความสามารถในการคอมไพล์ C # แบบทันทีและรันภายในโปรแกรม. NET นั้นง่ายขึ้นเล็กน้อย
Lawrence

คำตอบ:


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 namespace ( mono-project.com/CSharp_Compiler ) ซึ่งมีคอมไพเลอร์เป็นบริการเพื่อให้คุณสามารถรันโค้ด / ประเมินพื้นฐานแบบไดนามิก นิพจน์แบบอินไลน์โดยมีความยุ่งยากน้อยที่สุด
Noldorin

1
โลกแห่งความจริงต้องการอะไรในการทำสิ่งนี้ ฉันค่อนข้างเป็นมิตรกับการเขียนโปรแกรมโดยทั่วไปและฉันคิดว่านี่เจ๋ง แต่ฉันไม่สามารถคิดได้ว่าทำไมคุณถึงต้องการ / นี่จะมีประโยชน์ ขอบคุณถ้าคุณสามารถอธิบาย
Crash893

1
@ Crash893: ระบบสคริปต์สำหรับนักออกแบบแอพพลิเคชั่นประเภทใด ๆ ก็สามารถใช้ประโยชน์ได้ดี แน่นอนว่ามีทางเลือกอื่น ๆ เช่น IronPython LUA แต่นี่เป็นทางเลือกหนึ่ง โปรดทราบว่าระบบปลั๊กอินจะได้รับการพัฒนาที่ดีขึ้นโดยการเปิดเผยส่วนต่อประสานและการโหลด DLLs ที่คอมไพล์ซึ่งมีการใช้งานของพวกเขาแทนที่จะโหลดรหัสโดยตรง
Noldorin

ฉันคิดเสมอว่า "CodeDom" เป็นสิ่งที่ให้ฉันสร้างไฟล์รหัสโดยใช้ DOM - โมเดลวัตถุเอกสาร ใน System.CodeDom มีวัตถุที่จะแสดงสิ่งประดิษฐ์ทั้งหมดที่รหัสรวมถึง - วัตถุสำหรับคลาสสำหรับอินเทอร์เฟซสำหรับตัวสร้างคำสั่งคุณสมบัติเขตข้อมูลและอื่น ๆ ฉันสามารถสร้างรหัสโดยใช้โมเดลวัตถุนั้นได้ สิ่งที่แสดงในคำตอบนี้คือการรวบรวมไฟล์รหัสในโปรแกรม ไม่ใช่ CodeDom แม้ว่าจะเหมือนกับ CodeDom มันจะสร้างแอสเซมบลีแบบไดนามิก การเปรียบเทียบ: ฉันสามารถสร้างเพจ HTML โดยใช้ DOM หรือใช้สตริง concats
Cheeso

นี่เป็นบทความเพื่อให้การแสดง 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 # มุมเชลล์ตัวอย่าง

แก้ไข : หรือดีกว่ายังใช้ CodeDOM ตามที่ Noldorin แนะนำ ...


ใช่สิ่งที่ดีกับ CodeDOM คือมันสามารถสร้างแอสเซมบลีสำหรับคุณในหน่วยความจำ (เช่นเดียวกับการให้ข้อความแสดงข้อผิดพลาดและข้อมูลอื่น ๆ ในรูปแบบที่อ่านง่าย)
Noldorin

3
@Noldorin การใช้งาน C # CodeDOM ไม่ได้สร้างชุดประกอบในหน่วยความจำ คุณสามารถเปิดใช้งานการตั้งค่าสถานะสำหรับมัน แต่จะได้รับการละเว้น มันใช้ไฟล์ชั่วคราวแทน
Matthew Olenik

@ Matt: ใช่จุดที่ดี - ฉันลืมความจริงที่ว่า อย่างไรก็ตามมันยังลดความซับซ้อนของกระบวนการอย่างมาก (ทำให้ปรากฏอย่างมีประสิทธิภาพราวกับว่าการชุมนุมถูกสร้างขึ้นในหน่วยความจำ) และมีอินเตอร์เฟซที่มีการจัดการที่สมบูรณ์ซึ่งดีกว่าการจัดการกับกระบวนการมาก
Noldorin

นอกจากนี้ 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"); }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.