[C#] C # 코드 조각을 동적으로 컴파일하고 실행할 수 있습니까?

C # 코드 조각을 텍스트 파일 (또는 입력 스트림)에 저장하고 동적으로 실행할 수 있는지 궁금합니다. 나에게 제공된 것이 Main () 블록 내에서 잘 컴파일된다고 가정하면이 코드를 컴파일 및 / 또는 실행할 수 있습니까? 성능상의 이유로 컴파일하는 것을 선호합니다.

최소한 구현에 필요한 인터페이스를 정의한 다음 해당 인터페이스를 구현하는 코드 ‘섹션’을 제공 할 수 있습니다.



답변

C # / 모든 정적 .NET 언어에서 가장 좋은 솔루션은 이러한 작업에 CodeDOM 을 사용하는 것입니다. 참고로 다른 주요 목적은 코드 비트 또는 전체 클래스를 동적으로 생성하는 것입니다.

다음 은 재미를 위해 LINQ를 사용하는 LukeH의 블로그 에서 가져온 짧은 예제 입니다.

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 #에서 컴파일하는 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()웹 요청과 같이 정상적인 성능을 기대하는 어떤 것에도 전화를 연결해서는 안됩니다 . 대신 우선 순위가 낮은 스레드에서 필요한 코드를 사전에 컴파일하고 컴파일이 완료 될 때까지 해당 코드를 준비해야하는 코드를 처리 할 수 ​​있습니다. 예를 들어 배치 작업 프로세스에서 사용할 수 있습니다.


답변

컴파일하려면 csc 컴파일러에 대한 쉘 호출을 시작하면됩니다. 당신은 당신의 경로와 스위치를 똑바로 유지하려고 두통이있을 수 있지만 확실히 할 수 있습니다.

C # 코너 쉘 예제

편집하다 : 또는 더 나은 아직 Noldorin이 제안한대로 CodeDOM을 사용하십시오 …


답변

최근에 단위 테스트를위한 프로세스를 생성해야했습니다. 이 게시물은 코드를 문자열 또는 프로젝트의 코드로 사용하여 간단한 클래스를 만들 때 유용했습니다. 이 클래스를 빌드하려면 ICSharpCode.DecompilerMicrosoft.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"); }
}


답변