Roslyn - C# 컴파일러 API 에 대하여 (2)

[제목] Roslyn - C# 컴파일러 API 에 대하여 (2)

지난 아티클 (1) 에서는 Parser와 Syntax Tree를 통해 프로그램의 구조를 체크할 수 있는 Syntax API를 살펴보았다. 이번 아티클에서는 컴파일 과정과 관련된 API들에 대해 간략히 살펴보고자 한다.

C# 컴파일러의 Compile Pipeline을 간략히 표현하면 아래 그림과 같이 Parsing, Symbol 생성, Binding, MSIL 생성으로 요약할 수 있는데, Roslyn은 이 컴파일러의 각 Pipeline에 상응하는 API를 제공하고 있다.

그러면 먼저 컴파일의 전반 과정을 이해하기 위해 C# 컴파일러 API를 사용해서 C# 소스 코드를 컴파일하는 간단한 예를 들어 보자. 아래 코드는 간단한 Hello World 프로그램을 Compiler API를 통해 컴파일하여 EXE를 생성하는 예제이다.


// 소스코드에서 SyntaxTree 생성
SyntaxTree tree = CSharpSyntaxTree.ParseText(@"
using System;
namespace HelloWorld {
  class Program {
    static void Main(string[] args) {
        Console.WriteLine(""Hello, World!"");
    }
  }
}");

// Compilation 생성
var metaRef = new MetadataFileReference(typeof(object).Assembly.Location); // mscorlib
var compilation = CSharpCompilation.Create("Test").AddReferences(metaRef).AddSyntaxTrees(tree);

// EXE 생성
compilation.Emit(@"C:\Temp\Test.exe", @"C:\Temp\Test.pdb");

위의 과정을 보면, 처음 C# Parser를 통해 SyntaxTree를 생성한다. 이어 CSharpCompilation.Create() 메서드를 통해 CSharpCompilation 객체를 생성하고 여기에 필요한 Reference들과 파싱된 Syntax Tree들을 추가한다. CSharpCompilation 객체는 메모리상의 컴파일 중간 결과물로서 개발자는 이 객체를 통해 Semantic Analysis를 하거나 EXE/DLL 출력 즉 IL Emit을 할 수 있다. 위의 예제는 CSharpCompilation 객체로부터 직접 IL Emit을 하는 코드로서 EXE 와 더불어 PDB 파일(Optional)도 같이 생성하고 있다.

만약 소스코드가 문법적으로나 의미적으로 맞다면, 위의 코드는 실행가능한 EXE 파일을 만들어 낼 것이다. 하지만, 만약 문법에 오류가 있다면 어떻게 될까? 위의 코드에서 문법적 오류가 있거나 참조가 잘못되었더라도 의외로 EXE 파일은 만들어 진다. 하지만 이 EXE는 0 바이트로 실행이 불가능하다. 위의 코드에서는 파싱 에러 체크, Semantic 에러 체크, Reference 체크 등등의 모든 에러 체크가 빠졌기 때문에 만약 오류가 중간에 있다면 정상적인 EXE 를 만들지 못하게 된다.

컴파일러의 에러 체크는 Diagnostic API 들을 사용해 처리한다. 아래 예제에서 보는 바와 같이 CSharpCompilation 객체를 완성한 후, CSharpCompilation의 GetParseDiagnostics() 메서드를 사용해서 파싱 에러를 체크할 수 있다. 또한 CSharpCompilation의 GetDeclarationDiagnostics() 메서드를 사용해서 심벌 및 바인딩 에러를 체크하게 되고, 마지막으로 Emit() 메소드 호출 뒤에 리턴되는 EmitResult.Diagnostics 컬렉션으로부터 Emit 시에 발생하는 에러들을 체크할 수 있다. Diagnostics API를 사용하면 컴파일러 에러 번호 및 메시지, 소스라인 등의 정보를 얻을 수 있다.


// SyntaxTree 생성
SyntaxTree tree = CSharpSyntaxTree.ParseText(@"
using SystemA; // 2.심벌/바인딩 에러
namespace HelloWorld {
class Program {
     static void Main(string[] args) {  
         Console.WriteLin(""Hello, World!""); // 3.Emit 에러
      }
  //} // 1.파싱에러
}");
// Compilation 생성
var metaRef = new MetadataFileReference(typeof(object).Assembly.Location);
var compilation = CSharpCompilation.Create("Test").AddReferences(metaRef).AddSyntaxTrees(tree);

// 파싱 에러 체크
var parseDiags = compilation.GetParseDiagnostics();
if (parseDiags.Count() > 0)
{
    Console.WriteLine("Parse Errors");
    parseDiags.ToList().ForEach(p => Console.WriteLine(p.ToString()));
    return;
}

// 심벌 바인딩 에러 체크
var declDiags = compilation.GetDeclarationDiagnostics();
if (declDiags.Count() > 0)
{
    Console.WriteLine("Symbol Binding Errors");
    declDiags.ToList().ForEach(p => Console.WriteLine(p.ToString()));
    return;
}

// EXE 생성
EmitResult emitResult = compilation.Emit(@"C:\Temp\Test.exe", @"C:\Temp\Test.pdb");

// Emit 에러 체크
if (emitResult.Diagnostics.Count() > 0)
{
    Console.WriteLine("Emit Errors");
    emitResult.Diagnostics.ToList().ForEach(p => Console.WriteLine(p.ToString()));
}

위의 예제 소스코드는 3군데가 잘못되어 있다. 위 예제를 실행하면 먼저 파싱 에러를 출력하는데, 이는 class 의 Closing Brace ( } ) 가 코멘트로 막아져 있기 때문이다.


Parse Errors
(9,2): error CS1513: } expected

이 코멘트를 지우고 다시 실행해보면 using SymbolA 에서 심벌 바인딩 에러가 발생한다.


Symbol Binding Errors
(2,7): error CS0246: The type or namespace name 'SystemA' could not be found

그리고 심벌 오류를 바로 잡고 다시 실행 해보면, 아래와 같이 WriteLin() 메서드를 찾지 못하는 오류를 발생하게 된다.


Emit Errors
(6,18): error CS0117: 'System.Console' does not contain a definition for 'WriteLin'

메서드명을 WriteLine 으로 고치면 모든 에러가 Fix되어 컴파일이 성공적으로 이루어질 것이다.

다음 아티클에서는 Symbol 및 Semantic Analysis 에 관련된 API 에 대해 좀 더 자세히 알아볼 예정이다.

Roslyn - C# 컴파일러 API (1)
Roslyn - C# 컴파일러 API (2)
Roslyn - C# 컴파일러 API (3)



본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.