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/
정성태

1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...
NoWriterDateCnt.TitleFile(s)
13294정성태3/22/20234122.NET Framework: 2105. LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 - 두 번째
13293정성태3/22/20234191오류 유형: 853. dumpbin - warning LNK4048: Invalid format file; ignored
13292정성태3/21/20234305Windows: 232. C/C++ - 일반 창에도 사용 가능한 IsDialogMessage파일 다운로드1
13291정성태3/20/20234712.NET Framework: 2104. C# Windows Forms - WndProc 재정의와 IMessageFilter 사용 시의 차이점
13290정성태3/19/20234219.NET Framework: 2103. C# - 윈도우에서 기본 제공하는 FindText 대화창 사용법파일 다운로드1
13289정성태3/18/20233416Windows: 231. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 자식 윈도우를 생성하는 방법파일 다운로드1
13288정성태3/17/20233515Windows: 230. Win32 - 대화창의 DLU 단위를 pixel로 변경하는 방법파일 다운로드1
13287정성태3/16/20233683Windows: 229. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 윈도우를 직접 띄우는 방법파일 다운로드1
13286정성태3/15/20234146Windows: 228. Win32 - 리소스에 포함된 대화창 Template의 2진 코드 해석 방법
13285정성태3/14/20233736Windows: 227. Win32 C/C++ - Dialog Procedure를 재정의하는 방법파일 다운로드1
13284정성태3/13/20233936Windows: 226. Win32 C/C++ - Dialog에서 값을 반환하는 방법파일 다운로드1
13283정성태3/12/20233477오류 유형: 852. 파이썬 - TypeError: coercing to Unicode: need string or buffer, NoneType found
13282정성태3/12/20233807Linux: 58. WSL - nohup 옵션이 필요한 경우
13281정성태3/12/20233716Windows: 225. 윈도우 바탕화면의 아이콘들이 넓게 퍼지는 경우 [2]
13280정성태3/9/20234453개발 환경 구성: 670. WSL 2에서 호스팅 중인 TCP 서버를 외부에서 접근하는 방법
13279정성태3/9/20233996오류 유형: 851. 파이썬 ModuleNotFoundError: No module named '_cffi_backend'
13278정성태3/8/20233940개발 환경 구성: 669. WSL 2의 (init이 아닌) systemd 지원 [1]
13277정성태3/6/20234575개발 환경 구성: 668. 코드 사인용 인증서 신청 및 적용 방법(예: Digicert)
13276정성태3/5/20234306.NET Framework: 2102. C# 11 - ref struct/ref field를 위해 새롭게 도입된 scoped 예약어
13275정성태3/3/20234660.NET Framework: 2101. C# 11의 ref 필드 설명
13274정성태3/2/20234252.NET Framework: 2100. C# - ref 필드로 ref struct 타입을 허용하지 않는 이유
13273정성태2/28/20233953.NET Framework: 2099. C# - 관리 포인터로서의 ref 예약어 의미
13272정성태2/27/20234199오류 유형: 850. SSMS - mdf 파일을 Attach 시킬 때 Operating system error 5: "5(Access is denied.)" 에러
13271정성태2/25/20234142오류 유형: 849. Sql Server Configuration Manager가 시작 메뉴에 없는 경우
13270정성태2/24/20233754.NET Framework: 2098. dotnet build에 /p 옵션을 적용 시 유의점
13269정성태2/23/20234289스크립트: 46. 파이썬 - uvicorn의 콘솔 출력을 UDP로 전송
1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...