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


177

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

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


11
나는이 게시물이 몇 살이라는 것을 알고 있지만 Project Roslyn 의 소개로 원시 C #을 즉시 컴파일하고 .NET 프로그램 내에서 실행할 수있는 기능이 조금 더 쉽다고 생각했습니다.
로렌스

답변:


176

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네임 스페이스.


3
모노를 사용하는 것이 의심 스럽지만 실제로 컴파일러를 서비스로 포함하는 Mono.CSharp 네임 스페이스 ( mono-project.com/CSharp_Compiler )가 동적으로 기본 코드 / 평가를 실행할 수 있음을 지적하는 것이 가치가 있다고 생각했습니다. 번거롭지 않은 인라인 표현식.
Noldorin

1
이를 위해 실제로 필요한 것은 무엇입니까? 나는 일반적으로 프로그래밍에 꽤 녹색이며 이것이 멋지다고 생각하지만 왜 당신이 원하는지 / 생각할 수없는 이유를 생각할 수는 없습니다. 설명해 주시면 감사합니다.
Crash893

1
@ Crash893 : 거의 모든 종류의 디자이너 응용 프로그램을위한 스크립팅 시스템이이를 잘 활용할 수 있습니다. 물론 IronPython LUA와 같은 대안이 있지만 이것은 확실히 하나입니다. 플러그인 시스템은 코드를 직접로드하는 대신 인터페이스를 노출하고 해당 구현이 포함 된 컴파일 된 DLL을로드하여 더 잘 개발 될 수 있습니다.
Noldorin

저는 항상 "CodeDom"을 DOM (문서 객체 모델)을 사용하여 코드 파일을 만들 수있는 것으로 생각했습니다. System.CodeDom에는 클래스, 인터페이스, 생성자, 명령문, 속성, 필드 등 코드에 포함 된 모든 아티팩트를 나타내는 오브젝트가 있습니다. 그런 다음 해당 객체 모델을 사용하여 코드를 생성 할 수 있습니다. 이 답변에 여기에 표시된 것은 프로그램에서 코드 파일을 컴파일하는 것입니다. CodeDom이 아니라 CodeDom과 마찬가지로 동적으로 어셈블리를 생성합니다. 유추 : DOM을 사용하거나 문자열 연결을 사용하여 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 # 컴파일러가 사용하는 것과 동일한 코드이며 가장 큰 이점입니다. Complex는 상대적인 용어이지만 런타임에 코드를 컴파일하는 것은 어쨌든 복잡한 작업입니다. 그러나 위의 코드는 전혀 복잡하지 않습니다.
tugberk

41

다른 사람들은 이미 런타임에 코드를 생성하는 방법에 대한 좋은 대답을했기 때문에 두 번째 단락을 다루겠다고 생각했습니다. 나는 이것에 대한 경험이 있으며 그 경험에서 얻은 교훈을 공유하고 싶습니다.

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

interface기본 유형으로 사용하면 문제가있을 수 있습니다 . interface나중에 새로운 단일 메소드를 추가 하면 interface현재 구현하는 기존의 모든 클라이언트 제공 클래스가 이제 추상이되어 런타임에 클라이언트 제공 클래스를 컴파일하거나 인스턴스화 할 수 없습니다.

이전 인터페이스를 배송한지 약 1 년이 지나고 지원해야하는 많은 "레거시"데이터를 배포 한 후 새로운 방법을 추가 할 때가 왔습니다. 나는 이전 인터페이스에서 상속받은 새로운 인터페이스를 만들었지만이 방법은 사용 가능한 인터페이스를 확인해야하기 때문에 클라이언트 제공 클래스를로드하고 인스턴스화하는 것을 더 어렵게 만들었습니다.

당시 내가 생각한 한 가지 해결책은 실제 클래스를 아래 유형과 같은 기본 유형으로 사용하는 것이 었습니다. 클래스 자체는 추상으로 표시 될 수 있지만 모든 메소드는 빈 가상 메소드 여야합니다 (추상 메소드는 아님). 그런 다음 클라이언트는 원하는 메소드를 대체 할 수 있으며 기존 클라이언트 제공 코드를 무효화하지 않고 기본 클래스에 새 메소드를 추가 할 수 있습니다.

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

이 문제가 적용되는지 여부에 관계없이 코드 기반과 클라이언트 제공 코드 간의 인터페이스 버전을 지정하는 방법을 고려해야합니다.


2
그것은 귀중하고 유용한 관점입니다.
Cheeso

5

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


당신의 대답은 독특합니다. 다른 사람들은 내 문제를 해결하지 못합니다.
FindOutIslamNow

3

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

C # 코너 쉘 예제

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


예, CodeDOM의 장점은 메모리에서 어셈블리를 생성 할 수 있으며 오류 메시지 및 기타 정보를 쉽게 읽을 수있는 형식으로 제공 할 수 있다는 것입니다.
Noldorin

3
@Noldorin, C # CodeDOM 구현은 실제로 메모리에 어셈블리를 생성하지 않습니다. 이를 위해 플래그를 사용할 수 있지만 무시됩니다. 대신 임시 파일을 사용합니다.
Matthew Olenik

@ 매트 : 그래, 좋은 지적-그 사실을 잊었다. 그럼에도 불구하고 여전히 프로세스를 크게 단순화하고 ( 어셈블리가 메모리에서 생성 된 것처럼 효과적으로 표시 함) 완벽한 관리 인터페이스를 제공하므로 프로세스를 처리하는 것보다 훨씬 좋습니다.
Noldorin

또한 CodeDomProvider는 csc.exe를 호출하는 클래스 일뿐입니다.
justin.m.chase

1

최근에 단위 테스트를위한 프로세스를 생성해야했습니다. 이 게시물은 코드를 문자열 또는 프로젝트의 코드로 사용하여 간단한 클래스를 만들 때 유용했습니다. 이 클래스를 빌드하려면 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"); }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.