C#コードフラグメントを動的にコンパイルして実行することは可能ですか?


177

C#コードフラグメントをテキストファイル(または任意の入力ストリーム)に保存し、それらを動的に実行することが可能かどうか疑問に思っていましたか?提供されたものがMain()ブロック内で問題なくコンパイルされると仮定すると、このコードをコンパイルおよび/または実行することは可能ですか?パフォーマンス上の理由から、コンパイルしたいと思います。

少なくとも、実装するために必要なインターフェイスを定義し、そのインターフェイスを実装するコード「セクション」を提供することができます。


11
私はこの投稿が数年前であることを知っていますが、Project Roslynの紹介で言及する価値があると思いました。その場で生のC#をコンパイルして.NETプログラム内で実行する機能は少し簡単です。
ローレンス

回答:


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)が存在することを指摘するのは価値があると思いました。インライン式、最小限の手間で。
Noldorin 2009

1
これを行うための現実の世界のニーズは何でしょうか。私はプログラミング全般についてかなり環境に配慮しています。これはクールだと思いますが、あなたが望む理由や、これが役立つ理由は思いつきません。説明してくれてありがとう。
Crash893 2009年

1
@ Crash893:ほぼすべての種類のデザイナーアプリケーションのスクリプトシステムは、これをうまく利用できます。もちろん、IronPython LUAなどの代替手段がありますが、これは確かに1つです。プラグインシステムは、インターフェイスを公開し、コードを直接ロードするよりも、それらの実装を含むコンパイル済みDLLをロードすることで、より適切に開発されることに注意してください。
ノルドリン2009年

私はいつも "CodeDom"を、DOM(ドキュメントオブジェクトモデル)を使用してコードファイルを作成できるものと考えていました。System.CodeDomには、コードに含まれるすべてのアーティファクト(クラス、インターフェイス、コンストラクター、ステートメント、プロパティ、フィールドなど)を表すオブジェクトがあります。次に、そのオブジェクトモデルを使用してコードを作成できます。この回答に示されているのは、プログラムでコードファイルをコンパイルすることです。CodeDomではなく、CodeDomと同様に、動的にアセンブリを生成します。類推:DOMまたは文字列連結を使用してHTMLページを作成できます。
Cheeso、2009年

:ここでのSOの記事は、ショーのアクションでの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 2015年

41

他の人はすでに実行時にコードを生成する方法について良い答えを出しているので、私はあなたの2番目の段落に取り組むと思いました。私はこれについていくつかの経験を持っていますが、その経験から学んだ教訓を共有したいと思います。

少なくとも、実装するために必要なインターフェイスを定義し、そのインターフェイスを実装するコード「セクション」を提供することができます。

interface基本型として使用すると、問題が発生する可能性があります。interface将来、単一の新しいメソッドをに追加すると、interface現在実装されているすべての既存のクライアント提供クラスが抽象化されます。つまり、実行時にクライアント提供クラスをコンパイルまたはインスタンス化できなくなります。

古いインターフェイスを出荷してから約1年後、サポートする必要のある大量の「レガシー」データを配布した後、新しいメソッドを追加するときにこの問題が発生しました。古いインターフェイスを継承した新しいインターフェイスを作成してしまいましたが、この方法では、利用可能なインターフェイスを確認する必要があったため、クライアントが提供したクラスの読み込みとインスタンス化が難しくなりました。

当時私が考えていた解決策の1つは、実際のクラスを以下のような基本型として使用することでした。クラス自体を抽象としてマークすることもできますが、すべてのメソッドは(抽象メソッドではなく)空の仮想メソッドでなければなりません。その後、クライアントは必要なメソッドをオーバーライドでき、既存のクライアント提供のコードを無効にすることなく、基本クラスに新しいメソッドを追加できます。

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

この問題が当てはまるかどうかに関係なく、コードベースとクライアント提供のコードの間のインターフェイスのバージョン管理方法を検討する必要があります。


2
それは価値ある有益な視点です。
Cheeso、2009年

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 staticメソッドがあり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コンパイラへのシェル呼び出しを開始するだけです。パスとスイッチをまっすぐに維持しようとすると頭痛がするかもしれませんが、それは確かに実行できます。

C#コーナーシェルの例

編集:またはより良い、Noldorinが提案したようにCodeDOMを使用してください...


ええ、CodeDOMのいいところは、メモリ内にアセンブリを生成できること(およびエラーメッセージやその他の情報を読みやすい形式で提供できること)です。
ノルドリン2009

3
@ Noldorin、C#CodeDOM実装は、実際にはメモリ内にアセンブリを生成しません。フラグを有効にすることはできますが、無視されます。代わりに一時ファイルを使用します。
Matthew Olenik、

@マット:ええ、良い点-私はその事実を忘れていました。それにもかかわらず、それでもプロセスが大幅に簡略化され(アセンブリがメモリで生成されたかのように効果的に表示され)、プロセスを処理するよりもはるかに優れた完全なマネージインターフェイスが提供されます。
ノルドリン2009

また、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.