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


177

我想知道是否可以将C#代码片段保存到文本文件(或任何输入流)中,然后动态执行它们?假设提供给我的东西可以在任何Main()块中编译良好,是否可以编译和/或执行此代码?由于性能原因,我宁愿对其进行编译。

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


11
我知道这篇文章已有几年历史了,但我认为值得引入Roslyn项目,即时编译原始C#并在.NET程序中运行它的功能要容易一些。
劳伦斯

Answers:


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命名空间(mono-project.com/CSharp_Compiler),该命名空间实际上包含编译器作为服务,以便您可以动态运行基本代码/求值内联表达式,麻烦最少。
诺多林

1
现实世界中需要这样做吗?我一般在编程方面都很绿色,我认为这很酷,但是我想不出为什么您想要/这会有用的原因。谢谢,如果你能解释。
Crash893

1
@ Crash893:几乎任何类型的设计器应用程序的脚本系统都可以很好地利用此功能。当然,还有IronPython LUA之类的替代方法,但这当然是一种。请注意,通过公开接口并加载包含接口实现的已编译DLL,而不是直接加载代码,可以更好地开发插件系统。
诺多林

我一直认为“ CodeDom”是让我使用DOM(文档对象模型)创建代码文件的东西。在System.CodeDom中,有一些对象来表示代码包括的所有工件-类,接口,构造函数,语句,属性,字段等的对象。然后,我可以使用该对象模型构造代码。此答案此处显示的是在程序中编译代码文件。尽管与CodeDom类似,但不是CodeDom,它会动态生成程序集。打个比方:我可以使用DOM或使用字符串concats创建HTML页面。
Cheeso

这是一篇展示CodeDom实际运行的SO文章: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 2015年

41

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

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

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

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

我当时想到的一种解决方案是改为使用实际的类作为基本类型,例如以下一种。该类本身可以标记为抽象,但所有方法都应为空的虚拟方法(而不是抽象方法)。然后,客户端可以覆盖所需的方法,并且我可以在不使现有客户端提供的代码无效的情况下将新方法添加到基类。

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具有一个名为public的公共静态方法Get(),并以type返回StoryDataIds。这是调用该方法的样子:

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

警告:编译可能非常缓慢。在我们相对较快的服务器上,一小段相对简单的10行代码块将以2-10秒的正常优先级进行编译。您绝对不应将呼叫绑定到CompileAssemblyFromSource()具有正常性能期望的任何内容,例如Web请求。相反,可以在低优先级线程上主动编译所需的代码,并有一种处理需要该代码准备就绪的代码的方式,直到有机会完成编译为止。例如,您可以在批处理作业过程中使用它。


您的答案是独一无二的。其他人不能解决我的问题。
FindOutIslamNow

3

要进行编译,您可以仅启动对csc编译器的shell调用。您可能会头痛不已,想保持自己的路径和转弯,但肯定可以做到。

C#角壳示例

编辑:或更好,但使用CodeDOM作为Noldorin建议...


是的,CodeDOM的好处是它可以在内存中为您生成程序集(以及以易于阅读的格式提供错误消息和其他信息)。
诺多林

3
@ Noldorin,C#CodeDOM实现实际上并未在内存中生成程序集。您可以为其启用该标志,但是它将被忽略。它使用一个临时文件代替。
马修·奥莱尼克

@Matt:是的,很好-我忘记了这个事实。尽管如此,它仍然大大简化了流程(使程序集看起来好像在内存中生成一样有效),并提供了一个完整的托管接口,这比处理进程要好得多。
诺多林

另外,CodeDomProvider只是一个仍要调用csc.exe的类。
justin.m.chase 2011年

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"); }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.