.NET에서는 System.CodeDom 네임스페이스를 사용자에게 제공함으로써 동적 컴파일이 가능하게 합니다.

그리고 System.CodeDom.Compiler 네임스페이스에서는 C#, VB 언어 등의 컴파일을 가능하게 하는 컴파일러를 제공하기도 합니다. (대표적인 예로 CodeDomProvider 클래스가 있습니다)


동적 컴파일러라고 해서 사용하기가 어려운 것도 아닙니다.

컴파일하고자 하는 소스 코드 혹은 파일을 매개 변수로 전달하면 끝!


이렇게 간단하게 컴파일러를 만들 수 있으니, 사용자 정의 컴파일러나 플러그인 인프라를 만들 수 있겠죠.


일단! CodeDom 및 CodeDom.Compiler 네임스페이스에 대해서 보도록 하죠!

System.CodeDom.Compiler 네임스페이스 (MSDN)


동적으로 컴파일을 하기 위해서는 딱 두가지만 필요합니다.

CodeDomProvider 클래스와 CompilerParameters 클래스 이 두개만 알고 있으면 컴파일 OK!

(* 선택적으로 CompilerResults 클래스도 알고있으면 좋습니다)


CodeDomProvider 클래스 (MSDN)

CodeDomProvider 클래스는 추상 클래스이기 때문에 직접 사용할 수가 없습니다.

그래서 MS에서는 친절하게도 Microsoft 네임스페이스에 C#, VB 및 자바스크립트 코드를 컴파일하기 위한 CodeDomProvider 클래스를 제공해주고 있죠.


이번 게시글에선 C# 코드를 컴파일해보겠습니다. 그러니 CSharpCodeProvider 클래스를 사용해야겠죠

컴파일러 초기화는 어렵지 않습니다.

CodeDomProvider cs = new CSharpCodeProvider();
// 또는
CSharpCodeProvider cs = new CSharpCodeProvider();


둘 중 하나 마음에 드시는 것을 선택하시고 선언하시면 됩니다. 어차피 CSharpCodeProvider 클래스는 CodeDomProvider 클래스를 상속하기 때문에 CodeDomProvider 를 사용해도, CSharpCodeProvider 를 사용해도 무관합니다.


그 다음엔 CompilerParameters 클래스를 사용해야 합니다.

컴파일 옵션 및 참조를 관리하는 클래스로써 컴파일을 하기 위해선 필수 중의 필수입니다!

그러니 반드시 잘 숙지하셔야합니다.


CompilerParameters 클래스 (MSDN)


CompilerParameters 클래스의 속성들에 대한 설명입니다.

/*
 *  컴파일러 옵션 문자열
 *    CompilerParameters.CompilerOptions
 * 
 *  예제
 *    CompilerParameters.CompilerOptions = "/unsafe /define:aaa";
 */

/*
 *  컴파일된 어셈블리를 실행 파일 또는 라이브러리(DLL)로 만들 것인지를 결정
 *    CompilerParameters.GenerateExecutable
 * 
 *  예제 (파일 생성함)
 *    CompilerParameters.GenerateExecutable = true;
 */

/*
 *  컴파일된 어셈블리를 메모리에 위치시킬 것인지 결정
 *    CompilerParameters.GenerateInMemory
 * 
 *  예제 (메모리에 생성하지 않음)
 *    CompilerParameters.GenerateInMemory = false;
 */

/*
 *  디버그 정보를 포함할 것인지 결정 (주로 DEBUG 빌드에서 사용됨. RELEASE 빌드시에는 false를 전달하면됨)
 *    CompilerParameters.IncludeDebugInformation
 * 
 *  예제 (포함하지 않음)
 *    CompilerParameters.IncludeDebugInformation = false;
 */

/*
 *  디버그 정보를 포함할 것인지 결정 (주로 DEBUG 빌드에서 사용됨. RELEASE 빌드시에는 false를 전달하면됨)
 *    CompilerParameters.IncludeDebugInformation
 * 
 *  예제 (포함하지 않음)
 *    CompilerParameters.IncludeDebugInformation = false;
 */

/*
 *  어셈블리에 포함할 리소스를 설정
 *    CompilerParameters.EmbeddedResources
 * 
 *  예제 (icons.resx 리소스 파일을 포함)
 *    CompilerParameters.EmbeddedResources.Add("icons.resx");
 */

/*
 *  어셈블리에 참조되는 리소스를 설정
 *    CompilerParameters.LinkedResources
 * 
 *  예제 (icons.resx 리소스 파일이 참조됨)
 *    CompilerParameters.LinkedResources.Add("icons.resx");
 */

/*
 *  어셈블리의 메인 클래스 이름
 *    CompilerParameters.MainClass
 * 
 *  예제
 *    CompilerParameters.MainClass = "DynamicCompiler";
 */

/*
 *  컴파일된 어셈블리가 저장될 위치를 설정
 *  (GenerateExecutable 값이 true일 때에만 유효함)
 *    CompilerParameters.OutputAssembly
 * 
 *  예제
 *    CompilerParameters.OutputAssembly = @"C:\compiled.assembly.exe";
 */

/*
 *  어셈블리에 참조되는 어셈블리를 설정
 *    CompilerParameters.ReferencedAssemblies
 * 
 *  예제 (DirectX 라이브러리 참조됨)
 *    CompilerParameters.ReferencedAssemblies.Add("Microsoft.DirectX.dll");
 */

/*
 *  임시 파일의 목록
 *  (어셈블리를 컴파일하면서 만들어진 임시파일 목록)
 *    CompilerParameters.TempFiles
 * 
 *  예제
 *    -
 */

/*
 *  경고를 오류로 간주할 것인지를 설정
 *    CompilerParameters.TreatWarningsAsErrors
 * 
 *  예제 (경고를 오류로 간주함)
 *    CompilerParameters.TreatWarningsAsErrors = true;
 */

/*
 *  경고 레벨 설정
 *    CompilerParameters.WarningLevel
 * 
 *  예제 (0 = 경고 표시 안함 / 4 = 모든 경고 표시)
 *    CompilerParameters.WarningLevel = 4
 */

/*
 *  컴파일러 프로세스의 사용자 토큰 설정
 *    CompilerParameters.UserToken
 * 
 *  예제
 *    -
 */

/*
 *  어셈블리에 포함될 Win32 리소스 설정
 *    CompilerParameters.Win32Resource
 * 
 *  예제
 *    -
 */


CodeDomProvider 클래스에서 중요하게 살펴봐야할 것은 CompileAssemblyFromFile, CompileAssemblyFromCode 이 두가지 메서드가 끝입니다.둘 다 컴파일할 때 사용되고 하나는 파일 경로를, 다른 하나는 소스 문자열을 받는다는 것 외에는 차이가 없습니다.


그리고 CompilerResults 클래스를 살펴보도록 하죠.


CompilerResults 클래스 (MSDN)


(넘어가실 분들은 넘어가셔도 좋지만, 컴파일 경고, 오류 등을 표시하기 위해선 필수!)

/*
 *  컴파일된 어셈블리
 *  (성공적으로 컴파일됬을 경우에만 할당됩니다. 컴파일 오류가 발생한 경우 NullReferenceException 예외가 발생할 수 있습니다)
 *    CompilerResults.CompiledAssembly
 * 
 *  예제
 *    -
 */

/*
 *  컴파일 오류 목록
 *    CompilerResults.Errors
 * 
 *  예제
 *    -
 */

/*
 *  컴파일 출력 메세지 목록
 *    CompilerResults.Output
 * 
 *  예제
 *    -
 */

/*
 *  컴파일된 어셈블리가 위치한 경로
 *  (성공적으로 컴파일됬을 경우에만 할당됩니다. 컴파일 오류가 발생한 경우 NullReferenceException 예외가 발생할 수 있습니다)
 *    CompilerResults.PathToAssembly
 * 
 *  예제
 *    -
 */


자 이제 필수 요소들을 살펴봤으니 실질적인 컴파일을 도전해보도록 하겠습니다.


컴파일러 폼은 이렇게 디자인하였습니다.


그리고, Compile 버튼을 눌렀을 때 발생하는 이벤트는 이렇게 코딩했습니다.

void Button1Click(object sender, EventArgs e) {
    CodeDomProvider cs = new CSharpCodeProvider();
    CompilerParameters cp = new CompilerParameters();
    CompilerResults cr;
    
    cp.GenerateInMemory = true;
    cp.ReferencedAssemblies.Add("System.dll");
    cr = cs.CompileAssemblyFromSource(cp, textBox1.Text);
    
    List<String> msgs = new List<string>();
    Boolean hasError = false;
    foreach ( CompilerError ce in cr.Errors ) {
        String msg;
        if ( ce.IsWarning )
            msg = String.Format("[WARN] {0} in {1} at line {2}:{3}", ce.ErrorText, ce.FileName, ce.Line, ce.Column);
        else {
            msg = String.Format("[ERROR] {0} in {1} at line {2}:{3}", ce.ErrorText, ce.FileName, ce.Line, ce.Column);
            hasError = true;
        }
        msgs.Add(msg);
    }
    
    if ( hasError ) {
        textBox1.Text = "컴파일 실패";
        foreach (String m in msgs) {
            textBox1.AppendText("\r\n" + m);
        }
    } else {
        MessageBox.Show("컴파일 성공");
    }
}


복잡해보이지만 한번 자세히 보도록 하겠습니다.

우선 컴파일을 위해서 CodeDomProvider, CompilerParameters 클래스를 초기화하고, 컴파일 결과값을 받기 위한 CompilerResults 클래스를 '선언'만 했습니다.


그리고 메모리에 어셈블리를 생성하고 System.dll 어셈블리를 참조에 추가하겠다고 설정한 후에 CompileAssemblyFromSource 메서드를 사용해 소스 코드를 컴파일했습니다.


컴파일 결과가 저장된 CompilerResults 클래스의 Errors 속성에 접근해 오류 또는 경고가 있는지 확인하고, 오류는 오류대로 경고는 경고대로 접두어를 다르게 해서 메세지 목록에 추가하였고~


오류가 있을 경우엔 컴파일 실패 라는 텍스트와 오류, 경고 메세지를 함께 보여주고 컴파일이 성공한 경우엔 컴파일 성공 이라는 메세지를 띄우게 했습니다.



테스트 #1.

제대로 된 소스 코드를 컴파일 해봤습니다.

잘 되는군요!

그럼 제대로되지 않은 코드는 어떨까요?


테스트 #2.

정의되지 않은 형식 사용 + /unsafe 옵션을 주지 않은 상태에서 unsafe 코드 사용

컴파일 실패라는 메세지와 함께 경고, 오류 목록이 나오는 것을 보실 수 있습니다.


이 방법을 응용해서 사용자가 입력한 코드에 맘에 안드는 부분을 String.Replace 메서드로 다른 것으로 교체시켜 버리고 자신만의 플러그인 인프라를 구축할 수도 있겠죠. (또는 스크립트)


예제 프로젝트 파일:

myowncompiler.zip



+ Recent posts