簡體   English   中英

是否可以動態編譯和執行 C# 代碼片段?

[英]Is it possible to dynamically compile and execute C# code fragments?

我想知道是否可以將 C# 代碼片段保存到文本文件(或任何輸入流)中,然后動態執行它們? 假設提供給我的內容可以在任何 Main() 塊中正常編譯,是否可以編譯和/或執行此代碼? 出於性能原因,我更願意編譯它。

至少,我可以定義一個他們需要實現的接口,然后他們會提供一個實現該接口的代碼“部分”。

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命名空間運行運行時編譯的代碼。

您可以將一段 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();
        }
    }
}

其他人已經就如何在運行時生成代碼給出了很好的答案,所以我想我會解決你的第二段。 我對此有一些經驗,只想分享我從那次經歷中學到的教訓。

至少,我可以定義一個他們需要實現的接口,然后他們會提供一個實現該接口的代碼“部分”。

如果您使用interface作為基本類型,您可能會遇到問題。 如果您將來向interface添加一個新方法,那么所有實現該interface的現有客戶端提供的類現在都將變為抽象,這意味着您將無法在運行時編譯或實例化客戶端提供的類。

在發布舊界面大約 1 年並分發大量需要支持的“遺留”數據之后,當需要添加新方法時,我遇到了這個問題。 我最終創建了一個繼承自舊接口的新接口,但這種方法使得加載和實例化客戶端提供的類變得更加困難,因為我必須檢查哪個接口可用。

我當時想到的一種解決方案是使用實際的類作為基類型,如下所示。 類本身可以標記為抽象,但所有方法都應該是空的虛擬方法(不是抽象方法)。 然后,客戶可以覆蓋他們想要的方法,我可以向基類添加新方法,而不會使現有的客戶提供的代碼無效。

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

不管這個問題是否適用,您都應該考慮如何對代碼庫和客戶端提供的代碼之間的接口進行版本控制。

發現這很有用 - 確保編譯的程序集引用您當前引用的所有內容,因為您很有可能希望您正在編譯的 C# 在發出此代碼的代碼中使用某些類等:

(字符串code是正在編譯的動態 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()調用與任何具有正常性能預期的東西聯系起來,例如 Web 請求。 相反,在低優先級線程上主動編譯您需要的代碼,並有一種方法來處理需要該代碼准備好的代碼,直到它有機會完成編譯。 例如,您可以在批處理作業過程中使用它。

我最近需要為單元測試生成流程。 這篇文章很有用,因為我創建了一個簡單的類來使用作為字符串的代碼或我的項目中的代碼來做到這一點。 要構建此類,您需要ICSharpCode.DecompilerMicrosoft.CodeAnalysis NuGet 包。 這是課程:

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

要編譯,您只需啟動對 csc 編譯器的 shell 調用。 試圖保持路徑和開關筆直可能會讓您頭疼,但它肯定可以做到。

C# 角殼示例

編輯:或者更好的是,按照諾多林的建議使用 CodeDOM...

using System.CodeDom.Compiler;
using System.Diagnostics;
using Microsoft.CSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Reflection;

namespace ASL
{
    class Program
    {
        [Obsolete]
        static void Main(string[] args)
        {
            string code = @"
                using System;

             namespace First
             {
                public class Program
                {
                  public static void Main()
                    {
                        " +
                        "Console.WriteLine(\"Hello, world!\");"
                        + @"
                    }
                }
             }";
            Console.WriteLine(code);
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters();
            // Reference to System.Drawing library
            parameters.ReferencedAssemblies.Add("System.Drawing.dll");
            // True - memory generation, false - external file generation
            parameters.GenerateInMemory = true;
            // True - exe file generation, false - dll file generation
            parameters.GenerateExecutable = true;
            CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
            if (results.Errors.HasErrors)
            {
                StringBuilder sb = new StringBuilder();

                foreach (CompilerError error in results.Errors)
                {
                    sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText));
                }

                throw new InvalidOperationException(sb.ToString());
            }
            Assembly assembly = results.CompiledAssembly;
            Type program = assembly.GetType("First.Program");
            MethodInfo main = program.GetMethod("Main");
            main.Invoke(null, null);
            Console.ReadLine();
        }
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM