是否可以动态编译和执行 C# 代码片段?

Posted

技术标签:

【中文标题】是否可以动态编译和执行 C# 代码片段?【英文标题】:Is it possible to dynamically compile and execute C# code fragments? 【发布时间】:2010-10-24 00:10:38 【问题描述】:

我想知道是否可以将 C# 代码片段保存到文本文件(或任何输入流)中,然后动态执行它们?假设提供给我的内容可以在任何 Main() 块中正常编译,是否可以编译和/或执行此代码?出于性能原因,我更愿意编译它。

至少,我可以定义一个他们需要实现的接口,然后他们会提供一个实现该接口的代码“部分”。

【问题讨论】:

我知道这篇文章已经有几年的历史了,但我认为值得一提的是 Project Roslyn 的介绍,它能够即时编译原始 C# 并在 .NET 程序中运行它只是简单一点。 【参考方案1】:

要进行编译,您只需启动对 csc 编译器的 shell 调用。保持路径和开关笔直可能会让您头疼,但肯定可以做到。

C# Corner Shell Examples

编辑:或者更好的是,按照诺多林的建议使用 CodeDOM...

【讨论】:

是的,CodeDOM 的好处是它可以在内存中为您生成程序集(以及以易于阅读的格式提供错误消息和其他信息)。 @Noldorin,C# CodeDOM 实现实际上并没有在内存中生成程序集。您可以为其启用标志,但它会被忽略。它改为使用临时文件。 @Matt:是的,好点——我忘记了这个事实。尽管如此,它仍然极大地简化了流程(使其有效地看起来就好像程序集是在内存中生成的一样),并提供了一个完整的托管接口,这比处理流程要好得多。 另外,CodeDomProvider 只是一个调用 csc.exe 的类。【参考方案2】:

在 C#/所有静态 .NET 语言中的最佳解决方案是使用 CodeDOM 处理此类事情。 (请注意,它的另一个主要目的是动态构建代码位,甚至是整个类。)

这是来自LukeH's blog 的一个很好的简短示例,它使用一些 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,它利用编译器即时编译代码。如果你想然后运行代码,你只需要使用一点反射来动态加载程序集并执行它。

Here 是 C# 中的另一个示例(虽然不太简洁),它还准确地向您展示了如何使用 System.Reflection 命名空间运行运行时编译的代码。

【讨论】:

虽然我怀疑您使用 Mono,但我认为可能值得指出存在一个 Mono.CSharp 命名空间 (mono-project.com/CSharp_Compiler),它实际上包含一个编译器作为服务,以便您可以动态运行内联基本代码/求值表达式,省时省力。 现实世界需要这样做。总的来说,我对编程很陌生,我认为这很酷,但我想不出你想要/这会有用的原因。如果你能解释一下,谢谢。 @Crash893:几乎任何类型的设计器应用程序的脚本系统都可以很好地利用这一点。当然,还有 IronPython LUA 等替代方案,但这肯定是其中之一。请注意,通过公开接口并加载包含它们的实现的已编译 DLL,而不是直接加载代码,会更好地开发插件系统。 我一直认为“CodeDom”是让我使用 DOM(文档对象模型)创建代码文件的东西。在 System.CodeDom 中,有一些对象来表示代码包含的所有工件——类的对象、接口的对象、构造函数的对象、语句、属性、字段等等。然后我可以使用该对象模型构建代码。此答案中显示的是在程序中编译代码文件。不是 CodeDom,虽然像 CodeDom 一样,它动态生成程序集。类比:我可以使用 DOM 或使用字符串 concats 创建一个 html 页面。 这是一篇展示 CodeDom 运行的 SO 文章:***.com/questions/865052/…【参考方案3】:

其他人已经就如何在运行时生成代码给出了很好的答案,所以我想我会解决你的第二段。我对此有一些经验,只想分享我从那次经历中学到的教训。

至少,我可以定义一个 需要它们的接口 实施,然后他们将提供 实现该功能的代码“部分” 界面。

如果您使用interface 作为基本类型,您可能会遇到问题。如果您将来向interface 添加一个新方法,那么所有实现interface 的现有客户端提供的类现在都将变为抽象,这意味着您将无法在运行时编译或实例化客户端提供的类。

在发布旧界面大约 1 年并分发大量需要支持的“遗留”数据之后,当需要添加新方法时,我遇到了这个问题。我最终创建了一个继承自旧接口的新接口,但这种方法使得加载和实例化客户端提供的类变得更加困难,因为我必须检查哪个接口可用。

我当时想到的一个解决方案是使用一个实际的类作为基类型,如下所示。类本身可以标记为抽象,但所有方法都应该是空的虚拟方法(不是抽象方法)。然后,客户可以覆盖他们想要的方法,我可以向基类添加新方法,而不会使现有的客户提供的代码失效。

public abstract class BaseClass

    public virtual void Foo1()  
    public virtual bool Foo2()  return false; 
    ...

无论这个问题是否适用,您都应该考虑如何对代码库和客户端提供的代码之间的接口进行版本控制。

【讨论】:

这是一个有价值的、有用的观点。【参考方案4】:

You can compile a piece C# of code into memory and generate assembly bytes 与罗斯林。已经提到过,但值得在这里添加一些 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# 编译器使用的代码相同,这是最大的好处。复杂是一个相对术语,但在运行时编译代码无论如何都是一项复杂的工作。但是,上面的代码一点也不复杂。【参考方案5】:

发现这很有用 - 确保编译后的程序集引用您当前引用的所有内容,因为您很有可能希望您正在编译的 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 请求。相反,在低优先级线程上主动编译您需要的代码,并有一种方法来处理需要该代码准备好的代码,直到它有机会完成编译。例如,您可以在批处理作业过程中使用它。

【讨论】:

您的答案是独一无二的。其他人没有解决我的问题。 将当前运行的代码中所有引用的程序集都提供给您正在编译的程序集,这是一个不错的主意。我不得不使用 compilerParams.ReferencedAssemblies.Add() 为编译后的程序集提供更多引用的程序集。 我希望这种方法能解决我在单元测试中遇到的问题,其中 CodeDOM 生成的代码引用了与当前加载的相同程序集中的类,但来自不同的文件夹,但可以t 在执行期间找到它的类型。【参考方案6】:

我最近需要为单元测试生成进程。这篇文章很有用,因为我创建了一个简单的类来使用作为字符串的代码或我的项目中的代码来做到这一点。要构建此类,您将需要 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"); 

【讨论】:

【参考方案7】:
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();
        
    

【讨论】:

以上是关于是否可以动态编译和执行 C# 代码片段?的主要内容,如果未能解决你的问题,请参考以下文章

C#执行js动态编译的方法

c# 动态编译

程序动态编译代码

C#中可以嵌入js编译器吗?这样我就好在.net中用js来写代码了

如何用C#动态编译

c#字符串代码,动态创建编译器