Có thể tự động biên dịch và thực thi các đoạn mã C # không?


177

Tôi đã tự hỏi nếu có thể lưu các đoạn mã C # vào một tệp văn bản (hoặc bất kỳ luồng đầu vào nào), và sau đó thực hiện chúng một cách linh hoạt? Giả sử những gì được cung cấp cho tôi sẽ biên dịch tốt trong bất kỳ khối Main () nào, liệu có thể biên dịch và / hoặc thực thi mã này không? Tôi muốn biên dịch nó vì lý do hiệu suất.

Ít nhất, tôi có thể xác định một giao diện mà họ sẽ được yêu cầu thực hiện, sau đó họ sẽ cung cấp một mã 'phần' đã triển khai giao diện đó.


11
Tôi biết bài đăng này đã được vài năm tuổi, nhưng tôi nghĩ nó đáng được đề cập với sự ra mắt của Project Roslyn , khả năng biên dịch C # thô một cách nhanh chóng và chạy nó trong một chương trình .NET chỉ dễ dàng hơn một chút.
Lawrence

Câu trả lời:


176

Giải pháp tốt nhất trong C # / tất cả các ngôn ngữ .NET tĩnh là sử dụng CodeDOM cho những thứ đó. (Như một lưu ý, mục đích chính khác của nó là để xây dựng động các bit mã, hoặc thậm chí toàn bộ các lớp.)

Đây là một ví dụ ngắn lấy từ blog của LukeH , sử dụng một số LINQ quá chỉ để giải trí.

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

Lớp quan trọng chính ở đây là CSharpCodeProvider sử dụng trình biên dịch để biên dịch mã khi đang di chuyển. Nếu bạn muốn sau đó chạy mã, bạn chỉ cần sử dụng một chút sự phản chiếu để tự động tải lắp ráp và thực hiện nó.

Dưới đây là một ví dụ khác trong C # (mặc dù ít súc tích hơn) cũng cho bạn thấy chính xác cách chạy mã được biên dịch thời gian chạy bằng cách sử dụng System.Reflectionkhông gian tên.


3
Mặc dù tôi nghi ngờ việc bạn sử dụng Mono, tôi nghĩ rằng có thể đáng để chỉ ra rằng có tồn tại một không gian tên Mono.CSharp ( mono-project.com/CSharp_Compiler ) thực sự chứa trình biên dịch như một dịch vụ để bạn có thể tự động chạy mã / đánh giá cơ bản biểu thức nội tuyến, với rắc rối tối thiểu.
Noldorin

1
những gì sẽ là một thế giới thực sự cần thiết để làm điều này. Tôi khá xanh khi lập trình nói chung và tôi nghĩ điều này thật tuyệt nhưng tôi không thể nghĩ ra lý do tại sao bạn muốn / điều này sẽ hữu ích. Cảm ơn nếu bạn có thể giải thích.
Crash893

1
@ Crash893: Một hệ thống kịch bản cho hầu hết mọi loại ứng dụng thiết kế có thể sử dụng tốt điều này. Tất nhiên, có những lựa chọn thay thế như IronPython LUA, nhưng đây chắc chắn là một. Lưu ý rằng một hệ thống plugin sẽ được phát triển tốt hơn bằng cách hiển thị các giao diện và tải các tệp DLL được biên dịch có chứa các triển khai của chúng, thay vì tải mã trực tiếp.
Noldorin

Tôi luôn nghĩ "CodeDom" là thứ cho phép tôi tạo một tệp mã bằng DOM - một mô hình đối tượng tài liệu. Trong System.CodeDom, có các đối tượng để biểu diễn tất cả các tạo phẩm mà mã bao gồm - một đối tượng cho một lớp, cho một giao diện, cho một hàm tạo, một câu lệnh, một thuộc tính, một trường, v.v. Sau đó tôi có thể xây dựng mã bằng mô hình đối tượng đó. Những gì được hiển thị ở đây trong câu trả lời này là biên dịch một tệp mã, trong một chương trình. Không phải CodeDom, mặc dù giống như CodeDom, nó tự động tạo ra một hội đồng. Sự tương tự: Tôi có thể tạo một trang HTML bằng DOM hoặc sử dụng các chuỗi concat.
Cheeso

đây là một bài viết SO cho thấy CodeDom đang hoạt động: stackoverflow.com/questions/865052/ory
Cheeso

61

Bạn có thể biên dịch một đoạn mã C # vào bộ nhớ và tạo các byte lắp ráp với Roslyn. Nó đã được đề cập nhưng sẽ đáng để thêm một số ví dụ Roslyn cho điều này ở đây. Sau đây là ví dụ đầy đủ:

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

Đó là cùng một mã mà trình biên dịch C # sử dụng, đó là lợi ích lớn nhất. Complex là một thuật ngữ tương đối nhưng biên dịch mã trong thời gian chạy là một công việc phức tạp phải làm bằng mọi cách. Tuy nhiên, đoạn mã trên không phức tạp chút nào.
kéo co

41

Những người khác đã đưa ra câu trả lời tốt về cách tạo mã trong thời gian chạy vì vậy tôi nghĩ rằng tôi sẽ giải quyết đoạn thứ hai của bạn. Tôi có một số kinh nghiệm với điều này và chỉ muốn chia sẻ một bài học tôi học được từ kinh nghiệm đó.

Ít nhất, tôi có thể xác định một giao diện mà họ sẽ được yêu cầu thực hiện, sau đó họ sẽ cung cấp một mã 'phần' đã triển khai giao diện đó.

Bạn có thể có một vấn đề nếu bạn sử dụng interfacenhư là một loại cơ sở. Nếu bạn thêm một phương thức mới interfacevào tương lai thì tất cả các lớp do khách hàng cung cấp hiện có thực hiệninterface hiện trở nên trừu tượng, có nghĩa là bạn sẽ không thể biên dịch hoặc khởi tạo lớp do khách hàng cung cấp khi chạy.

Tôi gặp vấn đề này khi đến lúc phải thêm một phương thức mới sau khoảng 1 năm vận chuyển giao diện cũ và sau khi phân phối một lượng lớn dữ liệu "di sản" cần được hỗ trợ. Cuối cùng tôi đã tạo ra một giao diện mới được kế thừa từ giao diện cũ nhưng cách tiếp cận này khiến việc tải và khởi tạo các lớp do khách hàng cung cấp trở nên khó khăn hơn vì tôi phải kiểm tra giao diện nào có sẵn.

Một giải pháp tôi nghĩ lúc đó là thay vào đó sử dụng một lớp thực tế làm một loại cơ sở như kiểu bên dưới. Bản thân lớp có thể được đánh dấu trừu tượng nhưng tất cả các phương thức nên là các phương thức ảo trống (không phải là phương thức trừu tượng). Sau đó, khách hàng có thể ghi đè các phương thức họ muốn và tôi có thể thêm các phương thức mới vào lớp cơ sở mà không làm mất hiệu lực mã do khách hàng cung cấp.

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

Bất kể vấn đề này có áp dụng hay không, bạn nên xem xét cách phiên bản giao diện giữa cơ sở mã của bạn và mã do khách hàng cung cấp.


2
đó là một quan điểm có giá trị, hữu ích.
Cheeso

5

Tìm thấy điều này hữu ích - đảm bảo Hội đồng được biên dịch tham chiếu tất cả mọi thứ bạn hiện đã tham chiếu, vì rất có thể bạn muốn C # bạn đang biên dịch để sử dụng một số lớp, v.v. trong mã phát ra điều này:

        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;

Trong trường hợp của tôi, tôi đã phát ra một lớp, có tên được lưu trữ trong một chuỗi className, trong đó có một phương thức tĩnh công khai duy nhất được đặt tên Get(), được trả về với kiểu StoryDataIds. Đây là cách gọi phương thức đó trông như sau:

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

Cảnh báo: Quá trình biên dịch có thể gây ngạc nhiên, cực kỳ chậm. Một đoạn mã 10 dòng nhỏ, tương đối đơn giản sẽ biên dịch ở mức ưu tiên bình thường trong 2-10 giây trên máy chủ tương đối nhanh của chúng tôi. Bạn không bao giờ nên kết nối các cuộc gọi với CompileAssemblyFromSource()bất cứ điều gì với mong đợi hiệu suất bình thường, như yêu cầu web. Thay vào đó, hãy chủ động biên dịch mã bạn cần trên một luồng có mức độ ưu tiên thấp và có cách xử lý mã yêu cầu mã đó phải sẵn sàng, cho đến khi có cơ hội biên dịch xong. Ví dụ, bạn có thể sử dụng nó trong một quy trình công việc hàng loạt.


Câu trả lời của bạn là duy nhất. Những người khác không giải quyết vấn đề của tôi.
FindOutIslamNow

3

Để biên dịch, bạn có thể chỉ cần thực hiện một cuộc gọi shell đến trình biên dịch csc. Bạn có thể đau đầu khi cố gắng giữ đường đi và chuyển thẳng nhưng chắc chắn điều đó có thể được thực hiện.

Ví dụ về C Shell Corner

EDIT : Hoặc tốt hơn nữa, sử dụng CodeDOM như Noldorin đã đề xuất ...


Vâng, điều tuyệt vời với CodeDOM là nó có thể tạo ra bộ lắp ráp cho bạn trong bộ nhớ (cũng như cung cấp các thông báo lỗi và thông tin khác ở định dạng dễ đọc).
Noldorin

3
@Noldorin, Việc triển khai C # CodeDOM không thực sự tạo ra một tập hợp trong bộ nhớ. Bạn có thể kích hoạt cờ cho nó, nhưng nó bị bỏ qua. Nó sử dụng một tập tin tạm thời thay thế.
Matthew Olenik

@Matt: Vâng, điểm tốt - Tôi quên mất thực tế đó. Tuy nhiên, nó vẫn đơn giản hóa rất nhiều quá trình (làm cho nó xuất hiện một cách hiệu quả như thể lắp ráp được tạo ra trong bộ nhớ) và cung cấp một giao diện được quản lý hoàn chỉnh, đẹp hơn nhiều so với xử lý các quy trình.
Noldorin

Ngoài ra, CodeDomProvider chỉ là một lớp gọi vào csc.exe.
justin.m.chase

1

Gần đây tôi cần phải sinh ra các quá trình để thử nghiệm đơn vị. Bài đăng này rất hữu ích khi tôi tạo một lớp đơn giản để làm điều đó với mã dưới dạng chuỗi hoặc mã từ dự án của tôi. Để xây dựng lớp này, bạn sẽ cần các gói ICSharpCode.DecompilerMicrosoft.CodeAnalysisNuGet. Đây là lớp học:

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

Để sử dụng nó, hãy gọi các Runphương thức như sau:

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"); }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.