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)
13481정성태12/13/20232279개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20232647개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/20232332개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/20232523닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/20232257닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
13475정성태12/7/20232319닷넷: 2180. .NET 8 - 함수 포인터에 대한 Reflection 정보 조회파일 다운로드1
13474정성태12/6/20232169개발 환경 구성: 690. 닷넷 코어/5+ 버전의 ilasm/ildasm 실행 파일 구하는 방법 - 두 번째 이야기
13473정성태12/5/20232374닷넷: 2179. C# - 값 형식(Blittable)을 메모리 복사를 이용해 바이트 배열로 직렬화/역직렬화파일 다운로드1
13472정성태12/4/20232189C/C++: 164. Visual C++ - InterlockedCompareExchange128 사용 방법
13471정성태12/4/20232266Copilot - To enable GitHub Copilot, authorize this extension using GitHub's device flow
13470정성태12/2/20232559닷넷: 2178. C# - .NET 8부터 COM Interop에 대한 자동 소스 코드 생성 도입파일 다운로드1
13469정성태12/1/20232277닷넷: 2177. C# - (Interop DLL 없이) CoClass를 이용한 COM 개체 생성 방법파일 다운로드1
13468정성태12/1/20232219닷넷: 2176. C# - .NET Core/5+부터 달라진 RCW(Runtime Callable Wrapper) 대응 방식파일 다운로드1
13467정성태11/30/20232303오류 유형: 882. C# - Unhandled exception. System.Runtime.InteropServices.COMException (0x800080A5)파일 다운로드1
13466정성태11/29/20232486닷넷: 2175. C# - DllImport 메서드의 AOT 지원을 위한 LibraryImport 옵션
13465정성태11/28/20232239개발 환경 구성: 689. MSBuild - CopyToOutputDirectory가 "dotnet publish" 시에는 적용되지 않는 문제파일 다운로드1
13464정성태11/28/20232371닷넷: 2174. C# - .NET 7부터 UnmanagedCallersOnly 함수 export 기능을 AOT 빌드에 통합파일 다운로드1
13463정성태11/27/20232301오류 유형: 881. Visual Studio - NU1605: Warning As Error: Detected package downgrade
13462정성태11/27/20232340오류 유형: 880. Visual Studio - error CS0246: The type or namespace name '...' could not be found
13461정성태11/26/20232372닷넷: 2173. .NET Core 3/5+ 기반의 COM Server를 registry 등록 없이 사용하는 방법파일 다운로드1
13460정성태11/26/20232324닷넷: 2172. .NET 6+ 기반의 COM Server 내에 Type Library를 내장하는 방법파일 다운로드1
13459정성태11/26/20232295닷넷: 2171. .NET Core 3/5+ 기반의 COM Server를 기존의 regasm처럼 등록하는 방법파일 다운로드1
13458정성태11/26/20232305닷넷: 2170. .NET Core/5+ 기반의 COM Server를 tlb 파일을 생성하는 방법(tlbexp)
13457정성태11/25/20232252VS.NET IDE: 187. Visual Studio - 16.9 버전부터 추가된 "Display inline type hints" 옵션
13456정성태11/25/20232550닷넷: 2169. C# - OpenAI를 사용해 PDF 데이터를 대상으로 OpenAI 챗봇 작성 [1]파일 다운로드1
13455정성태11/25/20232452닷넷: 2168. C# - Azure.AI.OpenAI 패키지로 OpenAI 사용파일 다운로드1
1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...