mono: ECMA CLI 런타임

mono 런타임은 exe 파일이나 dll 파일을 해석하여 그것의 CIL(MSIL) 코드를 CPU의 네이티브 명령으로 옮겨서 실행하는 실행 엔진이다. 그 내용은 CIL(MSIL) 메타 데이터 로더, JIT 컴파일러 등의 실행 엔진, 메모리 관리(가베지 컬렉션), AppDomain 이나 리모, 스레드 관리,  I/O처리,  P/Invoke 구현, 디버거 지원 등 다방면에 걸친다.

 

어셈블리의 로드와 실행

mono CIL 코드를 실행하는 방식으로서 가장 간단한 것은 exe 형식의 CIL 코드 실행이다. executable(실행 형식)로서의 mono(Windows라면 mono.exe) exe 형식의 MSIL 코드를 실행하기 위한 콘솔 도구이다. 예를 들면 다음 명령은 실제로 executable mono를 콘솔에서 실행하고 있다.

 

<콘솔>

$ mono MyProgram.exe   // MyProgram.exe를 실행한다

 

.NET exe 파일은 Windows 상에서는 네이티브 실행 파일 형식(Coff)에 들어가 있지만 Windows 이외의 OS에서는 그렇지 않다. CIL은 어떻게 발버둥 쳐도 Linux의 표준 실행 파일 형식인 ELF 포맷에는 적합하지 않다. ECMA CLI는 이 점에서는 분명히 Windows에 유리한 사양을 책정하고 있다. "mono에서 인수에 실행 파일을 지정하여 실행할 수 밖에 없는 것은 불편하다"라는 것은 착각이다(Linux에서는 "binfmt_misc" 라는 기능을 사용해 .NET 형식의 바이너리를 mono의 호출로 연결할 수 있다. .exe 형식은 Windows의 기본 실행 파일로 Wine에서 실행하지 않으면 쓸 수 없는 가능성도 있으으므로 Linux 상에서 확장자나 MIME 타입으로 연관시키는 것은 추천할 수 없다).

 

.NET 애플리케이션은 exe 형식의 엔트리 포인트를 가질 수는 없다. 예를 들면 ASP.NET Web 어플리케이션은 dll 형식으로 생성되며, ASP.NET의 실행 엔진(IIS이라면 ISAPI)가 독자적으로 CLR 혹은 mono의 호스팅 API를 사용하여 독립된 응용 프로그램 도메인을 만들어서 거기에 dll을 로드하고, 소정의 엔트리 포인트에서 애플리케이션을 실행한다. Silverlight 이라면 CoreCLR로 불리는 호스트가 xap 애플리케이션 패키지에서 공약에 따라 dll을 찾아내 그것을 로드하고 있다.

 

같은 일이 mono에 대해서도 들어맞는다. mono의 실행 엔진의 몸체는 "libmono"라고 하는 라이브러리로 독립하고 있으며 이를 사용하면 독자적인 호스트를 작성할 수 있다. libmono API "mono의 임베디드 API" 또는 "embedded mono"로 불린다. 전형적인 이용 사례는 브라우저 플러그 인이다. Unity는 가장 유명한 embedded mono의 사용자라고 말할 수 있지만, 그들은 "Web Player"라는 런타임 플러그 인을 제공하고 있다. Mono팀에서도 과거 "Moonlight" 라는 Silverlight 호환 환경을 공개했었는데 이것도 브라우저 플러그 인 API의 표준적 존재로 NPAPI을 경유하여 mono 런타임을 실행하는 플러그 인이다. 구글은 "NaCL(native client)"라는 브라우저용 네이티브 코드 실행 환경에서 mono를 동작시키기 위한 패치를 mono에 컨트리뷰트 했다. mono를 움직이는 NaCL에서는 Unity를 움직일 수 있다.

 

mono 런타임은 어셈블리의 CIL 메타 데이터를 로드하고, 환경 변수나 구성 파일과 대조, 적절한 mscorlib.dll 파일을 로드하여 .NET 런타임의 프로파일을 선택한다. 로드 되는 mscorlib.dll 버전에 따라 그 런타임의 프로파일(.NET 2.0/.NET 4.0/.NET 4.5/iOS/Android )이 결정된다. .NET 2.0 프로파일은 공식은 이미 지원하지 않으므로 언제 없어져도 이상하지 않는 프로파일이므로 .NET 4.x로 이행하는 것이 좋다.

 

 

네이티브 실행 가능한 코드 생성

프로파일이 정해지면 AppDomain이 생성되고 거기에 어셈블리가 로드되어 실행 엔진에 의해 CPU 명령으로 변환된 CIL의 코드가 실행된다. 현재 실행 엔진은 2종류가 있다. "mini"로 불리는 JIT 컴파일러(실행 시 컴파일) AOT 컴파일러(사전 컴파일)이다(이하 단순히 JIT AOT로 표기). 이전에는 이 밖에 "mint" 라는 인터프리터도 있었지만 이미 역할을 끝내고 조만간 소멸할 예정이다. AOT.NET에서 NGen가 비슷한 일을 하고 있는데, 애플리케이션의 실행을 수반하지 않고 dll을 타겟 CPU의 명령으로 컴파일하여 네이티브 공유 라이브러리를 생성하는 기능이다.

 

mono 런타임에서는 직접 CPU 명령을 생성하는 mono 자신의 코드 생성 엔진에 더해, LLVM에 의한 코드 생성 엔진도 지원하고 있다. LLVM이 더 빠른 코드를 생성하는 부분은 적지 않다.  한편 LLVM을 로드 하기 위해 필요한 메모리가 증가하므로 LLVM은 만인 대상은 아니다.

* LLVM 코드생성: http://www.mono-project.com/Mono_LLVM

 

mono AOT는 실제로는 " AOT" "통상 AOT" 2 종류가 있다. AOT는 모든 CIL 코드를 네이티브 명령으로 변환 할 수 있는 것은 아니다. 실행 시 CPU 명령을 JIT에서 생성하는 것이 불가결한 기능이 있다. 예를 들면 System.Reflection.Emit 이름 공간의 API를 사용하여 동적으로 코드를 생성하고 실행하는 것은 AOT에서는 불가능하다. 통상의 AOT모드의 경우 AOT 변환되지 않는 코드를 만나면 코드는 JIT에 폴 백하여 실행된다. AOT의 경우는 에러가 되면 끝이다.

 

동적으로 생성된 코드가 동작하지 않는 환경이 있다. 대표적으로 Jailbreak 하지 않은 iOS 환경이다. iOS는 코드를 실행하는 플랫폼 차원에서 동적으로 코드를 생성하기 위해 필요한 코드를 무효화한다. AOT는 이러한 플랫폼에서 코드를 실행할 때 사용한다. Xamarin.iOS는 빌드 툴 체인에서 자동으로 이 AOT를 사용하여 애플리케이션 코드를 빌드한다(시뮬레이션용 빌드를 제외).

 

 

가베지 컬렉션(GC)

mono 런타임은 오랫동안 고전적 GC의 대표적인 존재인 "Boehm GC"에 손을 더한 것을 사용했지만 현재는 세대별 가베지 컬렉션을 구현하고 있다. 이 구현은 "SGen"이라고 불린다(원래는 "Simple Generational GC" 이었는데 지금은 분명히 simple이 아니다). mono 3.2 이후는 기본적으로 SGen이 유효하게 되어 있다. 현재의 Xamarin.iOS는 기본은 SGen이 아니며 선택 가능하다. Xamarin.Android Java GC와 협조 동작인 관계로 당초부터 SGen에서만 실행 가능했다.

 

 

mscorlib의 내부 호출 기능(및 파일 I/O의 특기 사항)

mono 런타임의 또 하나 중요한 기능은 mscorlib.dll에서 MethodImpl 속성에 "MethodImplOptions.InternalCall"를 지정하여 선언하고 있는 extern 메서드의 실체인 InternalCall(mono의 소스 코드에서는 "icall"으로 불린다). mono mscorlib 소스 코드(이하 "소스"로 표기)에는 C#에서는 구현할 수 없기 때문에 C에서 런타임 기능을 호출 하는 부분이 불가피하게 존재한다. 형 시스템에 접속 또는 쓰레드의 조작, I/O 조작 등이 있으므로 이들은 InternalCall을 통해 mono 런타임에서 네이티브로 실행된다.

 

여기서 특별히 언급해 둘 것이 I/O 주변이다. 이 부분은 Windows 이외의 환경에서는 "io-layer"로 불리는 Windows I/O API의 에뮬레이션 구현이 담당하고 있으며 직접 파일 I/O가 이뤄지고 있는 것은 아니다. 일반적으로는 Windows Linux에서는 파일 처리에 큰 차이가 있다. 가장 중요한 것은 Linux에서는 대문자와 소문자를 구분하는 것이다. Windows 상에서는 대문자와 소문자가 구분되지 않으므로 예를 들면 다음과 같은 코드는 문제없이 동작한다.

 

C#

File.WriteAllText ("FILE.txt", "test string");

return File.ReadAllText ("file.txt");

 

이를 Linux 환경에 가져가면 거의 실패하다. Linux 상에서 사용되는 파일 시스템은 "FILE.txt" "file.txt"을 별개로 다루기 때문이다. 이것은 프로그래머가 스스로 주의해야 할 일이지만 .NET 애플리케이션은 대부분 Windows 상에서 개발되고 있기 때문에 Windows에서 움직이면 OK라는 전제 아래 적혀 있는 것이 많다. 이러한 프로그램에도 어느 정도 움직이도록 mono에는 "MONO_IOMAP" 이란 환경 변수에서 Windows I/O 같은 거동을 지정할 수 있는 기능이 있다. "MONO_IOMAP=all"로 지정하고 mono 상에서 애플리케이션을 실행하면 위의 코드도 생각대로에 동작한다(참고로 이를 사용하면 파일 이름의 문제가 모두 해결하려는 게 아니다. 어디까지나 파일 I/O API가 미칠 수 있는 범위에서만이다. 파일 이름의 대 소문자는 항상 일치시켜 두는 것이 바람직하다).

 

참고로 Mac OS X에서 사용하는 HFS+ 파일 시스템에서는(기본 포맷에서는) 대 소문자를 구분하지 않는다. 한편 이는 mono에 한정 하지 않는 이야기지만 HFS+는 파일명을 취급할 때 Unicode NFD 정규화와 유사한 독자적인 변환을 실시하기 때문에 Mac OS X와 그 이외의 환경에서 크로스 개발하고 있는 경우에는 조심하는 것이 좋다(NTFS ext4 등의 파일 시스템은 특히 이러한 변환은 않는다). iOS Mac OS X Android Linux을 바탕으로 구축된 OS 라는 것을 기억해 두면 덫에 걸리지 않는다.

 

 

클래스 라이브러리

mono에 어느 클래스 라이브러리의 어셈블리가 있을지는 GitHub 저장소를 보는 것이 가장 빠르지만 각각 설명하는 것만으로도 너무 많으므로 개요만 정리한다. 기본적으로 Xamarin이 지원하고 있는 분야는 상용 지원으로서 비교적 두터운 개선을 받고 있다. 반대로 현재의 Xamarin이 모바일 플랫폼에서 사용하지 않는 클래스 라이브러리는 거의 관리되지 않는 상태이다.

 

.NET 3.0에서 추가된 API는 대부분 존재하지 않는다. WPF(Windows Presentation Foundation) WF(Windows Workflow Foundation)이 없고 일부 코드가 "olive"라는 다른 저장소에 형태만 존재하고 있다. WCF도 모바일에서 사용 되는 부분 이외는 거의 미완성으로 그치고 있다. 모바일 프로파일의 WCF Silverlight 2.0과 거의 같다.

 

Windows 고유 API를 전제로 하는 라이브러리(System.Management.dll, System.Core.dll System.IO.Pipes 이름 공간 등)도 구현은 없다. COM Windows의 고유 기능이라서 mono에서 동작하는 것은 기대할 수 없다(사실 Windows 한정으로 mono 런타임에 COM 기능을 구현하고 있는 커뮤니티 해커는 존재했지만 아무튼 의존하는 라이브러리 측이 Windows 전용이므로 mono로 지원하는 의의는 거의 없다).

 

.NET 클래스 라이브러리는 너무 방대하여 이것을 구현하는 Mono 클래스 라이브러리에는 미 구현이 많다. 구체적으로는 틀이나 멤버가 존재하지 않는 것과 존재하고 있지만 호출해 보면 NotImplementedException 예외를 던진다(원래 어떤 형태가 얼마나 구현되고 있는가 하는 정보는 Mono "class status" web 페이지에서 확인할 수 있도록 되어 있는데 본고 집필 시점에서 갱신이 멈춘 상태다).

 

mono의 클래스 라이브러리는 mono의 소스 트리 상 mcs/class 디렉토리 아래에 대량으로 작성되고 있다(역사적으로는 C#로 쓰여진 코드는 모두 "mcs"라는 모듈에 포함되었다. GitHub에 이행했을 때 mono의 소스 트리에 통합되었다). 디렉터리 구성 규칙은 기본적으로 다음과 같다.

 

mcs/class/[Assembly]/[Namespaces]/[Type]. cs

 

, 클래스 라이브러리의 테스트에는 NUnit(2013년 시점에서는 다소 오래된 2.4.8)이 이용되고 있으며, 각 라이브러리의 테스트의 소스 디렉터리 구성은 대체로 다음과 같다.

 

mcs/class/[Assembly]/Test/[Namespaces]/[Type]Test.cs

 

디렉토리 구조의 주요 예외로서 mscorlib 어셈블리(mscorlib.dll 파일) "corlib", System.Windows.Forms 어셈블리(System.Windows.Forms.dll 파일) "Managed.Windows.Forms" 라는 디렉토리 이름이 되어 있다(외에도 몇 가지 예외는 있다). 클래스 라이브러리는 물론 모두 C#으로 적혀 있다. 다만 빌드에 IDE는 사용하지 않고, make 명령이 사용되고 있다. 소스에 대해서는 aspnetwebstack, entityframeworkrx, cecil 등 외부 프로젝트를 git submodule 로 주입한 것도 있지만 어셈블리의 디렉토리 구성은 변하지 않는다(소스의 목록은 서브 모듈 속을 참조하고 있다).

 

소스 코딩 규칙은 mono적이다. Linux kernel 문화에 친화적이고 불필요하게 줄 수를 늘리지 않는 설계가 되어 있다. 이 코딩 규칙이 마음에 들지 않지만 소스를 읽을 필요가 있는 경우는 monodevelop의 코딩 스타일 설정을 Visual Studio로 한 후에 자동 포맷을 설정하면 낫다.

 

만약 클래스 라이브러리의 미 구현 부분을 구현하거나, 버그 등을 발견하고 수정을 시도 하고 싶은 경우는 한번 mono의 트리 전체를 빌드 한 뒤 그 어셈블리의 디렉토리로 이동하여 make run-test를 실행하면, NUnit 테스트가 실행되므로 편리하다. 참고로 빌드된 dll 파일 등은 mcs/class/lib/net_4_5 와 같은 디렉토리에 생성된다.

 

mono의 클래스 라이브러리에는 사실 복수의 프로파일이 있다. .NET 2.0, .NET 4.0, .NET 4.5로 각각 .NET의 프로파일에 상당한다. 클래스 라이브러리 디렉터리에서 make를 실행했을 때 빌드되는 것은 최신의 프로파일 뿐이다(현재는 .NET 4.5). 낡은 프로파일의 어셈블리를 빌드 하려면 make PROFILE=net_2_0 라는 빌드를 실행할 필요가 있다. 사실 더해서 모바일 프로파일도 있지만 그것에 대해서는 다음에 이야기 하겠다.

 

 

 

 

출처: http://www.buildinsider.net/mobile/insidexamarin/03

 

저작자 표시
신고
by 흥배 2014.02.26 08:00