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)
13247정성태2/7/20234962VS.NET IDE: 180. Visual Studio - 닷넷 소스 코드 디버깅 중 "Decompile source code"가 동작하는 않는 문제
13246정성태2/6/20234085개발 환경 구성: 664. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 - 두 번째 이야기
13245정성태2/6/20234627.NET Framework: 2093. C# - PEM 파일을 이용한 RSA 개인키/공개키 설정 방법파일 다운로드1
13244정성태2/5/20233983VS.NET IDE: 179. Visual Studio - External Tools에 Shell 내장 명령어 등록
13243정성태2/5/20234855디버깅 기술: 190. windbg - Win32 API 호출 시점에 BP 거는 방법 [1]
13242정성태2/4/20234292디버깅 기술: 189. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.UnauthorizedAccessException
13241정성태2/3/20233824디버깅 기술: 188. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.IO.FileNotFoundException
13240정성태2/1/20233982디버깅 기술: 187. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.Web.HttpException
13239정성태2/1/20233625디버깅 기술: 186. C# - CacheDependency의 숨겨진 예외 - System.Web.HttpException
13238정성태1/31/20235631.NET Framework: 2092. IIS 웹 사이트를 TLS 1.2 또는 TLS 1.3 프로토콜로만 운영하는 방법
13237정성태1/30/20235320.NET Framework: 2091. C# - 웹 사이트가 어떤 버전의 TLS/SSL을 지원하는지 확인하는 방법
13236정성태1/29/20234964개발 환경 구성: 663. openssl을 이용해 인트라넷 IIS 사이트의 SSL 인증서 생성
13235정성태1/29/20234508개발 환경 구성: 662. openssl - 윈도우 환경의 명령행에서 SAN 적용하는 방법
13234정성태1/28/20235547개발 환경 구성: 661. dnSpy를 이용해 소스 코드가 없는 .NET 어셈블리의 코드를 변경하는 방법 [1]
13233정성태1/28/20236897오류 유형: 840. C# - WebClient로 https 호출 시 "The request was aborted: Could not create SSL/TLS secure channel" 예외 발생
13232정성태1/27/20234698스크립트: 43. uwsgi의 --processes와 --threads 옵션
13231정성태1/27/20233614오류 유형: 839. python - TypeError: '...' object is not callable
13230정성태1/26/20234030개발 환경 구성: 660. WSL 2 내부로부터 호스트 측의 네트워크로 UDP 데이터가 1개의 패킷으로만 제한되는 문제
13229정성태1/25/20234966.NET Framework: 2090. C# - UDP Datagram의 최대 크기
13228정성태1/24/20235112.NET Framework: 2089. C# - WMI 논리 디스크가 속한 물리 디스크의 정보를 얻는 방법 [2]파일 다운로드1
13227정성태1/23/20234820개발 환경 구성: 659. Windows - IP MTU 값을 바꿀 수 있을까요? [1]
13226정성태1/23/20234491.NET Framework: 2088. .NET 5부터 지원하는 GetRawSocketOption 사용 시 주의할 점
13225정성태1/21/20233746개발 환경 구성: 658. Windows에서 실행 중인 소켓 서버를 다른 PC 또는 WSL에서 접속할 수 없는 경우
13224정성태1/21/20234090Windows: 221. Windows - Private/Public/Domain이 아닌 네트워크 어댑터 단위로 방화벽을 on/off하는 방법
13223정성태1/20/20234281오류 유형: 838. RDP 연결 오류 - The two computers couldn't connect in the amount of time allotted
13222정성태1/20/20233932개발 환경 구성: 657. WSL - DockerDesktop.vhdx 파일 위치를 옮기는 방법
1  2  3  4  5  6  7  8  9  10  11  12  13  14  [15]  ...