.NET에서 제공하는 병렬 컨테이너 중의 하나로 저장하는 요소의 순서가 없기 때문에 가장 성능이 좋다.

 

" Thread-safe Collections in .NET Framework 4 and Their Performance Characteristics" 에서 인용

 




PerformanceCharacteristicsOfThreadSafeCollection.pdf




신고
by 흥배 2014.06.26 08:00

.NET에서는 정적 라이브러리 라는 것이 없어서 외부 라이브러리를 사용하면 실행파일을 동작 시키기 위해서는 해당 라이브러리의 DLL이 필요해서 프로젝트를 빌드하면 실행파일과 사용한 라이브러리의 DLL에 같이 생성된다.

 

ILMerge 라는 툴을 사용하면 DLL을 실행파일과 묶어서 하나의 파일로 만들 수 있다.

 

ILMerge 다운로드: http://www.microsoft.com/en-us/download/details.aspx?id=17630

 

 

사용방법

ILMerge를 설치한 후 다음의 명령어로 exe DLL을 하나로 묶는다.

C:\Program Files\Microsoft\ILMerge\ILMerge /out:hoge_merge.exe hoge.exe piyo.dll foo.dll

/out: 에 출력 파일 이름을 지정한다. 다음 인수로는 묶고 싶은 exe, dll 파일 명을 지정한다(위에서는 hoge.exe piyo.dll foo.dll).

 

와일드카드로 파일 명 지정

인수로 넘기는 파일 명을 와일드카드(*)로 지정하고 싶다면 /wildcards를 지정한다.

C:\Program Files\Microsoft\ILMerge\ILMerge /wildcards /out:hoge_merge.exe hoge.exe *.dll

 

 

Visual C#에서 빌드 후 자동으로 파일 묶기

프로젝트 속성에서 '빌드 이벤트' '빌드 후 실행하는 커맨드 라인'에 다음 내용을 추가한다.

set fname_new=$(TargetName)_merge$(TargetExt)

C:\Program Files\Microsoft\ILMerge\ILMerge /wildcards /out:%fname_new% $(TargetFileName) *.dll

 

$(TargetName) … 애플리케이션 어셈블리 명

$(TargetExe) … 확장자(.exe

$(TargetFileName) … 출력 되는 실행 파일 명

 

아래와 같이 새로 만든 파일(실행 파일과 DLL이 합친)과 원래 파일 명이 같으면 디버그가 제대로 동작하지 않는다.

set fname_bk=$(TargetName)_bk$(TargetExt)

move $(TargetFileName) %fname_bk%

C:\Program Files\Microsoft\ILMerge\ILMerge /wildcards /out:$(TargetFileName) %fname_bk% *.dll

 

 

GUI 툴도 있다. http://sourceforge.net/projects/gilma/files/

 

 

참고: http://qiita.com/krsak/items/75a257cc0866a7e8e4aa

 

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

log4net 이란

아파치의 오픈소스 로그 라이브러리로 Java, C++, .NET 등 다양한 언어와 플랫폼을 지원하는 로그 라이브러리의 .NET 버전이다. 로그를 파일, 콘솔화면, DB로 출력할 수 있고 또 날짜, 파일 크기마다 로그를 새로 생성하도록 할 수 있다.

 

 

 

.NET에서 사용하기

.NET의 경우 NuGet을 사용하여 설치한다.

 

NuGet을 사용하여 log4net 라이브러리를 설치하면 아래 그림처럼 자동으로 프로젝트에 설정된다.

 

위 상태에서 빌드를 한다. 실행 파일이 있는 위치에 log4net 관련 dll와 xml 파일이 있다.

 

NuGet으로 설치한 경우 log4net 관련 설정 정보는 app.config 에 기록한다.

 

로그가 남도록 하면 아래와 같이 로그 파일이 생겨진다.

 

프로그램 실행 도중 로그 설정 파일을 변경하여 적용하기 위해서는 아래 처럼 AssemblyInfo.cs에 [assembly: log4net.Config.XmlConfigurator(Watch = true)] 을 추가한다.

 

 

로그 설정 정보가 있는 app.confog의 예

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

<startup>

<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />

</startup>

 

<log4net>

<!-- 파일을 사용하는 Appender -->

<appender name="LogToFile" type="log4net.Appender.FileAppender" >

<!-- 만들 로그 파일 -->

<param name="File" value="log-file.txt" />

<!-- 기존에 로그 파일이 있다면 연결해서 사용 여부 -->

<param name="AppendToFile" value="true" />

<!-- 로그 형식 -->

<layout type="log4net.Layout.PatternLayout">

<param name="ConversionPattern" value="%date [%thread] %-5level %logger - %message%newline" />

</layout>

</appender>

 

 

<!-- 날짜 단위로 Rolling 하는 파일 Appender -->

<appender name="DayRollingLogToFile" type="log4net.Appender.RollingFileAppender">

<!-- 로그 파일 이름의 선두 부분 -->

<File value="LogSample" />

<!-- 기존에 로그 파일이 있다면 연결해서 사용 여부 -->

<appendToFile value="true" />

<!-- 날짜 마다 파일을 작성하는 것을 지정 -->

<rollingStyle value="date" />

<!-- 로그 파일 이름 고정 여부. 고정하지 않을 때는 false -->

<staticLogFileName value="false" />

<!-- 파일 이름에의 날짜 부분 -->

<datePattern value='"."yyyyMMdd".log"' />

 

          

<layout type="log4net.Layout.PatternLayout">

<!-- 로그 형식 -->

<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />

</layout>

</appender>

 

 

<!-- 파일 크기 단위로 Rolling 하는 파일 Appender -->

<appender name="SizeRollingLogToFile" type="log4net.Appender.RollingFileAppender">

<file value="mylogfile.txt" />

<appendToFile value="true" />

<rollingStyle value="Size" />

<maxSizeRollBackups value="5" />

<maximumFileSize value="10MB" />

<staticLogFileName value="true" />

<layout type="log4net.Layout.PatternLayout">

<conversionPattern value="%date [%thread] %level %logger - %message%newline" />

</layout>

</appender>

 

      

<!-- 콘솔을 사용하는 Appender -->

<appender name="LogToConsole" type="log4net.Appender.ConsoleAppender">

<layout type="log4net.Layout.PatternLayout">

<conversionPattern value="%date{ABSOLUTE} [%thread] %level %logger - %message%newline"/>

</layout>

</appender>

 

 

<root>

<!-- 로그 레벨 지정. 아래에서는 INFO 레벨 이상만 로그가 남는다 -->

<level value="INFO" />

<!-- 아래 로그 레벨 중 하나 선택

<level value="OFF " />

<level value="FATAL" />

<level value="ERROR" />

<level value="WARN" />

<level value="INFO" />

<level value="DEBUG" />

<level value="ALL" />

-->

 

<!-- 어느 로그를 사용할 것인지 지정한다. 현재 파일로그와 날짜별 로그 사용 -->

<appender-ref ref="LogToFile" />

<appender-ref ref="DayRollingLogToFile" />

<!-- 복수 지정 가능 -->

</root>

</log4net>

 

</configuration>

   

 

예제 소스 코드

public partial class Form1 : Form

{

private static readonly log4net.ILog log =

 

log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

 

 

public Form1()

{

InitializeComponent();

}

 

private void button1_Click(object sender, EventArgs e)

{

log.Info("INFO 출력");

log.Debug("Debug 출력");

log.Error("Error 출력");

}

}

       

 

로그 정보에 로그를 남기는 코드 부분의 파일명이나 위치를 남기고 싶은 경우

로그 형식에 %location 을 사용한다.

<param name="ConversionPattern" value="%date [%thread] %-5level %logger - %location %message%newline" />

   

로그 파일을 특정 폴더에 남기고 싶은경우

현재 실행 파일이 있는 폴더의 Logs 폴더에 남기고 싶은 경우

<log4net>

<!--날짜 단위로 Rolling 하는 파일 Appender-->

<appender name="DayRollingLogToFile" type="log4net.Appender.RollingFileAppender">

<!--로그 파일 이름의 선두 부분-->

<File value="Logs\GameLog" />

   

 

로그 설정 정보를 app.config 파일 이외의 파일에 남기고 싶을 때

'AssemblyInfo.cs' 파일에 아래의 코드를 입력한다

    

[assembly: log4net.Config.DOMConfigurator(ConfigFile = "../../log4net.xml", Watch = true)]

또는

 

// 로그 설정 파일 로딩 후 적용

string logFilePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log4net_config.xml");

var finfo = new System.IO.FileInfo(logFilePath);

log4net.Config.XmlConfigurator.ConfigureAndWatch(finfo);

logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

 

    

nunit에서 사용할 때 동작하지 않는 경우

Log4Net.config 파일을 만들어서 아래와 같이 만든다.

    

<?xml version="1.0" encoding="utf-8" ?>

<log4net>

    <!-- 필요한 설정 정보 입력 -->

</log4net>

 

소스에서 위에서 만든 설정 파일을 사용하도록 한다.

[SetUp]

public void RunBeforeAnyTests()

{

string logFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log4Net.config");

FileInfo finfo = new FileInfo(logFilePath);

log4net.Config.XmlConfigurator.ConfigureAndWatch(finfo);

}

 

    

로그를 메일로 보내기

로그 설정 파일에서 log4net.Appender.SmtpAppender 를 사용한다.

보낼 메일의 문지 인코딩은 UTF-8을 사용한다.

로그 메시지 하나마다 메일을 보내는 것이 아니고 하나의 메일에는 복수의 로그 메시지가 있다.

설정 예 

<appender name="SmtpAppender" type="log4net.Appender.SmtpAppender">

<!-- 메일을 받을 곳의 주소 -->

<to value="to@domain.com" />

 

<!-- 메일을 보내는 곳의 주소 -->

<from value="from@domain.com" />

 

<!-- 제목 -->

<subject value="test logging message" />

 

<!-- 메일 서버 정보 -->

<smtpHost value="SMTPServer.domain.com" />

 

<!-- 메일을 보낼 때의 버퍼 사이즈 -->

<bufferSize value="512" />

<lossy value="true" />

<evaluator type="log4net.Core.LevelEvaluator">

<threshold value="WARN"/>

</evaluator>

 

 

<layout type="log4net.Layout.PatternLayout">

<conversionPattern value="%newline%date [%thread] %-5level %logger - %message%newline" />

</layout>

</appender>

   

 

Filter

특정 appender에서만 로그 레벨을 바꾸고 싶은 경우 <appender> 요소 내에서 <filter> 요소를 추가하여 조정할 수 있다.

로그 레벨을 FATAL 때만 남기고 싶다면 다음과 같이 한다.    

<filter type="log4net.Filter.LevelRangeFilter">

<param name="LevelMin" value="FATAL" />

<param name="LevelMax" value="FATAL" />

</filter>

   

 

로그 파일 로테이션 시 파일크기+날짜 사용하기

<appender name="debugAppender" type="log4net.Appender.RollingFileAppender">

<filter type="log4net.Filter.LevelMatchFilter">

<levelToMatch value="DEBUG" />

</filter>

<filter type="log4net.Filter.DenyAllFilter" />

<File value="Logs\debug.log" />

<PreserveLogFileNameExtension value="true" />

<appendToFile value="true" />

<rollingStyle value="Composite" />

<maximumFileSize value="32MB" />

 

 

로그 파일 이름에 날짜 붙이기

<File value="Logs\perf_.log" />

<staticLogFileName value="false" />

<datePattern value="yyyyMMdd-HHmm" />

 

perf_20140601-1140.log 라는 식으로 로그 파일이 만들어진다.



같은 로그 파일 이름이 있으면 일련 번호 붙이기

파일 사이즈 및 날짜로 로그 파일을 로테이션 할 때 새로 만드는 파일 이름이 이전과 같은 이름이 되어버려서 이전 것을 덮어 쓸 수 있다.

예를들면 파일 사이즈가 설정 값을 넘겨서 새로 만드는데 이전까지 사용한 로그 파일의 생성 날짜가 같은 날이면 지금 새로 만드는 것도 같은 이름이 되어 버린다.

이유는 파일 이름 규칙이 'xx-날짜.log'로 했기 때문이다.

이름 중복을 피하기 위해서 로그 파일 이름에 일련 번호를 붙이도록 한다.

CountDirection 설정을 사용한다. 사용 방법은 아래와 같다.

<File value="Logs\perf_.log" />

<CountDirection value="1" />

이후 로테이션으로 새로 파일을 만들 때 파일 이름이 다음처러 만들어진다.

info_20140612.1.log

info_20140612.2.log

 


로그 파일 개수 제한

생성된 로그 파일의 최대 개수를 제한 할 수 있다.

<maxSizeRollBackups value="10" />

   

 

서식 타입

%literal 리터럴을 표시(「%」을 출력하는 경우 등)

%newline 개행

%logger logger 이름

%class 클래스 이름

%date 실행 시간

%exception 예외 정보

%location 메소드 이름, 행 번호

%line 행 번호

%message 메시지

%method 메소드 이름

%level 로그 레벨

%thread 스레드 ID

%appdomain 실행 파일 이름

%identity 현재 컨텍스트의 유저 이름

            (System.Threading.Thread.CurrentPrincipal.Identity.Name 의 값이 설정 되어 있다.)

%username 유저 이름

 

 

 

 

 

웹 정보

Log4Net 간단 사용법(Web 포함) http://mvcp.tistory.com/404

log4net을 사용해 보자 http://nalra999.blog.me/110095620129

[ASP.NET] log4net의 설정과 사용법 예제 http://oneprimary.blog.me/30076001926

Apache log4net™ Config Examples http://logging.apache.org/log4net/release/config-examples.html    

신고
by 흥배 2014.06.13 08:00

Windows Forms

처리하지 못한 예외가 발생한 경우 Application.ThreadException 이벤트 핸들러가 자동으로 호출된다. 그래서 여기에 에러 메시지를 보여주는 기능 등을 작성하면 된다.

)

namespace WindowsFormsApplication1

{

    static class Program

    {

        [STAThread]

        static void Main()

        {

            Application.ThreadException +=

 new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);

            Application.EnableVisualStyles();

            Application.SetCompatibleTextRenderingDefault(false);

            Application.Run(new Form1());

        }

 

        static void Application_ThreadException(object sender,

 System.Threading.ThreadExceptionEventArgs e)

        {

            MessageBox.Show("처리하지 못한 예외 발생");

            Application.Exit();

        }

    }

}

 

 

Console

AppDomain.CurrentDomain.UnhandledException 이벤트 핸들러를 자동으로 호출한다.

namespace ConsoleApplication1

{

    class Program

    {

        static void Main(string[] args)

        {

            AppDomain.CurrentDomain.UnhandledException +=

new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

        }

 

        static void CurrentDomain_UnhandledException(object sender,

UnhandledExceptionEventArgs e)

        {

            MessageBox.Show("처리하지 못한 예외 발생");

            Environment.Exit(-1);

        }

    }

}

 

 

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

.NET 플랫폼용 범용 라이브러리.

ActiveRecord, Csv, Command Line Parsing, Configuration, Validation, Logging, Collections, Authentication 등의 기능을 가지고 있다.

닷넷 프레임워크: 4.0 및 3.5 지원


예제

http://commonlibrarynet.codeplex.com/wikipage?title=Examples&referringTitle=Documentation



Logging Example_Logging.cs

스레드 세이프. 단 백 그라운드로 로그를 저장하는 것은 아님.

콘솔, 파일, 데이터베이스에 로그 쓰기 지원.

복수의 로그 저장소를 사용 가능.

로그 레벨 지원.

log4net에 비해 기능은 부족하지만 간단해서 좋음


Account Example_Account.cs

계정 생성, 인증, 변경, 확인을 해주는 라이브러리.

인메모리나 DB 저장소를 지원한다. ADO.NET을 지원하는 DB라면 사용가능 제공하는 클래스를 사용하기 위해서는 'UserRepository' 클래스에 정의된 스키마를 사용해야 한다.

'UserValidator' 클래스를 통해서 계정 데이터 체크를 할 수 있다.

저장소 타입은 'Repository' 폴더에 정의 되어 있다.



Arguments Example_Arguments.cs

CommandLine 인자 파싱.

파싱하여 입력된 인자 값이나 (만약 없다면)default 값을 사용할 수 있다.



Automation Example_Automation.cs

xml에 닷넷으로 만든 Command 클래스를 상속한 클래스를 등록하면 실행 시킬 수 있다.

xml에는 변수 정의도 가능하다.

배치 명령어를 실행할 때 유용 AutoTest 프로그램을 만들 때 사용하면 좋을 듯



Benchmark Example_BenchMark.cs

성능 측정을 할 때 사용한다.

벤치마킹을 실행할 때 이름과 메시지를 지정할 수 있다.

벤치마킹 데이터를 저장하여 리포팅 할 수 있다.

벤치마킹 결과를 콘솔 및 지정한 로거에 출력한다.



BootStrapper Example_BootStrapper.cs

등록한 작업을 프로그램 실행 및 종료 시에 실행한다.

등록한 작업 중 선택적으로 실행할 수 있다.



CodeGenerator Example_CodeGenerator.cs

동적으로 프로퍼티를 가지는 클래스를 만들 수 있다.

CommonLinrary. NET에서 Entity 생성에 사용.

템플릿을 만들어 놓으면 클래스에서 함수도 생성 가능!!



Config Example_Config.cs

설정 정보를 읽는다.

ini 파일 형식과 DB에서 읽기를 지원한다.

복수의 설정 파일을 읽을 수 있다.



Cryptography Example_Cryptography.cs

암호화/복호화.

MD5 해쉬와 DES, 3DES 지원.



CSV, Database Example_Csv.cs Example_Database.cs

csv 형식 파일을 읽고, 쓰기 가능.

ADO.NET을 좀 더 쓰기 편하게 데이터베이스 기능 랩핑 쿼리 데이터 클래스 직력화 기능 제공



Diagnostics Example_Diagnostics.cs

Windows의 작업관리자에서 보여주는 컴퓨터 상태를 파일로 저장해 준다.



Errors Example_Errors.cs

에러를 체계적으로 관리.

복수의 에러를 추가.

에러를 키와 값으로 관리



Extensions Example_Extensions.cs

동적으로 지정된 위치에 있는 lib을 로딩.

클래스를 인스턴스화.



ImportExport Example_ImportExport.cs

문서 데이터를 읽어서 객체로, 객체를 문서 데이터로 변경.

문서 포맷 형식은 xml, csv, ini 지원.

파일로 출력 또는 string으로 출력.



QueueProcessor Example_QueueProcessor.cs

확장성 있는 queue 컨테이너.

큐를 넣은 후 process 호출로 저장된 큐를 처리한다. 기본으로 한번에 5번

큐 처리 함수를 이름을 붙여서 등록, 등록된 함수를 사용할 수 있다.

큐를 추가할 때 처리할 커스텀 타입용 프로세스 함수 추가.

큐 추가 때 호출된 함수를 이름 있는 함수 등록 가능.



Scheduler Example_Scheduler.cs

태스크 스케줄러.

주기적으로, 최대 실행 횟수, 최대 실행 시간을 지정할 수 있다.

QueueProcessor와 연동 가능.

스레드 세이프.

내부 구현은 .NET 라이브러리의 Timer 클래스를 사용. Timer 클래스는 워커 스레드에서 동작한다.



Scripting Example_Scripting.cs

자바스크립트와 비슷한 스크립트 언어를 호스팅 할 수 있다.



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

오픈소스인 Glimpse을 사용하면 ASP.NET MVC의 트레이스나 루트 정보를 쉽게 확인할 수 있다.

설치도 간단하다.

확장을 통해서 redis나 mongoDB 등의 다른 라이브러리의 상태도 확인해 볼 수 있다.

 

공식 사이트 http://getglimpse.com/

 

설치는 NuGet을 사용하여 설치할 수 있다.

 

예) 컨트롤러에 using System.Diagnostics을 추가하고 Index()에 Trace를 추가한다.

public ActionResult Index()

{

   ViewBag.Message = "Welcome to ASP.NET MVC!";

   Trace.Write("Glimpse Demo");

   Trace.TraceWarning("Glimpse Warning Demo");

   return View();

}

 

Ctrl+F5로 실행한다.

 

http://localhost:포트번호/Glimpse.axd에 이동한다.

 

Turn Glimpse On을 클릭한다(원래 페이지로 돌아간 후 오른쪽 하단에 있는 아이콘을 클릭한다(만약 표시되지 않으면 리로드한다).

아래와 같이 다양한 정보가 표시된다.

 

Trace를 클릭하면 소스에 기록한 트레이스가 출력된다.

 

 

web.config 설정을 통해 Azure에 설치된 앱을 원격으로 볼 수도 있다.

 

 

 

참고

http://getglimpse.com/

http://www.hanselman.com/blog/IfYoureNotUsingGlimpseWithASPNETForDebuggingAndProfilingYoureMissingOut.aspx

(일어) http://blogs.msdn.com/b/chack/archive/2013/05/21/glimpse-aspnet-diagnostics-framework.aspx

(일어) http://troushoo.blog.fc2.com/blog-entry-19.html

신고
by 흥배 2014.03.03 08:00

Mono 2.10 이후의 메이저 업데이트.

C# 5 지원과 .NET 4.5 호환성을 가진다.

 

주요 변경점

n  C# Async 컴파일러.

n  MS의 오픈 소스인 ASP.NET MVC4, ASP.NET WebPages, Entity Framework, Razor, System.Json 포함.

n  SGen 가베지컬렉터가 기본 GC가 되었다. 멀티프로세서를 활용하여 성능과 확장성을 향상.

n  Eval() API에서 식만이 아닌 형 전체를 컴파일 할 수 있게 되었다. Compile as Service도 글로벌 컴파일러가 아니게 되어 복수의 영역에서 인스턴스화가 가능.

n  ThreadLocal<T> List<T>등 몇 개의 형을 런타임 최적화.

n  성능 튜닝 용으로 컴파일러에 대해서 인라인 코드를 강제화 하는 새로운 속성을 추가.

n  MacOS 상에서 64비트 바이너리로서 컴파일 가능.

n  USB 기기 접속 성능 향상을 한 소프트웨어 디버거.

n  OS X Mono에는 F# 3.0 번들.

n  SQLite 구현으로 iOS의 암호 API가 지원되었다. 설정에 의한 스레드 모델 변경도 가능.

 

Release Notes

http://www.mono-project.com/Release_Notes_Mono_3.0

 

OpenSUSE 13.1에서는 기본으로 Mono 3.0.6 지원

 

 

 

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

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

Mono의 시작

Mono 프로젝트 리더인 Miguel de Icaza는 본래 Linux에 자유로운 데스크탑 환경을 구현하는 것을 목적으로 설립한 GNOME 프로젝트의 초기 리더이며, Mono는 처음에는 Linux 데스크탑 환경용 애플리케이션 개발을 쉽게 하기 위해 시작된 프로젝트이다.

당시 Miguel GNU 프로젝트에서 소프트웨어 대상을 받을 정도로 UNIX 해커이면서 마이크로소프트에 인턴 응모를 한 일이 있을 정도로 마이크로소프트 기술 열광자이고, COM 기술을 사랑하여 비슷한 기술인 CORBA를 응용한 'Bonobo'라는 소프트웨어를 개발하였고, GNOME 개발 촉진을 그렸던 역사도 있다.

그는 .NET Framework가 등장하자 바로 흥미를 여기로 옮겨서 GNOME 개발을 이것으로 하려고 당시 그가 설립한 Ximian사에서 Mono 프로젝트를 시작하였다.

 

Mono는 주로 다양한 ADO.NET 프로바이더나 ASP.NET 등 서버 사이드 기술을 구현하는 것을 시작하여(이것이 1.0) 다음은 Windows Forms을 구현하였다. 이것이 버전 2.0으로 .NET 2.0까지 구현했을 때 .NET 3.5가 나왔다. 이것을 사용하여 Paint.NET Mono로 구현한 것이 Paint-mono 이다.

 

.NET 기술은 광범위 하여 Mono 팀은 구현 대상을 조정할 필요가 있었다. 주로 런타임과 C# 컴파일러 개선, ASP.NET, WCF 등의 서버 사이드 기술 구현 등이 집중적으로 개발 되었다. 또 이것과는 별개로 Gtk#이나 MonoDevelop 개발도 병행하였다.

 

'애플리케이션을 자연스럽게 Linux에서도 동작시키자' 라는 레벨의 목적으로도 .NET Framework에 캐치업 하는 것은 쉽지 않다고 생각되지만 Mono 팀이 코어 부분을 착실하게 진행 시키는 중 하나의 전환점을 맞이하였다. MS ASP.NET MVC를 오픈소스로 공개하는 등 .NET Framework의 일부를 오픈 소스로 하는 흐름이었다. ASP.NET MVC, DLR은 오픈 소스로 공개되어서 그대로 Mono에 들어왔다.

이후 MS Entity Framework, Reactive Extensions 라는 .NET의 대규모 라이브러리를 차차 오픈 소스화 하였다. 이것들은 Mono에 그대로 들어와서 바이너리 배포 되고 있다.

 

일단 코어 부분에 집중할 수 있게 된 Mono팀은 완성도를 더욱 높이면서 대상 플랫폼을 확대할 수 있었다. C# 컴파일러는 재 빠르게 C# 5.0 기능을 구현하였다. iOS Android Xamarin 제품의 양 축이 되고 있지만 이것들을 타겟으로 하기 위해 런타임이 개선 되고 있는 측면도 크다. Mono는 현재로서는 주로 Xamarin이 제품으로 사용하는 범위의 기능을 높이는 방향을 향하고 있다.

 

 

Mono 소프트웨어 구성 요소

mono 소스 코드 저장소에는 런타임, C# 컴파일러, mono 클래스 라이브러리가 포함된다.

mono 공식 저장소: https://github.com/mono/mono

mono 소스 상에서 앞의 3개의 요소(런타임, C# 컴파일러, 클래스 라이브러리)는 각각 다음의 디렉토리에 존재한다.

mono/   최상위 디렉토리

|- mono   런타임

|- mcs/

    |- mcs  컴파일러

    |- class  클래스 라이브러리

 

 

 

mcs: C# 컴파일러

mcs C#으로 구현 되어 있고 초기는 csc로 빌드 되었다(msc 빌드에는 mcs가 필요하므로 현재도 부트스트랩용의 빌드된 mcs.exe가 먼저 사용된다).

 

최신 mcs async/await키워드, caller info 등을 포함한 C# 5.0까지의 기능을 모두 구현하고 있다. csc와 비슷한 명령 라인 옵션을 갖추어서 기존의 C# 코드를 문제 없이 빌드 할 수 있다. 문법 에러 체크도 .NET Framework csc와 같이 엄격하게 해준다(배경에는 C# 언어 사양이 규정하는 C#코드의 흐름 분석과 mcs의 충실한 구현이 있다). csc의 에러와 경고는 Visual Studio와 친화적으로 동작하기 위해 일정한 형식으로 출력되지만 mcs에서도 그것은 답습되어 MonoDevelop에서도 마찬가지로 메시지가 목록으로 표시된다. Windows 상에서 동작하는 MonoDevelop csc를 사용하고 있으나 그 컴파일 오류는 MonoDevelop에서 올바르게 목록으로 표시될 것이다.

 

ECMA 334 ECMA 335라는 사양은 C#의 소스 수준의 호환성과 CIL(바이너리 중간 코드) 모두에 대해 호환성을 갖기 위한 것이며, mcs가 생성하는 exe파일 및 dll 파일은 csc이 생성하는 것과 호환성이 있다. 대상 플랫폼에서 읽어지는 어셈블리이면 mcs가 생성한 것을 .NET Framework에서 읽어도 csc가 생성한 것을 mono에서 읽어도 각각 실행할 수 있을 것이다(물론 대응하는 프로파일의 유무 및 대상 프로파일에서의 참조 어셈블리의 유무는 여기에서는 다른 문제이다).

 

다만 mcs-debug 옵션으로 생성하는 디버그 기호 파일은 ".mdb" 라는 자체 포맷이 되어 있다. 이것은 ECMA CLI에 디버그 기호의 규정이 없고, pdb 파일의 사양이 비공개였기 때문이다(현재는 cildb라는 pdb mdb도 아닌 포맷이 규정되고 있다). 현재의 mono에는 이들을 상호 변환하기 위한 툴 pdb2mdb mdb2pdb가 존재한다. 참고로 이들은 포맷의 차이 밖에 없으며 정보의 성질 차이는 없을 것이다.

 

mcs에 의한 LINQ, dynamic 형의 지원 조건은 csc의 경우와 별로 바뀌지 않았다. 즉 확장 방법을 쓰려면 System.Core.dll을 참고로 한 후 "using System.Linq;" 라고 기술해야 하고, dynamic 형을 쓰려면 Microsoft.CSharp.dll을 참조에 더해야 한다(이는 간접적으로 Mono.CSharp.dll 라고 하는 라이브러리도 이용한다). 이들 라이브러리만 있으면 mcs dynamic 형을 충분히 지원할 수 있다(필자는 실제로 PlayStation Mobile이 아직 "PSSuite"로 불리던 시절에 이 환경에서 이들 어셈블리를 적절한 프로파일로 빌드 후 추가하여 지원되고 있지 않을 dynamic형을 기술하고 있는 코드를 동작 시킨 적이 있다).

 

그리고 mcs csc과 달리 다수의 라이브러리를 기본적으로 참조하지 않는다. csc.rsp가 없는 것 같은 상태라고 생각하면 좋다. 이것은. NET개발자에게는 다소 혼란의 원인이지만, 사용되지 않는 어셈블리를 헛되게 해석하면 그만큼 컴파일 시간이 희생된다. 그것을 싫어한 것이다.

 

 

mcs의 설계

mcs는 소스의 해석(파서), 시맨틱 분석, 코드 생성 등 일반적인 순서에 근거하는 언어 컴파일러 구현이므로 주목 할 것은 많지 않다. 소스의 해석이 jay 이라는 LALR(1)문법 파서 생성기를 사용하고 있다(원래는 Java의 소스를 생성하는 도구이지만 C# 생성 기능을 추가했다).

* jay: https://github.com/mono/mono/tree/master/mcs/jay

 

코드 생성은 과거 .NET mscorlib에 포함된 System.Reflection.Emit를 사용하고 있었지만 이것은 "크로스 프로파일" 형태로 어셈블리를 빌드 하는 것을 막는 것으로(예를 들면 NET 4.0 호환 어셈블리를 생성하려면 .NET 4.0 프로파일의 mscorlib.dll에서 동작하는 mcs가 필요했다), 현재는 "IKVM.Reflection.Emit"라고 하는 라이브러리를 사용하고 있다. IKVM Java 바이트 코드를 .NET/Mono 상에서 실행하게 하는 프로젝트이다. 만약 당신이 언어 처리 쪽으로 작성하고 있다면 System.Reflection.Emit 대신 IKVM.Reflection.Emit 사용을 검토하는 것이 뒤에 편하게 될지도 모른다. System.Reflection.Emit을 쓰고 있었을 때는 mono에는 타깃 프로필에 맞춰 mcs, gmcs, dmcs, smcs 같은 컴파일러 툴이 난립하고 있다. 지금은 mcs의 옵션 -sdk로 모두 대응하고 있다.

 

세만틱 분석은 mcs의 가장 복잡한 부분이지만 yield 키워드와 익명형, var에 의한 형 추론, dynamic형 사용, async/await 키워드의 Task 처리 등이 어떤 논리로 생성되고 있는지 만일 흥미가 있다면 mcs의 소스를 따라가면서 확인 가능하다.

 

 

mcs의 응용 사례

mcs는 오랫동안 단일 도구로 존재해 왔지만 최근에는 Mono.CSharp.dll 이라는 독립한 라이브러리로서 재이용 할 수 있도록 되어 있다. MonoDevelop SharpDevelop 두 프로젝트 간에 C# "서비스로서의 컴파일러"(Compiler as a Service 또는 Language Service)로 개발되고 있는"NRefactory"에서도 사용되고 있다. 마이크로 소프트 기술로 말하면 "Roslyn"에 상당하는 것이다.

 

마지막으로 mcs의 흥미로운 응용으로 mono C# shell을 소개한다. 이것은 csharp.exe라는 툴로 안에서는 Mono.CSharp.dll을 사용하여 C#로 인터랙티브 셸을 실현하는 것이다. Gtk#의 도구가 콘솔에서 실행할 수 있는 환경(Linux )이라면 gsharp이라는 GUI 셸도 이용할 수 있다.

* gsharp: https://github.com/mono/mono-tools/tree/master/gsharp

 

C#의 코드를 그 자리에서 쓰고 Gtk# Widget을 만들어 표시하는 것도 가능하다(Miguel은 일찍이 이 위에 Gtk.Canvas에서 도형을 그리는 데모를 하였다). MonoDevelop 사용자 커뮤니티에서는 이 mono C# shell MonoDevelop의 패드로 쓰도록 한 add-in도 공개되고 있다. 인터랙티브 셸은 이미 F#(fsi)의 독무대가 아니다.

 

 

 

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

저작자 표시
신고
by 흥배 2014.02.24 08:00
| 1 2 3 4 5 6 7 |