Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 10개 있습니다.)

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
; https://marketplace.visualstudio.com/items?itemName=VisualStudioProductTeam.VisualStudio2010SP1SDK

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

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

dotnet/roslyn
; https://github.com/dotnet/roslyn

설치 완료 이후, (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 . . .





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

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/10/2021]

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

비밀번호

댓글 작성자
 



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

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

... [76]  77  78  79  80  81  82  83  84  85  86  87  88  89  90  ...
NoWriterDateCnt.TitleFile(s)
12036정성태10/14/201925390.NET Framework: 866. C# - 고성능이 필요한 환경에서 GC가 발생하지 않는 네이티브 힙 사용파일 다운로드1
12035정성태10/13/201919569개발 환경 구성: 461. C# 8.0의 #nulable 관련 특성을 .NET Framework 프로젝트에서 사용하는 방법 [2]파일 다운로드1
12034정성태10/12/201918887개발 환경 구성: 460. .NET Core 환경에서 (프로젝트가 아닌) C# 코드 파일을 입력으로 컴파일하는 방법 [1]
12033정성태10/11/201923071개발 환경 구성: 459. .NET Framework 프로젝트에서 C# 8.0/9.0 컴파일러를 사용하는 방법
12032정성태10/8/201919203.NET Framework: 865. .NET Core 2.2/3.0 웹 프로젝트를 IIS에서 호스팅(Inproc, out-of-proc)하는 방법 - AspNetCoreModuleV2 소개
12031정성태10/7/201916467오류 유형: 569. Azure Site Extension 업그레이드 시 "System.IO.IOException: There is not enough space on the disk" 예외 발생
12030정성태10/5/201923261.NET Framework: 864. .NET Conf 2019 Korea - "닷넷 17년의 변화 정리 및 닷넷 코어 3.0" 발표 자료 [1]파일 다운로드1
12029정성태9/27/201924100제니퍼 .NET: 29. Jennifersoft provides a trial promotion on its APM solution such as JENNIFER, PHP, and .NET in 2019 and shares the examples of their application.
12028정성태9/26/201919052.NET Framework: 863. C# - Thread.Suspend 호출 시 응용 프로그램 hang 현상을 해결하기 위한 시도파일 다운로드1
12027정성태9/26/201914800오류 유형: 568. Consider app.config remapping of assembly "..." from Version "..." [...] to Version "..." [...] to solve conflict and get rid of warning.
12026정성태9/26/201920226.NET Framework: 862. C# - Active Directory의 LDAP 경로 및 정보 조회
12025정성태9/25/201918527제니퍼 .NET: 28. APM 솔루션 제니퍼, PHP, .NET 무료 사용 프로모션 2019 및 적용 사례 (8) [1]
12024정성태9/20/201920432.NET Framework: 861. HttpClient와 HttpClientHandler의 관계 [2]
12023정성태9/18/201920900.NET Framework: 860. ServicePointManager.DefaultConnectionLimit와 HttpClient의 관계파일 다운로드1
12022정성태9/12/201924855개발 환경 구성: 458. C# 8.0 (Preview) 신규 문법을 위한 개발 환경 구성 [3]
12021정성태9/12/201940652도서: 시작하세요! C# 8.0 프로그래밍 [4]
12020정성태9/11/201923832VC++: 134. SYSTEMTIME 값 기준으로 특정 시간이 지났는지를 판단하는 함수
12019정성태9/11/201917388Linux: 23. .NET Core + 리눅스 환경에서 Environment.CurrentDirectory 접근 시 주의 사항
12018정성태9/11/201916182오류 유형: 567. IIS - Unrecognized attribute 'targetFramework'. Note that attribute names are case-sensitive. (D:\lowSite4\web.config line 11)
12017정성태9/11/201919998오류 유형: 566. 비주얼 스튜디오 - Failed to register URL "http://localhost:6879/" for site "..." application "/". Error description: Access is denied. (0x80070005)
12016정성태9/5/201920011오류 유형: 565. git fetch - warning: 'C:\ProgramData/Git/config' has a dubious owner: '(unknown)'.
12015정성태9/3/201925396개발 환경 구성: 457. 윈도우 응용 프로그램의 Socket 연결 시 time-out 시간 제어
12014정성태9/3/201919136개발 환경 구성: 456. 명령행에서 AWS, Azure 등의 원격 저장소에 파일 관리하는 방법 - cyberduck/duck 소개
12013정성태8/28/201922051개발 환경 구성: 455. 윈도우에서 (테스트) 인증서 파일 만드는 방법 [3]
12012정성태8/28/201926611.NET Framework: 859. C# - HttpListener를 이용한 HTTPS 통신 방법
12011정성태8/27/201926210사물인터넷: 57. C# - Rapsberry Pi Zero W와 PC 간 Bluetooth 통신 예제 코드파일 다운로드1
... [76]  77  78  79  80  81  82  83  84  85  86  87  88  89  90  ...