Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일

Roslyn 맛보기 - C# 소스 코드를 스크립트 처럼 다루는 방법


Roslyn 맛보기 (1) - C# 소스 코드를 스크립트 처럼 다루는 방법
Roslyn 맛보기 (2) - C# Interactive (1)
Roslyn 맛보기 (3) - C# Interactive (2)
Roslyn 맛보기 (4) - Roslyn Services APIs를 이용한 Code Issue 및 Code Action 기능 소개
Roslyn 맛보기 (5) - Syntax Analysis (Roslyn Syntax API)
Roslyn 맛보기 (6) - Roslyn Symbol / Binding API
Roslyn 맛보기 (7) - SyntaxTree 조작

Roslyn을 간단하게 설명하면, 컴파일러에 대한 Object Model 구조를 제공하는 거라고 보시면 됩니다. 예를 들어, Excel 프로그램에서 제공되는 Object Model을 사용하면 Excel 이 다루는 xls 문서 자체를 처리하는 것이 가능한데요. 마찬가지로, 컴파일러라는 것이 "소스 코드"를 다루는 것이기 때문에 Roslyn을 통해서 "소스 코드"에 대한 구문 분석 서비스까지 받을 수 있는 것입니다.

여기서는 ^^ 따분한 구문분석은 제쳐두고, 문자열로 구성된 C# 소스 코드를 C# 응용 프로그램에서 스크립트처럼 사용하는 활용 예를 소개해 보겠습니다.

실습을 위해서는 당연히 Roslyn 을 설치해야 겠지요. ^^ 그 전에, 우선 "Visual Studio 2010 SP1 SDK"를 먼저 설치해야 합니다.

Visual Studio 2010 SP1 SDK
; http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=21835

그 다음 "Roslyn CTP" 버전을 다운로드 해서 설치하면 됩니다.

Microsoft “Roslyn” CTP
; http://www.microsoft.com/download/en/details.aspx?id=27746

설치 완료 이후, (x64의 경우) "C:\Program Files (x86)\Microsoft Codename Roslyn CTP" 폴더가 생성됩니다. 그 하위에 "Documentation" 폴더를 보면 "Step by step"식으로 예제를 실습할 수 있는 워드 문서가 들어 있는데요, 이번에는 그 중에서 "Interactive - Scripting Introduction.docx" 예제를 다룬다고 보면 될 것 같습니다.




간단하게 "C# Console Application"을 생성하고, 참조 추가 대화상자를 통해 다음과 같이 2개의 어셈블리를 추가합니다.

  • Roslyn.Compilers
  • Roslyn.Compilers.CSharp

준비는 다음과 같이 여러분의 코드에서 using 문을 추가하는 것으로 끝이 납니다.

using Roslyn.Compilers;
using Roslyn.Scripting.CSharp;
using Roslyn.Scripting;

이제 다음과 같이 C# 소스 코드를 스크립트 다루듯이 실행하는 것이 가능합니다.

static void Main(string[] args)
{
    Program pg = new Program();
    pg.TestIt();
}

private void TestIt()
{
    var engine = new ScriptEngine(); 
    engine.Execute("using System; Console.WriteLine(DateTime.Now);");
}

생각보다 간단하지요. ^^




좀더 나아가서, Session 이라는 개념을 알아보기 위해 다음과 같이 바꿔보고 실행해 봅니다.

private void TestIt()
{
    var engine = new ScriptEngine(); 
    engine.Execute("using System;");
    engine.Execute("Console.WriteLine(DateTime.Now);");
}

그럼, 다음과 같은 예외가 발생하는 것을 볼 수 있습니다.

Unhandled Exception: Roslyn.Compilers.CompilationErrorException: (1,19): error CS0103: The name 'DateTime' does not exist in the current context
   at Roslyn.Scripting.CommonScriptEngine.CompilationError(DiagnosticBag localDiagnostics, DiagnosticBag diagnostics)
   at Roslyn.Scripting.CSharp.ScriptEngine.Compile(String code, String fileName, DiagnosticBag diagnostics, Session session, Type delegateType, Type returnType, CancellationToken cancellationToken, Boolean isInteractive, Boolean isExecute, ICompilation& compilation, Delegate& factory)
   at Roslyn.Scripting.CommonScriptEngine.Execute[T](String code, String fileName, DiagnosticBag diagnostics, Session session, Boolean isInteractive)
   at Roslyn.Scripting.CommonScriptEngine.Execute(String code, Session session)
   at ConsoleApplication1.Program.TestIt() in D:\...\Program.cs:line 25
   at ConsoleApplication1.Program.Main(String[] args) in D:\...\ConsoleAppl
ication1\Program.cs:line 18

그렇습니다. 스크립트 엔진 자체는 "using System;" 코드와 "Console.WriteLine(DateTime.Now);" 코드에 대해 전혀 다른 문맥을 적용해서 실행하기 때문에 위와 같은 예외가 발생하는 것입니다.

오류를 수정하려면 다음과 같이 코딩하는 것도 가능하지만,

var engine = new ScriptEngine(); 
engine.Execute("System.Console.WriteLine(System.DateTime.Now);");

일종의 연속될 수 있는 '문맥'을 적용해서 해결하는 방법도 있습니다. 바로 이런 개념으로 다가설 수 있는 것이 "Session"입니다. 그래서, 같은 세션 변수를 Execute 메서드에 전달해 줌으로써 연속적인 실행 결과를 동일한 문맥에서 처리할 수 있습니다.

var engine = new ScriptEngine(); 
var session = Session.Create();

engine.Execute("using System;", session);
engine.Execute("Console.WriteLine(DateTime.Now);", session);




세션을 이해하셨으니, 이제 스크립트 내의 코드와 그것을 실행하는 주체, 즉 Host 측과의 코드를 연동하는 것을 알아보겠습니다.

이를 위해서는, 호스트 측을 하나의 '문맥' - 세션으로 지정해서 스크립트 측에 넘겨주면 됩니다.

예를 들어, 호스트 측에 정의된 "DoHostCode();"라는 메서드를 스크립트에서 실행하려면 다음과 같이 코드를 정의하면 됩니다.

static void Main(string[] args)
{
    Program pg = new Program();
    pg.TestIt();
}

private void TestIt()
{
    var engine = new ScriptEngine(); 

    var session = Session.Create(this);
    engine.Execute("using System;", session);
    engine.Execute("Console.WriteLine(DateTime.Now);", session);
    engine.Execute("DoHostCode();", session);
}

public void DoHostCode()
{
    Console.WriteLine("Program.DoHostCode called!");
}

그런데, 실행하면 ^^ 다음과 같은 예외를 만날 수 있습니다.

Unhandled Exception: Roslyn.Compilers.CompilationErrorException: (1,1): error CS7012: The name 'DoHostCode' does not exist in the current context 
    (are you missing a reference to assembly 'ConsoleApplication1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'?)
   at Roslyn.Scripting.CommonScriptEngine.CompilationError(DiagnosticBag localDiagnostics, DiagnosticBag diagnostics)
   at Roslyn.Scripting.CSharp.ScriptEngine.Compile(String code, String fileName, DiagnosticBag diagnostics, Session sess
ion, Type delegateType, Type returnType, CancellationToken cancellationToken, Boolean isInteractive, Boolean isExecute, ICompilation& compilation, Delegate& factory)
   at Roslyn.Scripting.CommonScriptEngine.Execute[T](String code, String fileName, DiagnosticBag diagnostics, Session session, Boolean isInteractive)
   at Roslyn.Scripting.CommonScriptEngine.Execute(String code, Session session)
   at ConsoleApplication1.Program.TestIt() in D:\...\Pr
ogram.cs:line 29
   at ConsoleApplication1.Program.Main(String[] args) in D:\...\Program.cs:line 18
Press any key to continue . . .

쉽게 생각해서, 스크립트 내부에서 실행하는 코드 역시 하나의 "DLL"을 만드는 "C# 프로젝트"라고 여기시면 됩니다. 만약, 여러분들이 위와 같은 코드를 수행하는 C# 프로젝트를 만들었다면, 당연히 ConsoleApplication1.exe 를 참조 추가했을 것입니다.

Roslyn의 스크립트 엔진에서는 다음과 같은 구문으로 가능합니다.

var engine = new ScriptEngine(new[]
                                {
                                    this.GetType().Assembly.Location  // ConsoleApplication1.exe 경로
                                }); 

위와 같이 바꾸었는데도, 실행해 보면 여전히 예외가 발생할 것입니다.

Unhandled Exception: System.TypeAccessException: Attempt by method 'Submission#2..ctor(Roslyn.Scripting.Session, System.Object ByRef)' to access type 'ConsoleApplication1.Program' failed.
   at Submission#2..ctor(Session session, Object& submissionResult)
   at Submission#2.<Factory>(Session session)
   at Roslyn.Scripting.CommonScriptEngine.Execute[T](String code, String fileName, DiagnosticBag diagnostics, Session session, Boolean isInteractive)
   at Roslyn.Scripting.CommonScriptEngine.Execute(String code, Session session)
   at ConsoleApplication1.Program.TestIt() in D:\...\Program.cs:line 31
   at ConsoleApplication1.Program.Main(String[] args) in D:\...\Program.cs:line 18
Press any key to continue . . .

아하... 다시 한번 스크립트 내부의 코드가 별도의 "C# 프로젝트"라고 생각해야 한다는 것을 상기시키는 군요. 외부에서 참조하는 것이니, 위의 코드에서 Program 타입이 public 이 아니어서 이런 문제가 발생하는 것입니다.

모든 문제를 해결한 최종 코드는 다음과 같습니다.

public class Program
{
    static void Main(string[] args)
    {
        Program pg = new Program();
        pg.TestIt();
    }

    private void TestIt()
    {
        var engine = new ScriptEngine(new[]
                                        {
                                            this.GetType().Assembly.Location
                                        }); 

        var session = Session.Create(this);
        engine.Execute("using System;", session);
        engine.Execute("Console.WriteLine(DateTime.Now);", session);
        engine.Execute("DoHostCode();", session);
    }

    public void DoHostCode()
    {
        Console.WriteLine("Program.DoHostCode called!");
    }
}

==== 실행 결과 ====
2011-10-21 오전 12:10:30
Program.DoHostCode called!

실습 하나만 더 해볼까요? 스크립트 측에서 Host에서 제공하는 이벤트를 구독하는 것도 다음과 같이 가능합니다.

public class Program
{
    public delegate void TimeChangedDelegate(object objState);
    public TimeChangedDelegate TimeChanged;

    static void Main(string[] args)
    {
        Program pg = new Program();
        pg.TestIt();
    }

    private void TestIt()
    {
        var engine = new ScriptEngine(new[]
                                        {
                                            "System",
                                            this.GetType().Assembly.Location
                                        }); 

        var session = Session.Create(this);
        engine.Execute("using System;", session);
        engine.Execute("Console.WriteLine(DateTime.Now);", session);
        engine.Execute("DoHostCode();", session);
        engine.Execute("void timeChanged(object state) { Console.WriteLine(state); }", session);
        engine.Execute("TimeChanged += new TimeChangedDelegate(timeChanged);", session);

        Console.WriteLine(TimeChanged != null);
        TimeChanged(DateTime.Now);
    }

    public void DoHostCode()
    {
        Console.WriteLine("Program.DoHostCode called!");
    }
}

==== 실행 결과 ====
2011-10-21 오전 1:00:42
Program.DoHostCode called!
True
2011-10-21 오전 1:00:42

이 정도면... Roslyn 에서 제공되는 스크립트 엔진에 대해서 대략 감을 잡으실 수 있겠죠? ^^

물론, 위의 작업들은 기존에도 csc.exe를 직접 호출한 후 Reflection 을 통해서 이미 가능했었습니다. 하지만, Roslyn 의 경우 좀더 유기적으로 통합할 수 있는 길을 제공한다는 차이가 있는 것입니다.

[첨부된 파일은 위의 코드를 포함한 예제 프로젝트입니다.]

참고로, 참조된 2개의 어셈블리를 "Copy Local == True" 속성을 주어 exe 파일과 함께 배포하면 Roslyn 이 설치되지 않은 PC 에서도 정상적으로 실행됩니다. 적어도 스크립트 엔진 기능에 대해서는 Roslyn.Compilers.CSharp.dll, Roslyn.Compilers.dll 2개의 어셈블리에서 끝을 내준다는 의미입니다. (2개의 파일을 더해서 3MB가 채 안되는 군요.)




기타 사항이라면, 아쉽게도 아직은 CTP 버전이기 때문에 제법 많은 기능들이 누락되어 있는 문제가 있습니다. 이에 대해서는 다음의 문서에 정리되어 있으니 한번쯤 확인해 보시는 것도 좋겠지요. ^^

Microsoft “Roslyn” CTP - Readme.mht
; http://www.microsoft.com/download/en/details.aspx?id=27746

일례로, 아직은 지원되지 않는 unchecked 구문을 사용하면,

engine.Execute("int _value = unchecked((int)5);", session);

다음과 같은 예외가 발생합니다.

Unhandled Exception: Roslyn.Compilers.CompilationErrorException: (1,14): error CS8000: This language feature ('UncheckedExpression') is not yet implemented in Roslyn.
   at Roslyn.Scripting.CommonScriptEngine.CompilationError(DiagnosticBag localDiagnostics, DiagnosticBag diagnostics)
   at Roslyn.Scripting.CSharp.ScriptEngine.Compile(String code, String fileName, DiagnosticBag diagnostics, Session session, Type delegateType, Type returnType, CancellationToken cancellationToken, Boolean isInteractive, Boolean isExecute, ICompilation& compilation, Delegate& factory)
   at Roslyn.Scripting.CommonScriptEngine.Execute[T](String code, String fileName, DiagnosticBag diagnostics, Session session, Boolean isInteractive)
   at Roslyn.Scripting.CommonScriptEngine.Execute(String code, Session session)
   at ConsoleApplication1.Program.TestIt() in D:\...\Program.cs:line 39
   at ConsoleApplication1.Program.Main(String[] args) in D:\...\Program.cs:line 21
Press any key to continue . . .





[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 10/28/2011 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 



2011-11-01 09시24분
[spowner] 와우. 좋은 내용 소개해주셔서 감사합니다.
[손님]
2011-11-01 10시05분
이렇게라도 웹 사이트에 정리하면서 글을 쓰니, 저 스스로도 공부하게 되더군요. ^^
정성태
2011-12-19 05시26분
[Jett] 좋은 정보 감사드립니다.
Roslyn을 통해서 런타임에 메서드 교체해서 테스트 해볼 수 있을정도로 나와준다면 좋겠네요.
SyntaxTree.ParseCompilationUnit 의 파일 읽기 기능이 아직 구현 전이니 ^^;;
[손님]
2011-12-19 09시59분
Rolsyn 의 목적 자체가 '컴파일 서비스'이기 때문에 런타임에 메서드 교체하는 서비스는 아마 제공되지 않을 듯 싶습니다. 굳이 한다면, PostSharp 처럼 컴파일 후에 한번 더 해당 어셈블리를 다시 읽어서 빌드하는 식으로 처리할 수 있을 것입니다.
정성태
2012-04-03 08시52분
[test] 실행을 시키면 계속적으로 메모리가 한 없이 늘어나다 다운되네요...
[손님]
2012-04-03 09시59분
"test"님 좋은 테스트를 하셨군요. ^^ 아직 베타 버전도 아닌 CTP 단계이니 기다려봐야겠지만 아마도 Session 수준에서 Dispose 메서드가 제공되지 않을까 싶습니다.
정성태
2014-01-15 02시39분
C#의 스크립트 기능이 아쉽다면, Roslyn보다는 scriptcs로 선택하는 것이 좋겠죠. ^^

scriptcs
; http://scriptcs.net/
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12266정성태7/14/202032오류 유형: 630. 사용자 계정을 지정해 CreateService API로 서비스를 등록한 경우 "Error 1069: The service did not start due to a logon failure." 오류발생
12265정성태7/10/202050오류 유형: 629. Visual Studio - 웹 애플리케이션 실행 시 "Unable to connect to web server 'IIS Express'." 오류 발생
12264정성태7/9/202040오류 유형: 628. docker: Error response from daemon: Conflict. The container name "..." is already in use by container "...".
12261정성태7/9/2020213VS.NET IDE: 148. 윈도우 10에서 .NET Core 응용 프로그램을 리눅스 환경에서 실행하는 2가지 방법 - docker, WSL 2 [3]
12260정성태7/8/202084.NET Framework: 926. C# - ETW를 이용한 ThreadPool 스레드 감시파일 다운로드1
12259정성태7/8/202041오류 유형: 627. nvlddmkm.sys의 BAD_POOL_HEADER BSOD 문제
12258정성태7/8/2020106기타: 77. DataDog APM 간략 소개
12257정성태7/7/202078.NET Framework: 925. C# - ETW를 이용한 Monitor Enter/Exit 감시파일 다운로드1
12256정성태7/7/2020122.NET Framework: 924. C# - Reflection으로 변경할 수 없는 readonly 정적 필드 [4]
12255정성태7/6/202087.NET Framework: 923. C# - ETW(Event Tracing for Windows)를 이용한 Finalizer 실행 감시파일 다운로드1
12254정성태7/2/202055오류 유형: 626. git - REMOTE HOST IDENTIFICATION HAS CHANGED!
12253정성태7/2/2020135.NET Framework: 922. C# - .NET ThreadPool의 Local/Global Queue파일 다운로드1
12252정성태7/2/2020113.NET Framework: 921. C# - I/O 스레드를 사용한 비동기 소켓 서버/클라이언트파일 다운로드2
12251정성태7/1/2020137.NET Framework: 920. C# - 파일의 비동기 처리 유무에 따른 스레드 상황파일 다운로드2
12250정성태7/1/2020368.NET Framework: 919. C# - 닷넷에서의 진정한 비동기 호출을 가능케 하는 I/O 스레드 사용법 [1]파일 다운로드1
12249정성태6/29/202055오류 유형: 625. Microsoft SQL Server 2019 RC1 Setup - 설치 제거 시 Warning 26003 오류 발생
12248정성태6/29/202057오류 유형: 624. SQL 서버 오류 - service-specific error code 17051
12247정성태6/29/2020157.NET Framework: 918. C# - 불린 형 상수를 반환값으로 포함하는 3항 연산자 사용 시 단축 표현 권장(IDE0075) [2]파일 다운로드1
12246정성태6/29/202095.NET Framework: 917. C# - USB 관련 ETW(Event Tracing for Windows)를 이용한 키보드 입력을 감지하는 방법
12245정성태6/25/2020265.NET Framework: 916. C# - Task.Yield 사용법 (2) [2]파일 다운로드1
12244정성태6/29/2020131.NET Framework: 915. ETW(Event Tracing for Windows)를 이용한 닷넷 프로그램의 내부 이벤트 활용파일 다운로드1
12243정성태6/23/202086VS.NET IDE: 147. Visual C++ 프로젝트 - .NET Core EXE를 "Debugger Type"으로 지원하는 기능 추가
12242정성태6/24/202058오류 유형: 623. AADSTS90072 - User account '...' from identity provider 'live.com' does not exist in tenant 'Microsoft Services'
12241정성태6/26/2020147.NET Framework: 914. C# - Task.Yield 사용법파일 다운로드1
12240정성태6/23/2020125오류 유형: 622. 소켓 바인딩 시 "System.Net.Sockets.SocketException: An attempt was made to access a socket in a way forbidden by its access permissions" 오류 발생
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...