블로그 이미지
대갈장군

calendar

1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31

Notice

'프로그래밍'에 해당되는 글 102

  1. 2017.05.31 Asynchronous programming
  2. 2016.02.08 Getting Physical With Memory
  3. 2016.02.06 Anatomy of a Program in Memory
  4. 2015.02.12 C Language Constructors and Destructors with GCC
  5. 2014.08.28 typename에 대해 좋은글 발견!
  6. 2014.08.07 Headers and Includes: Why and How
  7. 2013.09.27 ClickOnce를 이용한 윈도우 폼의 배포3
  8. 2013.09.26 Arduino에서 입력으로 들어오는 트리거 신호 지연시키기
  9. 2013.09.24 Implicit Platform Invoke
  10. 2013.09.24 Explicit PInvoke C++ (DllImport Attribute)
  11. 2013.09.20 Mixed, Pure, Verifiable
  12. 2013.05.24 Setup Project로 만든 단축 아이콘에 Run as Administrator 옵션 부여 하기
  13. 2013.05.03 [MSDN] .NET Framework 구조4
  14. 2013.04.25 Quick C++/CLI - Learn C++/CLI in less than 10 minutes
  15. 2013.04.25 Pure C++: Hello, C++/CLI
  16. 2013.04.17 Machine Learning 튜토리얼
  17. 2013.04.13 Best Practices For Managed And Native Code Interoperability - Jesse Kaplan
  18. 2013.02.12 __try / __finally / AbnormalTermination()
  19. 2012.12.18 __try / __finally 예제 코드
  20. 2012.12.18 GetLastError() / FormatMessage() 함수 사용 샘플 코드
  21. 2012.12.15 iexpress를 이용한 어플리케이션 재배포 패키지 만들기
  22. 2012.12.15 Determining which DLLs to Redistribute (어떤 DLL을 재배포할껀가?)
  23. 2012.12.14 Dependency Walker 아이콘 & 에러와 경고의 해석
  24. 2012.12.14 Dependency Walker의 검색 가능한 의존성의 타입들
  25. 2012.12.14 Visual C++ 2010에서 작성한 프로그램의 배포 방법 세 가지
  26. 2012.12.13 커널 오브젝트를 이용한 스레드 동기화 - 이벤트 오브젝트
  27. 2012.12.13 커널 오브젝트 (Kernel Objects)
  28. 2012.12.13 Understanding InvokeRequired() 함수
  29. 2011.03.12 윈도우 메모리 파이널!
  30. 2011.03.12 윈도우 메모리 - 4 (힙과 메모리 맵)
2017. 5. 31. 23:43 프로그래밍/C#

https://docs.microsoft.com/en-us/dotnet/csharp/async


만약 입력과 출력에 관계된 일련의 처리 (데이터 베이스 처리나 네트워크를 통한 데이터 처리와 같은 일들)를 해야 할 경우에 이른바 asynchronous programming이 필요한데 이것은 내가 사용해 본 Node JS의 기본적인 처리 방법과 같다. 


요청이 들어오면 콜백 함수를 정해두고 일이 마무리 되면 콜백을 호출하는 일련의 처리 방식을 비동기 (asynchronous) 적이다 라고 하는 것.


C#은 기본적으로 제공해주는 비동기 함수 호출을 위해 아주 유용한 라이브러리 함수가 제공되니 그것이 바로 await / async 이구나.


비동기 프로그래밍의 핵심은 Task와 Task<T> 객체인데 이들은 비동기 operation들을 모델링한다. 이 객체들은 async와 await 키워드에 의해서 지원되는데 다음과 같은 경우를 생각해 볼 수 있다.


입출력에 관계된 코드의 경우, await operation은 Task나 Task<T>를 async 함수 내부에서 리턴하게 된다.


CPU에 관계된 코드의 경우 (CPU를 background로 사용하는 코드) await는 Task.Run 함수를 사용해서 background thread를 시작하게 된다.


이 await 키워드가 바로 마법을 행하는 놈인데 이놈은 실행되는 시점에 즉시 자신을 호출한 호출자에게 컨트롤을 '양보' (yield)한다. 다른 말로 하자면 버튼을 누르는 그 즉시 버튼을 컨트롤 하는 스레드에게 우선순위를 양보함으로써 UI가 사용자에 입력에 즉각적으로 반응하도록 한다는 것. 


이제 예를 들어보자. 가장 단순하게 인터넷에서 어떤 페이지의 버튼을 눌렀을 때, async와 await를 이용해서 비동기 프로그래밍을 구현한 것이다.


C#
private readonly HttpClient _httpClient = new HttpClient();

downloadButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI as the request
    // from the web service is happening.
    //
    // The UI thread is now free to perform other work.
    var stringData = await _httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};


매우 간단하게 비동기 프로그래밍을 구현했다. 다만, 위 코드에서 보다시피 GetStringAsync라고 하는 비동기 전용 함수가 미리 구현되어 있어서 사용한다는 점. 그리고 버튼의 클릭 이벤트 핸들러 추가시 async 키워드를 사용했다는 점 주의.


다음으로는 CPU에 바인딩 된 경우에 대해서 알아보면, 많은 일을 처리해야 하는 경우에 사용할 수 있는 것이 바로 비동기 프로그래밍이다.


아래의 코드가 바로 그 예제이며 버튼이 눌러 졌을 때 비동기적으로 background thread를 Task.Run()을 통해서 호출하고 사용자는 스무스하게 플레이 할 수 있도록 UI에게 컨트롤을 양보한다.


C#
private DamageResult CalculateDamageDone()
{
    // Code omitted:
    //
    // Does an expensive calculation and returns
    // the result of that calculation.
}


calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI CalculateDamageDone()
    // performs its work.  The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};



여기서 몇가지 중요한 점이 있는데 await 키워드는 반드시 async로 정의된 함수 안에서만 쓸수 있다는 점. 상식적으로 생각해서 컴파일러에게 이 함수는 비동기 함수를 포함하고 있으니 너가 적절한 조치를 취해야 한다는 것을 미리 알려줘야 한다는 점에서 이런 룰이 적용된 것으로 보인다. 물론, 암묵적인 변환을 할수 있을지는 몰라도, 아마도 명시적인 것이 더 적합할 듯.


또 한가지 더 중요한 점으로 지적된 것이, 비동기 프로그래밍을 하는 것이 성능에 있어서 분명한 이득이 될 때 사용하라는 점. 비동기 프로그래밍은 공짜로 주어지는 것이 아니기 때문에 확실히 성능상의 이득이 있을때 적절히 사용하라는 점. 오버헤드가 있다는 말.


추가적인 예제가 아래와 같이 나와 있다. 특정 웹사이트에 접속해서 특정 단어가 몇개나 들어가 있는지 검사해서 숫자를 결과를 리턴하는 것인데 await와 async로 만들었다.


C#
private readonly HttpClient _httpClient = new HttpClient();

[HttpGet]
[Route("DotNetCount")]
public async Task<int> GetDotNetCountAsync()
{
    // Suspends GetDotNetCountAsync() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await _httpClient.DownloadStringAsync("http://dotnetfoundation.org");

    return Regex.Matches(html, ".NET").Count;
}


드디어 Task<T>가 그 모습을 드러냈는데 T가 바로 리턴 값의 타입이다. 아래 코드는 UWP의 경우의 코드이다.


C#
private readonly HttpClient _httpClient = new HttpClient();

private async void SeeTheDotNets_Click(object sender, RoutedEventArgs e)
{
    // Capture the task handle here so we can await the background task later.
    var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("http://www.dotnetfoundation.org");

    // Any other work on the UI thread can be done here, such as enabling a Progress Bar.
    // This is important to do here, before the "await" call, so that the user
    // sees the progress bar before execution of this method is yielded.
    NetworkProgressBar.IsEnabled = true;
    NetworkProgressBar.Visibility = Visibility.Visible;

    // The await operator suspends SeeTheDotNets_Click, returning control to its caller.
    // This is what allows the app to be responsive and not hang on the UI thread.
    var html = await getDotNetFoundationHtmlTask;
    int count = Regex.Matches(html, ".NET").Count;

    DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";

    NetworkProgressBar.IsEnabled = false;
    NetworkProgressBar.Visbility = Visibility.Collapsed;
}


뭐 비슷한데 가운데 추가적으로 프로그래스 바를 보여주는 옵션이 있네. 굳이 UWP의 경우를 따로 보여줘야 했나 싶다만, 아마도 UWP를 대세로 밀고 있는 듯 한 느낌적인 느낌?


그리고, 추가적으로 여러개의 task가 완료되기를 기다릴수도 있는데 바로 Task.WhenAll과 Task.WhenAny와 같은 것을 통해서 여러개의 비동기 background job들을 관리 가능하다.


C#
public async Task<User> GetUser(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.
}

public static Task<IEnumerable<User>> GetUsers(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();

    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUser(id));
    }

    return await Task.WhenAll(getUserTasks);
}


Task<User> 타입을 가지는 List를 만들어서 (getUserTasks) 각각의 userID를 리스트에 추가한 다음 한꺼번에 await를 통해서 호출하고 동시에 모든 작업이 완료되면 비동기적으로 그 결과를 리턴하게 된다. 


같은 일을 LINQ를 이용하는 경우에는 다음과 같다. 


C#
public async Task<User> GetUser(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.
}

public static async Task<User[]> GetUsers(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUser(id));
    return await Task.WhenAll(getUserTasks);
}


훨씬 간결한 형태의 코드가 가능하다. LINQ의 파워가 느껴진다... 포스가 느껴진다... 



마지막으로 중요한 몇가지 포인트들이 있는데,


1. async 함수는 반드시 최소한 한개의 await 키워드를 함수 내부에 가지고 있어야 한다. 없어도 컴파일은 되지만 효율이 겁나 떨어진다. 불필요한 async 남발은 피하라는 것.


2. async void는 반드시 event handler에만 사용하라. event의 경우 리턴값이 없으므로 Task나 Task<T>를 쓸 필요가 없고 그런 경우에는 async void를 쓰면 된다. 다만 이 경우에 컴파일러 입장에서는 몇몇 이유로 검증하는 것이 어렵기 때문에 최대한 async void를 사용하는 횟수를 줄여서 필요한 경우에만 쓰고 남발하지 말라는 것. 


3. LINQ와 async를 함께 쓰는 것은 매우 포스가 있지만 중요한 점은 LINQ 자체가 구현 되어 있는 방식이 async와 맞지 않는 부분도 있고 잘못 쓰게 되면 데드락이 걸리기 때문에 정확히 알고 있는 경우 아니면 되도록이면 함께 쓰지 말라.




'프로그래밍 > C#' 카테고리의 다른 글

C# 이벤트 - 객체 지향적 메시지  (0) 2010.06.25
C# - 제너릭 (Generic) 에 대한 질문  (0) 2010.06.18
Covariance and Contravariance  (0) 2010.06.18
델리게이트 - Delegate  (0) 2010.06.17
posted by 대갈장군
2016. 2. 8. 23:04 프로그래밍/C++

출처: http://duartes.org/gustavo/blog/post/getting-physical-with-memory/

저자: Gustavo Duarte


이 글도 상당히 도움이 되는 글인듯 하여 번역하여 옮겨둔다.


원래 복잡한 시스템을 이해하려면 가장 기초가 되는 원초적인 것을 들여다 볼 필요가 있다. 그런 점에서 우리는 프로세서와 버스 간의 인터페이스에 대해서 알아보자... 저자는 EE 전공자가 아니므로 EE 친구들이 걱정하는 그런 부분은 가뿐히 무시하겠다고... CS! CS! /yay


Physical Memory Access

위의 그림은 Core 2 디자인이다. 코어 2의 경우 775개의 핀을 가지고 있다. 그중 절반은 파워만 제공하고 데이터를 운송하지 않는다. 그럼 대략 387.5 개 인가? 작동하는 핀들을 그룹으로 묶어주면 사실 코어2의 디자인은 상당히 단순하다. 위의 그림을 보면 중요한 핀들을 보여주고 있는데 Address pin, Data pin, Request pin 들이다. 이 동작들은 FSB (Front Side Bus)라 불리는 곳에서 이른바 Transaction이라는 이름으로 시행되는데 FSB Transaction은 5개의 페이즈를 가지고 있단다. Arbitration, Request, Snoop, Response, Data 요렇게 다섯가지. Agent라 함은 프로세서에 Northbridge까지 포함한다. 한국말로 요원들이라고 해야 겠군... 이 요원들이 다섯가지 페이즈를 호출하여 사용하는데 각각 다른 형태의 임무를 수행한다는 군...


우리가 관심있는 분야는 오직 Request Phase다. Request Agent (주로 한개의 프로세서) 에 의해 2개의 packet이 출력으로 나오는 페이즈가 바로 Request Phase. 

FSB Request Phase, Packet A

위 그림처럼 35-3 (33 비트)의 길이를 가지는 필드를 먼저 보게 되는데 이것은 물리 주소를 알려주는 필드이다. 그리고 바로 뒤에 REQ 핀이 오는데 이 녀석의 각 비트 값에 따라 어떠한 형태의 접근 및 처리를 원하는지 알려준다. 이 첫번째 패킷이 나간 후에 바로 두 번째 패킷이 나가는데 아래 그림과 같다.

FSB Request Phase, Packet B

여기서 재미 있는 부분은 바로 Attribute Signals 필드인데 (31:24) 여기를 보면 메모리 캐싱 동작의 5가지 종류에 대해서 정의하고 있다. FSB에 이 정보를 제공함으로써 Request Agent는 다른 프로세서들에게 이 Transaction이 그들의 캐시에 영향을 주는지 알려주며 메모리 컨트롤러인 Northbridge에게 어떻게 동작해야 하는지도 알려준다. 프로세서는 커널에 의해 작성되는 page table을 참조하여 주어진 메모리 영역의 타입을 결정한다.


일반적으로 커널은 모든 RAM 영역을 write-back으로 간주하는데 그것이 최상의 퍼포먼스를 내기 때문이다. Write-back 모드에서는 메모리 액세스의 단위가 cache line 이 되고 이것은 64 바이트이다 (코어2에서). 만약 하나의 프로그램에서 1 바이트를 메모리에서 읽으면 프로세서는 그 1바이트를 담고 있는 전체 cache line 을 L2 와 L1 캐시에 로드한다. 또한 프로그램이 메모리에 쓰는 경우, 프로세서는 캐시에 있는 라인만 수정하고 main memory는 업데이트 하지 않는다. 나중에, 처리해야 하는 시점이 오면 그제서야 cache line 전체를 한방에 메인 메모리로 써버린다. 고로 대부분의 request는 Length field가 11 (64 bytes) 값을 가지고있다. 다음 그림을 보면 캐시에 없는 데이터를 어떻게 읽어들이는지 보여준다.

Memory Read Sequence Diagram

위 그림을 보아하니 대충 이해가 간다. 프로그램이 특정 주소를 달라고 요청하게 되면 우선 L1 캐시에 있는지 확인해보고 없으면 L2로 가고 거기도 없으면 FSB로 요청해 그 주소로 직접적인 액세싱을 하게 된다는 거. 길이는 64바이트로 고정되어 있군. 


추가적으로 인텔 컴퓨터의 일부 메모리 영역은 장치로 연결된 경우가 있다. 즉, 메모리 영역이 실질적인 RAM이 아니라 하드 드라이브의 일부이거나 네트워크 상의 공간일 수 있다는 것인데 이런경우에는 커널이 이런 주소 영역을 uncacheable 로 도장을 쾅 찍어둔다. 당연히 이런 주소 공간을 캐시에 담아둔다는 것은 말이 안된다. 이런 경우에는 길이가 64바이트가 아닐수 있으므로 Length field가 64 바이트가 아닐수 있다는 점..


글의 결론은

1. 퍼포먼스가 중요한 프로그램의 경우에는 같은 캐시 라인 안에 필요한 데이터를 팩킹해서 처리하도록 노력하는 것이 성능향상에 도움이 된다. 캐시에서 바로 가져오는 경우 성능의 향상이 어마어마 하다.

2. 하나의 cache line 안에 포함되는 메모리 엑세싱Atomic 이 보장된다. 원소성이라고 하는데 이것이 좋은 이유는 중간에 다른 스레드가 개입하여 중단하는 일이 없기 때문이다. 즉, 멀티 스레드에도 안전성을 제공한다는 거.

3. FSB는 모든 에이전트에 의해 공유된다. 모든 에이전트는 모든 transaction을 듣고 있어야 하는데 이런 것이 FSB에 교통혼잡을 유발하고 성능을 떨어뜨리는 이유가 되었다. 바로 Core i7에서는 이러한 문제를 해결하기 위해 프로세서가 바로 메모리로 접근할수 있도록 변경했다. 그것이 성능 향상에 큰 도움.






posted by 대갈장군
2016. 2. 6. 05:55 프로그래밍/C++
출처: http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory/

저자: Gustavo Duarte


요즘 메모리 관련 문제를 다루다 보니 점점 더 힙 속으로 빠져들고 있던 차에 좋은 글을 발견하고 내 개인 소장용으로 옮겨 볼까 한다.


일반적으로 멀티 테스킹 OS에서 각각의 프로세스는 그들 자신만의 메모리 샌드 박스에서 실행된다. 즉, 각각의 프로세스가 개별적인 주소 공간을 소유한다는 의미... 이 샌드 박스는 일반적으로 32비트 환경일 경우 Virtual Address Space라 부르고 약 4GB의 메모리 공간이 주어진다. (32비트니까~)


이 가상 메모리 주소 공간은 실질적인 (물질적인) 메모리와 이른바 Page Table이라는 놈을 통해 맵핑 되는데, 뭐, 쉽게 말해서 가상 주소는 말그대로 가상으로 주어진 공간이고 실제 메모리가 할당되려면 맵핑이 필요한데 이 주소록을 가지고 있는 녀석이 Page Table이라는 거지. 재미 있는 점은, 컴퓨터에 돌아가는 모든 프로세스는 이 룰에 따라야 한다는 말이다. 심지어 커널 자신 조차도... 쉽게 생각하면 커널은 예외일 것 같지만 그렇지 않다는 점을 글쓴이는 말하고 싶었던가 보다. 아무튼, 그래서 이 가상 주소 공간은 반드시 커널을 위해 일부를 쓴다는 점이다. (아래 그림처럼)

Kernel/User Memory Split

리눅스는 기본적으로 1GB만 커널이 사용하도록 기본 셋팅이 되지만 윈도우는 2GB나 커널을 위해 할당한다. 즉, 프로세스 자신은 남은 2GB만 사용가능하다는 거네... 이런 위도우 같은 경우를 봤나... 물론 윈도우도 3GB까지 확장하는 옵션이 있단다... 휴..


뭐 이쯤에서 눈치 챘겠지만 각 프로세스가 4GB씩이나 할당하면 나의 구린 컴퓨터는 고작 16GB 램인데 프로세스 4개 돌리면 램 바닥찍고 끝나는가 라는 의문이 스멀스멀 든다면 4 곱하기 4는 16이므로 정답..


물론, 각각의 프로세스가 4GB를 무조건 쓴다는 건 아니라는 점... 시작은 미미하게 해서 끝은 창대해 질수 있겠지만 일반적으로 시작할때는 최소 필요 메모리만 할당해서 최대 4GB까지 쓸수 있다는 점을 이론적으로 보여주는 것일 뿐, 실제로 프로세스가 무조건 4GB 할당하고 출발하는 건 아니라는 점.


다만, 프로그래머가 알아야 할 것은 실수로 커널 메모리 영역을 침범하면 운영체제가 "네 이놈! 여기가 어디라고 감히 들이대느냐!!" 하면서 프로그램을 종료시켜 버린다는 거... 또 한가지 중요한 점은, 커널 코드와 데이터는 항상 어디서나 접근 가능하고 인터럽트를 핸들링 하며 시스템 콜을 받아들일 준비가 되어 있는 반면, 사용자 주소 영역은 프로세스가 변경되면 그에 따라 변화한다는 점... (아래 그림)

Process Switch Effects on Virtual Memory

위 그림에서 사용자 주소 영역에 하늘색 표신 된 부분은 메모리가 할당되어 사용되어진 영역이고 흰색은 할당되지 않은 영역이다. 재미 있는 건, 저자가 Firefox는 메모리를 무지하게 잡아 먹는다면서 저렇게 Firefox의 메모리 할당을 크게 그렸다는 점... 크롬이 더 많이 먹는거 아닌가 몰라...

Flexible Process Address Space Layout In Linux

위 그림을 보면 대략적인 프로세스의 주소공간을 (4GB) 볼 수 있다. 일반적으로 모든게 제대로 잘 작동했을 경우 대부분의 프로그램은 위의 그림과 같은 구조의 메모리 공간을 가지고 시작하게 된다. 사실 이점에 대해서 저자는 보안 취약점이 될 수 있다고 지적하고 있다. 해커가 이런 메모리 구조를 미리 파악하고 대충 찍어서 맞추는 경우 보안상의 헛점이 드러날 수 있다는 점. 그래서 요즘은 메모리를 랜덤하게 유행한단다... 이 글이 써진 시점이 무려 2009년이니... 허허


위 그림에서 사용자 공간의 최상층에 위치하고 있는 것이 바로 Stack인데 이 녀석은 지역 변수를 저장하고 함수의 인자들을 저장하는 용도로 주로 사용된다. 함수를 호출하게 되면 스택영역에 Stack Frame이라는 놈을 넣게 되는데 이 함수가 리턴 될 때 이 Stack Frame도 파괴된다. 스택 자체가 LIFO 방식으로 단순하게 동작하므로 pushing과 popping이 매우 빠르고 효율적으로 수행 가능하다. 그리고 스택 영역을 자주 그리고 많이 사용하면 결국 CPU 캐시에 스택 메모리가 활성화 되어 있게 되므로 스피드를 더더욱 업업업~ 각각의 스레드는 자신만의 스택을 가진다.


만약 사용자가 스택 영역에 할당된 현재의 영역보다 더 큰 데이터를 집어 넣게 되면? 이렇게 되면 (리눅스의 경우) expand_stack()이라는 함수 (결국, acct_stack_growth() 함수 호출하는 녀석)를 호출해서 스택 영역의 확장이 가능한가 체크한다. 만약 스택 영억이 기본 최대 사이즈 (8MB)보다 작다면 스택 영역이 확장된다. 근데, 만약 이 8MB를 다 써버리면? 그때 바로 터지는 것이 Stack Overflow이고 프로그램은 Segmentation Fault를 받으면서 장렬히 전사한다. 또 한가지 흥미로운 점은 스택 영역은 한번 확장되면 스택이 줄어든다고 해서 확장된 영역을 줄이진 않는다. 이것이 마치 미국의 연방 정부의  예산과 같다고... 늘리기만 하고 줄이진 않는다는 이야기.. ㅋㅋ


자, 그 다음은 스택 아래에 위치한 Memory Mapping Segment라는 놈인데 이 놈은 파일의 내용을 직접적으로 메모리로 맵핑해준다. 이런 일을 해주는 대표적인 함수로는 mmap(), CreateFilemapping(), MapViewOfFile() 같은 함수들이다. 이러한 Memory Mapping은 고성능 파일 입출력이나 Dynamic library들을 로딩하기 위해서 사용되어 진다. 그리고 이른바 Anonymous Memory Mapping도 가능한데 이건 임의 메모리 맵핑이라 해야 하겠네.. 파일이나 라이브러리 같은걸 맵핑 하는게 아니라 malloc() 같은 함수가 제법 큰 메모리 공간을 할당하려고 할때 종종 이 구역의 메모리를 임의 메모리 맵핑이라는 형태로 사용하도록 해준다는 군. 메모리를 좀더 효율적이고 많이 사용하기 위해 그런듯...


그다음은 바로 힙! 엉덩이 아니지요.. Heap! 사실 프로그램 돌리다 보면 터지는 메모리 에러의 90% 이상이 힙에서 터진다고 봐도 과언이 아니다. 이 힙 영역은 프로그램이 런타임 (실행중) 중에 메모리를 임의의 크기로 할당할때 사용되는 영역이다. 이러한 동적 메모리 할당 때문에 힙 메모리 사용은 커널과 언어의 런타임이 서로 협상해서 만들어 진다. 대표적인 힙 할당 함수로 C에서는 malloc이 있고 C#이나 C++의 경우에는 new가 있다. 


만약 메모리 공간이 여유가 있다면 언어의 런타임에서 알아서 처리하고 끝낸다. 만약 현재의 할당된 힙으로 공간이 부족하다면 시스템 콜 (brk())을 통해 확장되는데 이것이 바로 커널의 개입이다. 사실 힙의 관리는 정말 복잡한데 머리 좋고 똑똑한 사람들이 최선을 다해 알아서 잘 관리하도록 만들어 놨다고... 뭐, 아무튼...


그리고 마지막으로 제일 아래 영역이 보이는데 BSS, data 그리고 program text다. BSS와 data store의 경우 static global variable들을 저장하는 용도로 사용된다. BSS는 할당되지 않은 (uninitialized) static variable들을 저장하고 data segment의 경우에는 initialized된 static variable들을 저장한다. 


뭐 몇가지 설명이 더 있기는 한데 그렇게 필요해 보이진 않아서 스킵~ 




posted by 대갈장군
2015. 2. 12. 04:06 프로그래밍/C

Source: http://phoxis.org/2011/04/27/c-language-constructors-and-destructors-with-gcc/


Constructors and Destructors are special functions. These are one of the features provided by an Object Oriented Programming language. Constructors and Destructors are defined inside an object class. When an object is instantiated, ie. defined of or dynamically allocated of that class type, the Constructor function of that class is executed automatically. There might be many constructors of which the correct implementation is automatically selected by the compiler. When this object is destroyed or deallocated, the Destructor function is automatically executed. For example when the scope of the object has finished or the object was dynamically allocated and now being freed. The Constructors and the Destructors are generally contains initialization and cleanup codes respectively required by an object to operate correctly. Because these functions are automatically invoked by the compiler therefore the programmer freed from the headache of calling them manually.

There is no such thing called ‘constructors’ and ‘destructors’ in C programming language or in structured languages, although there is no boundaries on defining such functions which act like them. You need to make functions which act like the constructors and destructors and then call them manually.

The GCC constructor and destructor attributes

GCC has attributes with which you can tell the compiler about how a lot of things should be handled by the compiler. Among such attributes the below function attributes are used to define constructors and destructors in C language. These would only work under GCC. As there is no objects and class approach in C the working of these functions are not like C++ or other OOP language constructor and destructors. With this feature, the functions defined as constructor function would be executed before the function main starts to execute, and the destructor would be executed after the main has finished execution. The GCC function attributes to define constructors and destructors are as follows:

1
2
3
4
__attribute__((constructor))
__attribute__((destructor))
__attribute__((constructor (PRIORITY)))
__attribute__((destructor (PRIORITY)))

For example, to declare a function named begin () as a constructor, and end () as a destructor, you need to tell gcc about these functions through the following declaration.

1
2
void begin (void) __attribute__((constructor));
void end (void) __attribute__((destructor));

An alternate way to flag a function as a C constructor or destructor can also be done at the time of the function definition.

1
2
3
4
5
6
7
8
__attribute__((constructor)) void begin (void)
{
 /* Function Body */
}
__attribute__((destructor)) void end (void)
{
 /* Function Body */
}

After declaring the functions as constructors and destructors as above, gcc will automatically call begin () before calling main () and call end () after leaving main or after the execution of exit () function. The following sample code demonstrates the feature.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
 
void begin (void) __attribute__((constructor));
void end (void) __attribute__((destructor));
 
int main (void)
{
  printf ("\nInside main ()");
}
 
void begin (void)
{
  printf ("\nIn begin ()");
}
 
void end (void)
{
  printf ("\nIn end ()\n");
}

Execution of this code will come up with an output which clearly shows how the functions were executed.

1
2
3
In begin ()
Inside main ()
In end ()

Multiple Constructors and Destructors

Multiple constructors and destructors can be defined and can be automatically executed depending upon their priority. In this case the syntax is __attribute__((constructor (PRIORITY))) and __attribute__((destructor (PRIORITY))). In this case the function prototypes would look like.

1
2
3
4
5
6
7
8
void begin_0 (void) __attribute__((constructor (101)));
void end_0 (void) __attribute__((destructor (101)));
 
void begin_1 (void) __attribute__((constructor (102)));
void end_1 (void) __attribute__((destructor (102)));
 
void begin_2 (void) __attribute__((constructor (103)));
void end_2 (void) __attribute__((destructor (103)));

The constructors with lower priority value would be executed first. The destructors withhigher priority value would be executed first. So the constructors would be called in the sequence: begin_0begin_1 ()begin_2 () . and the destructors are called in the sequence end_2 ()end_1 ()end_0 (). Note the LIFO execution sequence of the constructors and destructors depending on the priority values.

The sample code below demonstrates this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
 
void begin_0 (void) __attribute__((constructor (101)));
void end_0 (void) __attribute__((destructor (101)));
 
void begin_1 (void) __attribute__((constructor (102)));
void end_1 (void) __attribute__((destructor (102)));
 
void begin_2 (void) __attribute__((constructor (103)));
void end_2 (void) __attribute__((destructor (103)));
 
int main (void)
{
  printf ("\nInside main ()");
}
 
void begin_0 (void)
{
  printf ("\nIn begin_0 ()");
}
 
void end_0 (void)
{
  printf ("\nIn end_0 ()");
}
 
void begin_1 (void)
{
  printf ("\nIn begin_1 ()");
}
 
void end_1 (void)
{
  printf ("\nIn end_1 ()");
}
 
void begin_2 (void)
{
  printf ("\nIn begin_2 ()");
}
 
void end_2 (void)
{
  printf ("\nIn end_2 ()");
}

The output is as below:

1
2
3
4
5
6
7
In begin_0 ()
In begin_1 ()
In begin_2 ()
Inside main ()
In end_2 ()
In end_1 ()
In end_0 ()

Note that, when compiling with priority values between 0 and 100 (inclusive), gcc would throw you warnings that the priority values from 0 to 100 are reserved for implementation, so these values might be used internally that we might not know. So it is better to use values out of this range. The value of the priority does not depend, instead the relative values of the priority is the determinant of the sequence of execution.

Note that the function main () is not the first function/code block to execute in your code there are a lot of code already executed before main starts to execute. The function main is the user’s code entry point, but the program entry point is not the main function. There is a startup function which prepares the environment for the execution. The startup functions first call the functions declared as constructors and then calls the main. When main returns the control to the startup function it then calls those functions which you have declared as the destructors. There are separate sections in the executable .ctors and .dtors which hold these functions. (not discussed here).

As there is no class object creation in C language does not have such features like in C++ or other OOP languages but this feature can bring some flexibility by calling the functions automatically on execution and termination of the code which you needed to do inside the main. For example one use may be like, you have a dynamically allocated global variable, might point to a linked list head or an array, or a file descriptor which you can allocate inside a constructor. If some error is encountered you can immediately call exit () or the program terminates normally, depending on the error code you can make cleanup in the destructors.

Another good use is probably calling the initialization functions of some library, a bunch of which needs to be called in each program. You can make a separate file with the constructor files calling properly the library functions (probably operating on globals) and calling the library cleanup functions in the destructors. Whenever you make a program what you need to do is to compile your code with this file containing the constructors and destructors and forget about calling the initialization and cleanup functions in the main program. But doing this will make your code unportable, as it would only work under GCC.

결론:

__attribute__ 속성을 사용해서 C에서도 생성자와 파괴자 역활을 하는 함수를 선언하여 사용할 수 있다. main 함수가 호출되기 전에 자동으로 GCC가 __attribute__ 로 선언된 생성자 함수를 호출해 주고 main이 끝나는 곳에서 다시 __attribute__로 선언된 파괴자 함수를 호출하여 마무리.


장점:

1. 이런식으로 C에서 C++의 생성자 / 파괴자 흉내를 낼 수 있다

2. 사용 예로는 동적으로 선언되는 전역 변수를 할당 및 해제를 원큐에 해결 가능 (메모리 해제 걱정 NO!)

3. 에러를 만날 경우 즉각 exit를 호출하여도 자동으로 파괴자가 호출되므로 자원 해제가 용이함

4. 여러개의 프로그램에서 같은 라이브러리를 사용하는 경우, 생성자로 main 진입 전에 라이브러리를 초기화 시켜 주고 파괴자에서 해제해 주면 아주 굿!


단점:

GCC에서만 작동하므로 소스 코드가 portable이 아니다. 뭐, 그거야 당연한 거지만..


'프로그래밍 > C' 카테고리의 다른 글

C Standard Library  (0) 2010.09.25
싱글 스레드에서 멀티 스레드로... 왜 /MD 인가?  (0) 2010.03.11
호출 규약  (0) 2009.07.28
posted by 대갈장군
2014. 8. 28. 04:47 프로그래밍/C++

출처: http://ikpil.com/540


내가 C++에 조예가 깊어서 글을 남기는 것이 아니라, Effecitve C++ 을 공부하는 사람들이 이 글을 보고, 도움이 되었으면 하는 생각과, 혹시 내가 틀린것이 있다면 지적해 주시지 않을까 란 생각으로 글을 올리는것임을 미리 밝힙니다.  - 최익필


typename .. 뭐 이렇게만 보면 이런게 있었나 싶다. 하지만 템플릿에서는 흔하게 보는 키워드인데, 이 키워드에 대해서 제대로 파악하자고 하는것 같다.

처음부터 진행하자면 typename 과 class 는 똑같은 의미인데, 이렇게 말만 하면 혼동의 요지가 있으니, 코드도 포함해 주는 센스를 발휘해 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ikpil.com or ikpil.tistory.com
 
#include <iostream>
//template <typename _T>  // class _T 와 같은 의미
template <class _T>
class babo
{
typedef _T value_type;
public:
    void Draw( void )
    {
        std::cout << sizeof(value_type) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
};
 
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<int> Test;
     
    // 뭐~ 출력
    Test.Draw();
 
    return 0;
}


위의 주석을 다 읽어 보면 무슨 말인지 알듯 싶다. 하지만 스콧마이어어가 쓰고, 곽용재씨가 번역한 이번 항목에선 typename 은 또 다른 의미를 지니고 있다고 알려 주려고 하는듯 하다.

그렇다 typename 키워드는 .. ... 은 타입이다! 를 알려 주는 용도로 사용 한다. 요렇게만 들으면 또 .. 햇갈린다. 더 이야기 하자면 typename 키워는 템플릿 선언 내부에서만 사용 할수 있다는 것을 기억해야 한다. 아래 소스코드를 첨부 하겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// ikpil.com or ikpil.tistory.com
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<int> Test;
     
    typename int a; // 컴파일 에러를 볼수 있을 것이다.
 
    // 뭐~ 출력
    Test.Draw();
 
    return 0;
}


그렇다면 이것은 템플릿 내부에서 써보자면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ikpil.com or ikpil.tistory.com
 
#include <iostream>
template <typename _T>    // class _T 와 같은 의미
class babo
{
typedef _T value_type;
public:
    void Draw( void )
    {
        std::cout << sizeof(value_type) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
public:
    typename int hehehe;    // 이렇게 쓸수 있다.
};


이렇게 쓸수 있다는 것인데, 이것만 본다면, 
1. int 는 타입이다
2. 그 타임으로 a 객체 생성!

.. 이것을 보고 비웃지 않는다면, 당신이야 말로 성인(成人)이다. 왜냐하면 int 는 누가봐도 타입인데 굳이 typename 을 피곤하게 붙인다는 것에 대해서 말이다.

그렇다면 다음 코드는 어떻게 이해 할 것인가.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ikpil.com or ikpil.tistory.com
#include <iostream>
#include <vector>
 
template <typename _T>    // class _T 와 같은 의미
class babo
{
public:
    void Draw( void )
    {
        std::cout << sizeof(p) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
public:
    _T::iterator muhaha; // 이것 때문에 컴파일이 되지 않는다.
};
 
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<std::vector<int> > Test;
     
    // 뭐~ 출력
    Test.Draw();   
 
    return 0;
}


.. 물론 나는 _T의 타입인 iterator 란것을 분명이 알고 있다. 하지만 컴파일러는 다르다. 어떤 초특급 울트라 천재가 컴파일러를 만든다 하더라도, .. 저것이 "타입" 인것을 .. 알려면 몇달 몇일 간 별 수를 다 생각해야 할것이다. 

_T::iterator muhaha; 이것을
typename _T::iterator muhaha; 라고 바꾸면 정상적으로 컴파일이 되는 것을 볼수 있을것이다. 그렇다! typename은 템플릿 내부에서 사용하면서, 컴파일러에게 템플릿 내부에서 "이건 타입이야" 라고 알릴 필요가 있을 때 써준다.


이쯤에서 용어 설명을 하는게 좋을듯 싶은데,
_T 처럼 타입을 인자로 받아 그것을 타입으로 쓰기 때문에 _T를 의존 이름(dependent name) 이라 외국에서 불린다.요~런 사항에서 의존 이름(dependent name)속에 또 다시 다른 이름이 정의된 경우 그녀석은 중첩 의존 타입 이름(nested dependent type name .. 뭐 외국 따라가야지, .. 태생이 외국인인데..)이라고 불리는데,

바로 템플릿 내부에서 중첩 의존 타입 이름(nested dependent type name)이 있을 경우, 그녀석도 타입인것을 인지하지 못하는 사태가 발생 할 수 있다.(발생 할수 있다는 이유는 MSVC2005에선 발생하지만, 다른 컴파일러에선 발생 안할수도 있다는 뜻이다. 뭐.. 대부분은 발생한다.)

여담으로 int 같은 것들은 비의존 이름(non-dependent name)이라고 불린다. .. 뭐 그런게 있다~ 라고 알아두면 좋을듯 싶고.. 여기서 한가지 짚고 넘어가야 한다. 이런 typename 을 쓸때, template 내부여도 못쓰는 경우가 두가지 있다는 것이다!

이 경우는 중첩 의존 타입 이름(nested dependent type name)이 기본 클래스의 리스트에 있거나, 멤버 초기화 리스트 내의 기본 클래스 식별자로 있을경우 typename을 붙여 주면 안된다는 것이다!(사실 이 구역은 컴파일러가 알아서 이녀석은 타입이다! 라고 알수 있는 곳이기도 하다.) 이렇게만 말하면 다시 .. 난해 할 수 있으니 코드를 달아 주는 센스!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// ikpil.com or ikpil.tistory.com
// typename 을 예외적으로 사용 할수 없는
// 구역을 나타내기 위해서 이렇게 코드를 만든다.
 
#include <iostream>
#include <vector>
 
template <typename _T>
class NON_TYPENAME
{
public:
    // 뭐 클래스 네임만 신경쓰고 나머진
    // 무시해도 된다.
    NON_TYPENAME() { }
    // 소멸자
    ~NON_TYPENAME() { }
private:
    _T m_p;
};
 
 
template <typename _T>    // class _T 와 같은 의미
class babo
    : public NON_TYPENAME<_T> // <-- 여기가 기본 클래스의 리스트
{
public:
    babo()
        : NON_TYPENAME<_T>() // <-- 여기가 멤버 초기화 리스트
    {
 
    }
    void Draw( void )
    {
        std::cout << sizeof(p) << "의 용량을 가진 클래스" <<std::endl;
    }
private:
    _T *p;
public:
    typename _T::iterator muhaha;   // <-- 여긴 사용 할수 있다~
};
 
int main( void )
{
    // int 타입을 가진 babo 클래스의 객체 Test 선언
    babo<std::vector<int> > Test;
     
    // 뭐~ 출력
    Test.Draw();   
 
    return 0;
}


자.. 이제 이해가 되었는가? 여기에 코딩 스타일이 좀더 편해질만한 이야기를 하나 더 들려 준다.
바로 typename _T::iterator muhaha; 를 쓸때 한번이나 두번 쯤이야 써주겠지만 3 이상 넘어가면 .. 슬슬 피곤해 진다. 이럴 때는 typedef typename _T::iterator iterator; 라고 쓰고 iterator muhaha; 라고 쓰면. 뭐 쉽지 않니한가? ㅋㅋ


여담으로 몇가지 더 이야기를 한다.
이 이야기는 곽용재씨가 들려주는 이야기로 스콧 마이어스와 허브서터, 안드레 알렉산드레스쿠는 사용자가 정의한 타입에 대해서는 class 를 쓰고 기본 제공 타입에 대해서는 typename 을 쓴다. 그리고 비야네 스트롭스트룹은 class 를 주로 쓰는 편이라고 한다;

이렇게 굳이 꼭 정해 둘 필요가 있을까? 하는 사람도 있지만, 코드가 몇만줄 넘어가면, 조금이라도 코드에 정보 아닌 정보를 남겨두는 편이, 디버깅이나 유지보수 등에 도움이 될때가 많다. 이런 습관도 있으니 배워두자는 취지에서 곽용재씨가 남겨 주셧던것 같다.


이것만은 잊지 말자!
1. 템플릿 매개변수를 선언할 때, class 나 typename 이나 똑같다.
2. 중첩 의존 타입 이름(nested dependent type name)을 식별하는 용도는 typename 키워드이다.
3. typename 은 템플릿 내부에서만 사용 되고, 초기화리스트 및 기본 클래스 리스트에서는 못쓴다!


관련링크
http://www.ibm.com ... typename keword <-- ibm의 typename 설명서 아, 영문이다 제길
http://pages.cs.wisc.edu/~driscoll/typename.html <-- 여기도 외국 . typename 설명서 .. 영문이다.
http://keiphoto.egloos.com/4182181 <-- 짤막한 .. 영문으로.
http://kukuta.tistory.com/12 <-- 오오! 괜찮은 설명~ 아 여긴 한국
http://codefactory.zc.bz/50 <-- 댓글이 설명이 좋았음
http://rein.upnl.org/wordpress/archives/494 <-- typename 관련 글은 아니지만, 템플린 관련해서 좋은 글
http://www.filewiki.net/tc/34 <-- typename 을 어떤 식으로 사용하는지 소스로 확인 할수 있음
http://dblab.co.kr/119 <-- 역시 마찬가지
http://kldp.org/node/62341 <-- typename 문법 없이 사용할 경우, 질문 내용
http://kldp.org/node/39641 <-- 역시 질답 내요이 좋음

posted by 대갈장군
2014. 8. 7. 22:57 프로그래밍

출처: http://www.cplusplus.com/articles/Gw6AC542/


 1> 왜 우리는 헤더 파일이 필요한가? 


만약 당신이 C++ 처음 접하는 사람이라면 프로그램이 왜 #include 를 하는지와 여러개의 .cpp 파일이 필요한 지 궁금할 것이다. 이유는 다음과 같다.


  1. Compile 속도를 높인다. 당신의 프로그램이 커지면 커질수록 코드의 양도 많아진다. 만약 모든 것이 하나의 파일에 다 들어간다면 작은 변경에도 큰 파일 전체가 컴파일 되어야 하므로 시간이 오래 걸린다. 물론 작은 프로그램의 경우에는 이거 별거 아니지만 적당한 규모 이상일 경우에는 컴파일 타임을 무시 할 수 없다.
  2. 코드가 보다 잘 정렬된다. (Organized) 만약 내가 코드의 각 부분을 컨셉에 따라 잘 나누어 놓으면 어떤 파일을 변경해야 하는지 손 쉽게 알 수 있으므로 잘 정리된 프로그램은 수정이 용이 하다.
  3. interface를 implementation으로 부터 분리 할 수 있다. 즉, 헤더에 프로그램의 interface를 정의해 주고 cpp 코드에 implementation을 하면 아주 깔끔하다.
C++ 프로그램들은 두 단계를 거쳐 생성된다. 첫번째 단계는 각각의 소스 파일들이 각각 컴파일 되는 것이다. 컴파일러는 각각의 컴파일된 소스파일에 대한 intermediate file들을 생성해 낸다. 이 중간 단계의 파일들을 우리는 object file이라고 부른다. 그리고 일단 모든 파일들이 각각 컴파일 된 후에는 모든 object 파일들을 링크하게 된다. 바로 이때 Binary 파일 (exe 파일)이 생성된다.


자, 좀 더 자세히 설명하자면, 각각의 소스 파일이 다른 소스 파일과는 완전히 별개로 컴파일 된다. 이말인 즉슨, a.cpp라는 소스 파일은 b.cpp라는 소스 파일 안에 무슨 일이 벌어지고 있는지 전~혀 모르고 있다는 말이다. 예를 들자면,


// in myclass.cpp

class MyClass
{
public:
  void foo();
  int bar;
};

void MyClass::foo()
{
  // do stuff
}


// in main.cpp

int main()
{
  MyClass a; // Compiler error: 'MyClass' is unidentified
  return 0;
}



위 두 파일의 경우 분명히 MyClass가 myclass.cpp에 선언되었지만 main.cpp에서는 이 클래스에 대해서 알수 있는 길이 없다는 것. 즉, 에러가 난다. 너무나도 당연한 에러다.


여기서 헤더파일의 역활이 드러난다. 헤더 파일은 다른 .cpp 파일들에게 interface를 공개하는 역활을 한다. 물론 implementation은 cpp파일에 남겨두고 오직 interface만 공개하는 것이 헤더 파일의 임무이다. 위의 경우를 예를 들자면 헤더 파일은 다음과 같아야 한다.


// in myclass.h

class MyClass
{
public:
  void foo();
  int bar;
};


// in myclass.cpp
#include "myclass.h"

void MyClass::foo()
{
}


//in main.cpp
#include "myclass.h"  // defines MyClass

int main()
{
  MyClass a; // no longer produces an error, because MyClass is defined
  return 0;
}


자, 이제 main.cpp에서 MyClass 클래스에 대해서 구조를 알 수 있으므로 컴파일에 에러가 나지 않는다.



 2> 확장자에 따른 차이점은?  


그렇다면 여기서 궁금한 것 한가지, .h / .cpp / .hpp / .cc 등등과 같은 서로 다른 확장자의 파일들은 무슨 차이가 있는가?


생각보다 룰이 간단한데, 모든 .h__ 파일들은 걍 헤더 파일이다.

모든 .c__ 파일들은 모두 C++ 소스 코드들이다. 무슨 확장자던 관계없다.

C 코드는 반드시 .c 파일이어야 한다. (요거만 예외적이네)


  3> Include Guards


C++ 컴파일러는 뇌가 없다. 우리가 시키는 대로만 하는 것이 컴파일러이다. 고로 헤더를 두번 include 시키게 되면 이미 정의된 놈이라는 어이없는 에러가 마구마구 터질 것이다. 아마도 어떤 바보가 이렇게 하겠느냐고 질문하겠지만 이런 경우는 의외로 빈번하게 발생한다. 예를 들자면 다음과 같은 경우이다.


1
2
// x.h
class X { };


1
2
3
4
// a.h
#include "x.h"

class A { X x; };


1
2
3
4
// b.h
#include "x.h"

class B { X x; };


1
2
3
4
// main.cpp

#include "a.h"  // also includes "x.h"
#include "b.h"  // includes x.h again!  ERROR 


a.h와 b.h가 둘다 x.h를 include 하는 상황에서 main.cpp에서 a.h와 b.h를 두 번 include 하게 되면 x.h가 두 번 불러지는 셈이된다. 고로 에러...


종종 어떤 사람들은 헤더 파일에서는 include를 하지 말라는 터무니 없는 말을 하는데 이것은 잘못된 지식이다. 헤더에 무슨 파일을 포함하던지 상관이 없다. 단, 두 가지 조건을 만족해야 한다.


1. 반드시 필요한 헤더만 include하라

2. include guards를 통해서 multiple include를 미연에 방지하라


여기서 처음 보는 단어의 등장. Include Guards라는 것인데, 이것은 다음과 같은 것이다.


1
2
3
4
5
6
7
8
//x.h

#ifndef __X_H_INCLUDED__   // if x.h hasn't been included yet...
#define __X_H_INCLUDED__   //   #define this so the compiler knows it has been included

class X { };

#endif 


자, 이렇게 헤더를 둘러쌓으면 x.h가 두번째 include 될때 __X_H_INCLUDED__가 이미 정의되었으므로 헤더가 통째로 스킵되어 버린다. 고로 에러가 없이 지나간다. 고로, 무조건 include guard를 해라, 모든 헤더파일에 해라. 해도 무해하다... 해라 해라 또 해라.



  4> 올바른 include 방법


내가 만드는 클래스는 종종 다른 클래스들에게 의존적인 경우가 많이 있을 것이다. 예를 들자면 파생 클래스의 경우 부모 클래스를 알고 있어야 한다. 부모가 있어야 자식이 나오기 때문이지.


일단 두 가지 종류의 Dependency 가 있는데 다음과 같다.

1. Forward declare가 가능한 것

2. #include가 되어야만 하는 것


자, 예를 들어 Class A가 Class B를 사용하고 있다면 Class B는 Class A의 Dependency 중 한 개다. 그렇다면 Class A 내부에서 B를 어떻게 사용하는 가에 따라 forward declared 될 것인지 아니면 included 되어야 할지 분류가 가능하다. 다음과 같은 룰을 따르면 된다.


1. 만약 A가 B에게로 어떤 reference 도 만들지 않았을 경우: 아무것도 할 필요 없음

2. 만약 B에게 향하는 유일한 reference 가 friend declaration인 경우: 아무것도 할 필요 없음

3. 만약 A가 B 포인터 혹은 레퍼런스를 포함한 경우: Forward Declare

4. 만약 하나 혹은 하나 이상의 함수가 B의 object/pointer/reference를 입력 변수로 가진 경우 (또는 리턴 타입으로 가지는 경우): Forward Declare

5. 만약 B가 A의 부모 클래스라면: #include "b.h"

6. 만약 A가 B의 객체를 포함한다면: #include "b.h"


자, 말로 설명하니 좀 이해가 안된다. 코드로 보면, 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//=================================
// include guard
#ifndef __MYCLASS_H_INCLUDED__
#define __MYCLASS_H_INCLUDED__

//=================================
// forward declared dependencies
class Foo;
class Bar;

//=================================
// included dependencies
#include <vector>
#include "parent.h"

//=================================
// the actual class
class MyClass : public Parent  // Parent object, so #include "parent.h"
{
public:
  std::vector<int> avector;    // vector object, so #include <vector>
  Foo* foo;                    // Foo pointer, so forward declare Foo
  void Func(Bar& bar);         // Bar reference, so forward declare Bar

  friend class MyFriend;       // friend declaration is not a dependency
                               //   don't do anything about MyFriend
};

#endif // __MYCLASS_H_INCLUDED__ 


위에서 설명한 모든 경우에 대해서 잘 보여주고 있다. Forward Declare는 헤더 시작 부분에 먼저 정의 하라는 의미네. 여기서 명심해야 할 점은, Foo class의 경우 MyClass 본체에서 포인터로만 사용되고 있기 때문에 include를 할 필요가 없고 다만 forward declare만 해주면 된다는 점. 최대한 include를 최소화 하는 방향으로 가는 것이 가장 올바른 코딩이다. 무분별한 include는 문제를 야기 할 수 있다는 점 명심하란다.



  5> 왜 위의 방법이 '올바른' 방법인가?


그렇다면 왜 필자가 말하는 방법이 이른바 '올바른' include 방법인가? 일단 해답은 Object Oriented에 있다. 필자가 이루고자 하는 기본적인 것은 myclass.h 파일이 그 자체로 self-contained, 즉, 완전체가 되기 위해서는 위와 같은 법칙을 따라야 한다는 것이다. 만약 다른 소스 파일에서 myclass를 써야 한다면 #include "myclass.h" 단 한줄로 모든것을 끝내버릴 수 있다. 


반면 헤더 파일안에 헤더 파일을 include 하지 않는 원칙을 따르게 되면 다음과 같은 문제가 생긴다. myclass를 사용하려면 myclass가 필요로 하는 모든 헤더 파일을 먼저 추가해야 하는 문제가 발생한다. 다음과 같은 코드를 보자.

1
2
3
4
5
6
7
//example.cpp

//  I want to use MyClass
#include "myclass.h"   // will always work, no matter what MyClass looks like.
                       // You're done
               //  (provided myclass.h follows my outline above and does
               //   not make unnecessary #includes) 


위의 코드를 보면 myclass.h를 포함시켰는데 만약 필자가 제시한 방법대로 했다면 다른 어떤 파일도 추가로 필요하지 않지만 만약 그렇지 않은 경우라면 다음과 같은 이유로 에러가 빠빡 터진다.


Here is an example of why so-and-so's method is bad:

1
2
3
4
5
//example.cpp

//  I want to use MyClass
#include "myclass.h"
   // ERROR 'Parent' undefined 



so-and-so: "Hrm... okay...."

1
2
3
#include "parent.h"
#include "myclass.h"
   // ERROR 'std::vector' undefined 



1
2
3
4
#include "parent.h"
#include <vector>
#include "myclass.h"
   // ERROR 'Support' undefined 



so-and-so: "WTF? MyClass doesn't even use Support! But alright..."

1
2
3
4
5
#include "parent.h"
#include <vector>
#include "support.h"
#include "myclass.h"
   // ERROR 'Support' undefined 


제일 마지막 줄에 Support가 없어서 에러가 터지는 이유는 parent.h에서 support 클래스를 사용하고 있기 때문이란다. 고로 support.h가 parent.h 보다 앞에 include되어야 에러가 안난다. 자, 고로 문제는 헤더 파일에 다른 헤더 파일을 포함하지 않는 경우, 하나의 클래스를 사용하기 위해 해당 클래스가 사용하는 모든 헤더파일을 먼저 불러와야 한다는 단점과 더불어 헤더 파일의 순서 또한 중요하다는 것을 알 수 있다. 이런 문제는 사실 비일비재 하게 발생하고 있다.



  6> Circular Dependencies


1
2
3
4
// a.h -- assume it's guarded
#include "b.h"

class A { B* b; };


1
2
3
4
// b.h -- assume it's guarded
#include "a.h"

class B { A* a };


상호 의존이라 함은 위의 예제 코드 처럼 서로가 서로의 클래스르 사용하는 경우를 말한다. 그냥 보기에는 문제가 없어 보이지만 실제로 컴파일러의 흐름을 따라가보면 아래와 같은 문제가 발생한다.


1
2
// a.cpp
#include "a.h" 


The compiler will do the following:

1
2
3
4
5
6
7
8
9
10
11
12
#include "a.h"

   // start compiling a.h
   #include "b.h"

      // start compiling b.h
      #include "a.h"

         // compilation of a.h skipped because it's guarded

      // resume compiling b.h
      class B { A* a };        // <--- ERROR, A is undeclared 


자 위 코드를 차례대로 따라가 보면, a.cpp를 컴파일 시작하면 우선 a.h 헤더 파일이 선두이므로 a.h를 컴파일 하기 시작하는데 a.h의 선두에는 b.h가 선언되므로 b.h를 컴파일 하려고 하는데 다시 b.h의 선두에 a.h가 선언 되므로 a.h를 컴파일 하려고 하자, include guard가 되어버린 경우, a.h를 스킵하고 지나가버리게 된다. 고로 A라는 클래스를 알 수 없다는 에러가 터진다. 이것이 circular include의 문제점이다. 


이것을 방지하려면 앞서 살펴본 원칙대로 forward declare를 하면 된다. 왜냐면 reference 타입만 사용된 경우에는 forward declare를 통해 이런 circular include의 연결 고리를 끊어줄 수 있기 때문이다.


고로 만약 두 헤더 파일이 서로의 클래스를 직접적인 객체로 사용하는 경우에는 이 circular include 문제를 해결할 수 가 없다. 


1
2
3
4
5
6
7
8
// a.h (guarded)

#include "b.h"

class A
{
  B b;   // B is an object, can't be forward declared
};


1
2
3
4
5
6
7
8
// b.h (guarded)

#include "a.h"

class B
{
  A a;   // A is an object, can't be forward declared
};


헌데 잘 보면 이러한 경우는 구조 자체가 설계가 잘못 된 경우다. 이런것은 infinite recursion을 발생시키게 된다. 고로 올바른 형태는 다른 클래스의 포인터를 사용하는 것이 올바르다. Forward declare가 바로 이런 이유로 사용되는 것이다.



 7> 인라인 함수 


1
2
3
4
5
6
7
8
9
10
class B
{
public:
  void Func(const A& a)   // parameter, so forward declare is okay
  {
    a.DoSomething();      // but now that we've dereferenced it, it
                          //  becomes an #include dependency
               // = we now have a potential circular inclusion
  }
};


자, 위의 코드를 보면 인라인 함수가 circular inclusion 문제를 야기할 수 있음을 알 수 있다. 입력 변수로 선언된 시점 까지는 forward declare가 가능하지만 DoSomething()이라는 함수를 호출하는 순간 객체가 구현되므로 반드시 #include를 해야하는 문제가 발생한다.


고로 이것을 고치려면 아래와 같이 바꾸면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// b.h  (assume its guarded)

//------------------
class A;  // forward declared dependency

//------------------
class B
{
public:
  void Func(const A& a);  // okay, A is forward declared
};

//------------------
#include "a.h"        // A is now an include dependency

inline void B::Func(const A& a)
{
  a.DoSomething();    // okay!  a.h has been included
}


단순히 봐서 이게 뭐가 다르지 라는 생각이 들것이다. 헤더 파일을 인라인 함수 앞에 가져다 놓고 인라인 함수를 클래스 선언에서 분리했다는 점 외에는 차이점이 없다. 그런데... 이것은 완벽하게 안전하다. 왜냐면 Class B가 완전히 정의된 후에 a.h가 include 되었기 떄문이란다. 흠.


그런데 헤더 파일 안에 함수가 구현되어 있는게 보기 싫은 분들은 다음과 같이 해도 된다. 


1
2
3
4
5
6
7
// b.h

    // blah blah

class B { /* blah blah */ };

#include "b_inline.h"  // or I sometimes use "b.hpp" 


1
2
3
4
5
6
7
8
9
10
11
// b_inline.h (or b.hpp -- whatever)

#include "a.h"
#include "b.h"  // not necessary, but harmless
                //  you can do this to make this "feel" like a source
                //  file, even though it isn't

inline void B::Func(const A& a)
{
  a.DoSomething();
}


인라인 함수 부분은 .hpp 파일로 분리해 놨다. 이제 보니 hpp가 왜 hpp인지 알겠다. 헤더 파일이지만 코드를 구현하고 있기 때문에 pp를 붙여 놓은 것이다.



 8> Forward Declaring Templates 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// a.h

// included dependencies
#include "b.h"

// the class template
template <typename T>
class Tem
{
 /*...*/
  B b;
};

// class most commonly used with 'int'
typedef Tem<int> A;  // typedef'd as 'A' 


1
2
3
4
5
6
7
8
9
10
11
// b.h

// forward declared dependencies
class A;  // error!

// the class
class B
{
 /* ... */
  A* ptr;
};


사실 forward declare은 굉장히 간단한 것이다. 헌데 만약 그것이 template class에 대한 것이라면 문제가 좀 복잡해 진다. 위의 예를 보면 알수 있듯이 template 클래스 Tem이라는 것이 B 클래스를 사용하고 있다. 고로 b.h를 반드시 include 해야 하고, typedef을 통해 A라는 클래스를 Tem<int> 클래스로 정의했다.


그리고 b.h에서 A 클래스를 forward declare 하려고 하면 에러가 빡 터진다.. 왜?


왜냐면 A는 클래스가 이니라 사실 typedef이기 때문이다. 문제는 a.h를 include 할 수 없는데 왜냐면 circular inclusion 문제가 발생하기 때문이다. 그러면 어떻게 해결하느냐?


a.h에서 해준 것 처럼 똑같이 어떤 템플릿 클래스를 typedef 했는지 고대로 알려주면 A라는 클래스를 forward declare 가능하다. 즉, 아래와 같이 친절하게 알려주어야 한다.


1
2
template <typename T> class Tem;  // forward declare our template
typedef Tem<int> A;               // then typedef 'A' 


근데 이건 단순히 class A라는 한 줄에 비해 참 보기 싫다. 그래서 실용적인 해결 방법은 이러한 템플릿 클래스의 typedef를 따로 헤도로 선언하여 포함시키는 것이다. 아래와 같이...


1
2
3
4
5
6
7
8
9
10
//a.h

#include "b.h"

template <typename T>
class Tem
{
 /*...*/
  B b;
};


1
2
3
4
//a_fwd.h

template <typename T> class Tem;
typedef Tem<int> A;


1
2
3
4
5
6
7
8
9
//b.h

#include "a_fwd.h"

class B
{
 /*...*/
  A* ptr;
};


이런식으로 템플릿 클래스의 typedef 또한 forward declare가 가능하다는 점~






posted by 대갈장군
2013. 9. 27. 03:45 프로그래밍/MSDN

http://msdn.microsoft.com/en-us/library/ms953320.aspx


Introduction

Visual Studio 2005에서 새로 추가된 ClickOnce라고 불리는 새로운 윈도우 폼 배포 방식이 있다. 이 방식은 어플리케이션의 설치와 웹 어플리케이션의 업그레이드를 스마트 클라이언트로 가능케 해주는 좋은 친구다. 이 글은 윈도우 폼의 장점과 ClickOnce라고 불리는 녀석에 대해서 자세하고 세세하게 살펴볼 예정이란다.


왜 윈도우 폼인가?

여기서 말하는 윈도우 폼이라 함은 .NET 을 기반으로 작성된 윈도우 폼 어플리케이션을 말한다. 뭐 대표적인 예로는 C#으로 작성된 윈폼이 있겠네. 웹 어플리케이션이 등장한 후로 사람들이 이 웹 어플리케이션에 지대한 관심을 보이는데 그 이유는 단지 '쿨'하다는 이유 말고도 다음과 같은 장점들이 있다.


  • 웹 어플리케이션은 세계 어디서나 언제든지 인터넷 커넥션만 있으면 접근 가능, 또한 실행 가능하다. 심지어는 클라이언트가 윈도우를 운영체제로 사용하지 않아도 상관이 없다. 유비쿼터스 환경을 추구하는 어플리케이션이라면 웹 어플리케이션이 짱!
  • 웹 어플리케이션은 배포와 업뎃이 쉽다. 걍 설치 파일을 서버에 업로드만 해놓으면 사용자들이 설치하면 끄읏! DLL 걱정도 없고 레지스트리 걱정도 없고 COM 클래스 등록 걱정조차도 없다. 걍 돌아간다... 이것이야 말로 프로그래머의 퐌타지!
이 글의 초점은 바로 '배포'에 있다. HTTP 프로토콜을 이용한 웹 어플리케이션의 설치 및 업뎃은 당연히 일반적인 윈도우즈 어플리케이션에 비해 장점이 있다.

하지만... 안타깝게도, 장점만 있는 것은 세상에 없으니 단점도 있다. 
  • 우선 유저 인터페이스가 구리다. 웹 어플리케이션은 일반적인 윈도우즈 어플리케이션이 가지는 드래그 드롭이나 우클릭등 이런게 아예 불가능하거나 지원이 매우 매우 힘들다. 
  • 비록 위의 단점을 해결하기 위해 쿨해보이는 유저 인터페이스나 각종 기능을 추가한다고 하더라도 해당 스크립트를 작성하는 게 절라게 어렵다. 이유는 설명안했지만, 아마도 다양한 클라이언트의 경우를 고려하려면 수 많은 경우를 고려해야 겠지?
  • 아시다시피 서버를 사용하는 중앙 집중형 방식이므로 사용자에게 기다릴 줄아는 참을성을 요구한다. 난 힘들겠는데...
  • 화면에 출력하는 프린팅이 오직 화면 출력만으로 제한된다. 
  • 플러그 인이나 ActiveX 컨트로 때문에 위에서 나열한 문제들이 종합되어 나타난다. 뭐, 그렇지만 이 문제점은 웹 어플리케이션이 아니더라도 나타는 문제점이래요. 이놈의 액티브엑스는 내논 자식이구만.
자, 이제 웹 어플리케이션과 윈도우 어플리케이션을 결혼시키면 어떨까? 물론 장점만 모은 자식이 나오기를 원해서다. 재미 있는 사실은 이미 .NET 프레임워크는 초기 버전에서부터 이 둘을 결혼시켜 놓았다는 점. 

이러한 종류의 어플리케션은 intranet이나 extranet 두가지 모두에 대해서 잘 적용될 수 있으며 클라이언트가 .NET 프레임워크와 인터넷 익스플로러를 가지고 있다는 전제하에 원활하고 수월하게 작동한다.

.NET Framework 1.x : HREFing .EXEs
.NET Framework 버전 1.0과 1.1에서 기본 탑제한 기능으로 윈도우 폼 어플리케이션을 HTTP를 통해 배포하기가 있었다. 기본적으로 이것은 HREF 태크를 사용해 Managed 실행파일을 링크로 걸어주는 것이다. 이렇게 하면 사용자는 인터넷 익스플로러를 이용하면 닷넷 프레임워크가 깔려있다는 전제하에서 실행파일을 다운받아서 설치하는 것 뿐만 아니라 필요한 DLL까지 같이 다운받아 준다. 이러한 형태의 배포를 이른바 'hrefing EXEs' 라고 부른다. HREF 헤더는 HTML 공부를 한 사람이라면 누구나 아는 이른바 '링크' 걸기 태그이다. 즉, 여기 누르면 여기로 연결됩니다 하는 것.

HREF 태그의 예를 보자면,
<a href="MainProject.exe">Call MainProject</a>

완전 HTML 문법과 동일하다. 이 방식은 상당히 쉽고 여러번 다른 글에서 논의된 적이 있는 방법이다. 아래 글 참조.
기본적으로 닷넷 어셈블리 (.exe 파일이나 .dll 파일들)는 배포를 위한 기본 구성요소이기 때문에 프로그래머는 어플리케이션을 여러개의 DLL과 한개의 메인 EXE파일로 구성하면 된다. 이렇게 하면 메인 EXE 파일이 중심을 잡고 각 함수를 DLL에서 불러다 사용하는 구조가 되므로 무언가 업뎃을 해야 한다면 메인은 거의 그대로 유지하고 단순히 DLL 파일만 바꿔주면 새로운 버젼이 된다.

지금까지 위에서 이야기 한걸로 보아하면 겁나 쉽고 만만하게 보이지만 사실 몇가지 해주어야 할 것들이 있다.
  • 닷넷 프레임워크가 반드시 클라이언트 (사용자) 컴퓨터에 설치되어 있어야 한다. 
  • 내가 만든 어플리케이션은 클라이언트 입장에서 봤을 때 '부분적으로 신뢰'된다. 이걸 좋게 보려면 참 장점인게 클라이언트의 컴퓨터에 최대한 접근을 적게하려 하므로 클라이언트 입장에서는 더욱 안전하다. 다만, 프로그래머 입장에서 좀더 심층적인 작업, 예를 들자면 파일을 열고 쓰고 닫고 COM 오브젝트를 불러다 쓰고 싶다면 반드시 클라이언트의 보안 규정에 따라야 하므로 좀 귀찮은 과정이 추가된다.
  • 기본적으로 어플리케이션은 몇몇 DLL을 필요로 하게 마련이고 이것을 로드해야 하는데 느린 인터넷 속도와 현재의 설계방식 때문에 좀 느릴수 있다는 점. 
  • 업데이트 자체가 파일 단위 업뎃이라서 어플리케이션이 반만 업데이트 될수도 있다. 헐퀴... 이거 무서운데?
  • 오프라인 모드도 있다는데 이것이 인터넷 익스플로러에만 있고 어플리케이션 자체에는 없다?
  • 기본적으로 어플리케이션의 속성을 조절하기 위한 .config 파일이 없다. 하지만 할 수 있는 방법이 있어. 근데 링크가 죽었네? ㅋㅋ 
  • 오, 프로그램 시작메뉴나 데스크 탑에 단축 아이콘 생성이 안돼! 헐 이거 대박 단점인데? 
Update Application Block
마이크로소프트에서는 위에서 언급한 단점들을 극복하기 위해서 이른바 Updater Application Block (UAB)라는 것을 만들었는데 이 업뎃 블락이라는 놈은 내가 작성한 어플리케이션에 HTTP를 통해 어플리케이션의 각각의 조각을 다운로드 하는 것을 관리하기 위한 라이브러리다. 

이것은 기본 닷넷 프레임워크가 제공하는 것보다 몇가지 장점을 가지는데 다음과 같다.
  • 이것은 로컬 어플리케이션으로 작동한다. 즉, 퍼포먼스 페널티가 없다. 왜냐면 하드 드라이브에서 실행하는 것과 같으므로.
  • 업뎃의 Atomic operation을 보장한다. 즉, 업뎃 하면 100% 업뎃 한다. 중간에 끊기 없기
  • 어플리케이션이 manifest 파일을 가진다. (명세서)
  • 100% Trusted 모드로 실행된다. 즉, 보안 규약이고 나발이고 걍 내 맘대로 할 수 있다.
  • 시작메뉴에 단축 아이콘 생성된다! 
반면, 아까 말한대로 무조건 장점만 있으면 세상이 아름답겠지만 단점도 있다.
  • 이걸 쓰려고 하면 어플리케이션의 대대적인 수정이 불가피하다.
  • 옛날 버전 윈도우에서는 안된다. 98, ME에서 작동 안되고 2000이상되어야 한다. 98/ME 아직도 쓰는 사람 있나? 손들어보자...
  • 100% 트러스트 모드로 작동하므로 난장판 만들 확률 또한 존재한다.
  • 마이크로 소프트에의해 지원 받지 못한다. 헐? 이건 뭔 소리야... 마이크로소프트에서 만들었다면서?
헐, 좀 보아하니까 이거 죄다 연결된 링크도 깨지고 안되네. 이 UAB라는 놈은 2004년 (이글이 쓰여진 시점)에는 한창 잘나갔으나 지금은 사라졌나?

ClickOnce
드이어 우리가 궁금해하는 그 이름. 클릭원스, 한번클릭 님이 등장하셨다. 기본적으로 이 ClickOnce는 UAB가 가지는 모든 장점을 흡수하고 적은 단점을 가지고 추가적인 기능을 더 가지는 놈이다. 즉, 킹.왕.짱.이다. 저자가 생각하는 최고의 장점은 코드 보안을 지킨다는 점이란다. 난 없는게 더 좋던데...ㅋㅋㅋ

HREF EXEs와 비교하자면 ClickOnce는 다음과 같은 장점을 가진다.
  • 업뎃의 Atomic Operation 100% 보장 (중간에 끊기 없음)
  • 오프라인모드 지원할 뿐만아니라 어느정도 수준으로 할 건지도 조절가능. 심지어 어플리케이션이 온라인인지 오프라인인지 알아내는 API도 제공됨
  • Visual Studio와 연동도 뛰어나 어플리케이션이 요구하는 보안 수준도 설정가능하다
  • Bootstraper와 함께 오기때문에 필요한 컴포넌트를 쉽게 다운 받을 수 있다. 심지어는 닷넷 프레이워크 자체를 다운받는 것도 가능하다. 즉, 백지상태의 클라이언트에서도 인터넷만 연결되어 있다면 필요한 모든 것을 다 받아 설치할 수 있다는 말.
  • 시작메뉴에 단축 아이콘 생성 가능!
ClickOnce는 Visual Studio 2005의 기능중 하나이며 이전에는 Whidbey라고 불렸단다. 왜 그런 이름을? 난 알길이 없네... 

A ClickOnce Application
자, 이제 어떻게 사용하는지 볼까나. 다음과 같은 단계를 거쳐야 한다.

  1. Visual Studio 시작 
  2. 새 프로젝트 생성
  3. 간단한 C# 윈폼 프로젝트 선택
  4. 이름 맘대로 정하고 생성
  5. 간단한 버튼 추가해 주고
  6. 버튼에 간단한 코드 MessageBox.Show() 같은거 넣어주기
이제 이 다음부터는 프로젝트 속성에서 설정하는 것만 남았다. 사실 이 글이 쓰여진 시점으로 부터 워낙 많이 시간이 흘러 지금 내가 사용하는 2012에서는 약간 다른 점도 있지만 기본적으로는 아주 유사하다.


프로젝트 속성의 Publish 탭에 들어가면 각종 퍼블리슁 옵션이 나오는데 여기서 주목해야 할 것은 Publishing Location과 Install Mode and Setting 부분이다.

Publishing Location에서 어디에다 이 어플리케이션을 퍼블리슁 할거냐 묻는 건데 보시다시피 네트워크 공간도 가능하다. 고로 웹 서버에 올려놓기도 가능하다.

그리고 아래 버튼에 보면 Prerequisites 버튼이 있는데 이게 참 착한 놈이다. 


위 그림처럼 어떤 어셈블리가 필요한지 선택해주면 내가 만든 어플리케이션을 설치하기 전에 미리 이 조건이 충족되는지 확인해 보고 없으면 알아서 다운 받아서 설치한다. 참 착하다.

여기서 기타 등등 모든 세부 메뉴를 설명하기에는 시간 낭비인것 같고, 아무튼 이 ClickOnce라는 기능은 참으로 착하다... 웹 서버에 내가 작성한 어플리케이션을 넣어두고 업뎃까지 자동으로 알아서 하며 윈도우 운영체제의 보안 체계도 지키면서 필요한 놈들은 알아서 척척 미리 설치까지 하는 이 기능이야 말로 정말 대다나다. 

마무리 하자면 배포에는 몇가지 방법이 있다. 일단 제일 간단한 방법이 필요한 파일, 즉, EXE 파일과 DLL을 걍 복사해서 USB나 CD로 다른 컴퓨터에 넣는 방법이다. 이 방법은 문제가 있을 확률이 높다. 왜냐면 내가 복사를 할 컴퓨터에 필요한 구성 요소들이 무조건 있다라고 가정하고 있기 때문이다. 이것은 매우 위험한 전제 가정이다. 

두 번째 방법은 위에서 언급한 방법보다 조금 진화된 방법인데, 필수 파일은 복사해서 주고 필요 구성 요소들은 마이크로소프트가 제공하는 Redistribution Package를 설치해서 충당하는 방법이다. 이것이 물론 좀더 진화되기는 했는만 여전히 어떤 Package를 설치해야 하는지 그리고 얼마만큼이나 설치를 해야 하는지에 대한 질문에 대답을 해야 한다. 고로 복잡하기는 마찬가지다.

세 번째 방법은 Setup 프로젝트를 솔루션에 추가하여 Setup.msi 파일을 생성해내는 것이다. 이것은 ClickOnce의 Offline 버전이라고 보면 된다. 일반적으로 CD로 배포되는 어플리케이션들이 죄다 이런 방식이다. 물론 웹에서 다운 받는 파일도 대부분이 이런 형태를 띄고 있다. 이 방법은 ClickOnce와 마찬가지로 각종 옵션을 설정할수 있으며 Prerequisites를 설정할 수 있기 때문에 좋다.

마지막이 아마도 이 ClickOnce라는 방법이 아닐까 싶다. 코드 보안도 유지하고 공간 낭비도 적고 매번 새로운 파일을 제공하지 않아도 알아서 업뎃도 하며 각종 편의 사항은 모두 지원하는... 다만 인터넷이 빨라야 하고 큰 크기의 어플리케이션 설치에 사용하기에는 조금 무리가 있지 않나 싶다. 하지만 요즘 나오는 대형 게임을 보면 알수 있다시피 거의 이런 ClickOnce 형태의 독자적인 웹 서버 베이스의 설치 과정을 가지는 것을 보면 분명 배포의 최종 단계는 중앙 서버를 이용한 인터넷 퍼블리슁이라고 봐야 겠다. 





'프로그래밍 > MSDN' 카테고리의 다른 글

Implicit Platform Invoke  (0) 2013.09.24
Explicit PInvoke C++ (DllImport Attribute)  (0) 2013.09.24
Mixed, Pure, Verifiable  (0) 2013.09.20
[MSDN] .NET Framework 구조  (4) 2013.05.03
Pure C++: Hello, C++/CLI  (0) 2013.04.25
posted by 대갈장군
2013. 9. 26. 05:58 프로그래밍/Arduino

어쩌다 보니 Arduino를 사용하게 되었는데 이 조그마한 기판을 사용하는 목적은 입력으로 들어오는 5 V / 60 Hz의 신호를 원하는 시간만큼 딜레이를 줘서 고대로 내보내거나 혹은 주파수를 원하는 형태 즉, 30 Hz, 20 Hz, 15 Hz로 변환 하는 것이었다.


처음 생각은 간단하겠는데 였는데 생각보다 쉽지 않았다. 제일 먼저 시도한 것이 analogRead() 함수 였는데 이것의 문제점은 기본적으로 이 함수는 최대한 빨리 입력으로 들어오는 신호를 읽어 들인다는 것이다. 다시 말해서 모든 입력 트리거를 캐치하지 못할 수도 있다는 것.


왜냐면 입력으로 들어오는 트리거 신호는 60 Hz 이므로 16.6ms 마다 5 V에서 0 V로 떨어졌다 바로 다시 5 V로 바뀌는 구조 인데 여기서 0 V로 유지되는 시간이 무지 무지 짧다는 점. 그렇기 때문에 analogRead() 함수가 아니라 attachInterrupt() 함수를 이용해야 한다. 


http://arduino.cc/en/Reference/attachInterrupt 를 참고하면 어떻게 attachInterrupt() 함수를 사용하는지 알수 있다. 기본적으로 디지털 핀 2번으로 입력을 넣고 코드에서 attachInterrupt() 함수를 해당 핀에 연결시키면 트리거가 들어올때 마다 100% 확률로 연결되어진 콜백함수를 호출한다. 고로 신호를 읽을 걱정은 안해도 된다.


다만 그 다음 스텝인 신호 지연을 어떻게 하느냐는 것이었는데, 이것은 micros() 함수를 사용해서 트리거가 들어오는 시점(0 V로 떨어지는 순간)의 시간을 기억해 둔 다음 지연시키고자 하는 만큼의 시간을 더해서 기억해 둔 다음 현재의 시간이 기억해둔 시점보다 크거나 기억해둔 시점 더하기 1 ms 의 시간보다 적으면 (그 범위에 있다면), 그 시간동안 0 V를 출력 핀으로 전송한다. 그 외의 시간에는 계속해서 5 V를 출력 핀으로 전송한다. 물론 원한다면 1 ms이아니라 더 정교한 시간 조절도 가능하다. 하지만 반드시 어느 정도의 시간동안은 0 V를 유지해야 하는데 왜냐면 너무 짧게 0 V를 유지하면 오실로스코프에도 잡히지 않을 정도로 빠르게 통과해 버린다.


추가적으로 60 Hz 속도의 주파수를 30, 20, 15 Hz로 변경하는 방법은 몫과 나눗셈 그리고 나머지를 이용한 방법이다. attachInterrupt() 함수를 CHANGE 플래그와 함께 사용하면 입력 신호가 상태 변화를 일으키는 모든 경우에 콜백 함수를 호출한다. 즉, 하나의 트리거에 대해서 낮은 볼트에서 높은 볼트, 그리고 높은 볼트에서 낮은 볼트로 두 번 변화를 일으키므로 두 번의 콜백 함수가 호출된다.


고로 나눗셈을 이용한 주파수 변환시에 이것을 고려해서 60 Hz에서는 2로 나누고 30 Hz에서는 4로 20 Hz에서는 6 그리고 15 Hz에서는 8로 나누면 된다. 콜백함수가 매번 호출될 때 마다 카운터를 하나씩 올려서 정해진 몫으로 나누면 남은 나머지가 바로 언제 0 V가 되어야 하는지를 알려준다. 원한다면 쉬프팅도 가능하다.


또 가장 주의 해야 할 점이 Arduino와 같은 계판 기기는 주로 오랜 시간 작동하므로 항상 변수의 범주에 대해서 깊이 생각해봐야 했다. 단순히 int로 선언한 카운터가 시간이 지나 한참 지나 결국 오버플로우가 되면 0으로 초기화 되면서 알수 없는 에러가 터지기 때문에 내가 작성한 코드에서는 적당한 시점에서 카운터를 0으로 초기화 시켜서 오버플로우가 발생하지 않도록 했다.


다만, micros() 함수가 메뉴얼에 보면 56분 정도 후에 리셋된다고(오버플로우로 인한) 해 놨는데 이것이 어떠한 영향을 미칠지에 대해서 검증이 좀 필요한 것 같다. 


추가로 시리얼 포트를 통한 통신으로 C# GUI와 연동하면 사용자의 어여쁜 GUI와 강력크한 Arduino의 능력을 섞어서 쓸수 있다. 아마도 더 좋은 방법이 많이 있겠지만...



posted by 대갈장군
2013. 9. 24. 05:57 프로그래밍/MSDN


// vcmcppv2_impl_dllimp.cpp
// compile with: /clr:pure user32.lib
using namespace System::Runtime::InteropServices;

// Implicit DLLImport specifying calling convention
extern "C" int __stdcall MessageBeep(int);

// explicit DLLImport needed here to use P/Invoke marshalling because
// System::String ^ is not the type of the first parameter to printf
[DllImport("msvcrt.dll", EntryPoint = "printf", CallingConvention = CallingConvention::Cdecl,  CharSet = CharSet::Ansi)]
// or just
// [DllImport("msvcrt.dll")]
int printf(System::String ^, ...); 

int main() {
   // (string literals are System::String by default)
   printf("Begin beep\n");
   MessageBeep(100000);
   printf("Done\n");
}
Begin beep
Done

위의 코드를 보면 PInvoke에 대한 경우들을 잘 설명하고 있는데 우선 첫번째가 Implicit DLLImport이다. Native로 작성된 DLL 어디엔가 __stdcall 호출 규약을 가지는 함수 MessageBeep(int) 가 정의되어 있다고 알려주는 한 줄의 코드로써 상황 종료다. 


다만 이런 암묵적인 PInvoke를 사용하기 위해서는 반드시 호출 함수의 입력 변수가 마샬링이 필요 없어야 한다. MessageBeep() 함수의 경우 int를 받기 때문에 Int32로 마샬링 없이 변환이 가능하므로 암묵적 PInvoke 사용이 가능하다.


두번째를 보게 되면 명시적인 DLLImport인데 이 명시적인 것도 두가지 방법이 있네. 첫번째는 보아하니 msvcrt.dll에서 printf 함수를 어떤 형태의 호출 규약으로 그리고 어떤 형태의 타입 (ANSI냐 Unicode냐)까지 명확하게 정의하는 것과 혹은 걍 간단하게 어떤 DLL을 가져와라라고 말하는 방법이다. 둘다 된다면 당연히 두번째 방법이 더 쉬지 않나? 


어쨌든 첫번째 방법은 단순한 함수 선언이고 두번째 방법은 DllImport 속성을 이용한 명시적 DLL 파일 이름을 지정하는 것. 그것이 가장 큰 차이점이고 두번째 방법 내부에서 또 추가적으로 더 명시할 수 있는 방법도 있다는 말. 


http://msdn.microsoft.com/en-us/library/2x8kf7zx.aspx

'프로그래밍 > MSDN' 카테고리의 다른 글

ClickOnce를 이용한 윈도우 폼의 배포  (3) 2013.09.27
Explicit PInvoke C++ (DllImport Attribute)  (0) 2013.09.24
Mixed, Pure, Verifiable  (0) 2013.09.20
[MSDN] .NET Framework 구조  (4) 2013.05.03
Pure C++: Hello, C++/CLI  (0) 2013.04.25
posted by 대갈장군
2013. 9. 24. 05:18 프로그래밍/MSDN

.NET Framework는 Platform Invoke (줄여서 PInvoke)라고 불리는 Managed Application에서 Unmanaged 함수를 호출할 수 있도록 해주는 기능을 Dllimport 속성을 이용해서 지원한다. 이 PInvoke는 두 가지 형태가 있는데 첫번째는 Explicit (명시적) 호출 방법이고 두 번째는 Implicit (암묵적) 호출 방법이다. 오늘 알아볼 녀석은 Explicit (명시적) 호출 방법이다.


DllImportAttribute를 사용해서 PInvoke를 수행하는데 이 녀석은 각각의 DLL 진입점에서 함수 선언 직전에 위치함으로 여기서 부터는 이 Unmanaged DLL을 사용하겠다고 명시하는 것이다. 


Unmanaged DLL 함수는 Managed 코드로 융합되기 위해서 몇가지 추가 코드와 간단한 데이터 변환을 추가로 가지게 되면 이를 통해서 Managed 코드는 Unmanaged DLL의 함수로 접근이 가능하다. PInvoke는 /clr, /clr:pure, /clr:safe 세 종류의 스위치에 대해서 모두 사용 가능하다. 소 파워풀!


Platform invoke


위의 그림을 보면 PInvoke에 대한 설명이 잘 나와 있는 것 같다. Managed코드가 컴파일러를 통과해서 CLR 서포트를 받는 어셈블리로 생성이 되면 메타 데이터와 IL (MSIL) 코드를 가지게 된다. 그리고 이런 Managed 코드 내부에서 만약 PInvoke가 호출되게 되면 Standard Marshalling Service를 통해 중간 과정에 필요한 변환을 거쳐서 Unmanaged DLL 함수를 호출하게 된다. 


이 PInvoke는 사실 CLR (Common Language Runtime)이 제공하는 서비스이다. 이 서비스를 통해서 Managed Code가 Unmanaged DLL 함수를 호출 할 수 있도록 해준다. 이러한 형태의 Marshalling, 마셜링 (컨테이너를 수송하기 위해 부두 내에서 이동, 정렬 하여 잠시 대기한다는 의미의 전문 용어) 서비스는 이미 Runtime과 COM의 호환 작용 및 "It Just Works" (IJW)를 위해서도 이미 사용된 적 있는 메커니즘이다. 


마셜링에 대해서 좀 더 자세히 살펴 보고 싶지만 일단 넘어가고, PInvoke를 실제적으로 어떻게 사용하는지 살펴보자.


다음의 코드는 PInvoke를 사용하는 예를 보여주는데 puts라고 불리는 native function은 msvcrt.dll에 정의되어 있다. 즉, puts가 바로 Unmanaged function이고 우리는 그것을 managed code에서 불러다가 사용하려고 하는 것이다. 


// platform_invocation_services.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

[DllImport("msvcrt", CharSet=CharSet::Ansi)]
extern "C" int puts(String ^);

int main() {
   String ^ pStr = "Hello World!";
   puts(pStr);
}

위의 코드를 보아하니 메인 함수 진입점 직전에 DllImport 속성을 이용해서 msvcrt DLL 파일을 연결하고있다. 그리고 바로 다음 줄에 보니 puts 함수를 외부에 존재하는 함수라고 명시적으로 알려주고 있다. 그리고 내부 코드에서 puts를 사용해서 출력을 하고 있다.


위와 같은 역활을 하는 코드를 IJW로 작성해 보면 다음과 같다.


// platform_invocation_services_2.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

#include <stdio.h>

int main() {
   String ^ pStr = "Hello World!";
   char* pChars = (char*)Marshal::StringToHGlobalAnsi(pStr).ToPointer(); 
   puts(pChars);
   
   Marshal::FreeHGlobal((IntPtr)pChars);
}

IJW를 보아하니 명시적으로 Native DLL을 임포트 하라는 DllImport 속성을 사용하지 않았지만 간단하게 puts() 함수를 선언하고 있는 stdio.h 헤더를 선언하고 메인 함수 내부에서 마샬링 서비스를 직접적으로 호출해서 라이브로 변환한 다음 사용된 힙 메모리를 직접 해제 하는 것 같다. 불행히도 MSDN에서는 자세하게 위 코드에 대해서 설명하지 않는다. 아마도 IJW에 대한 페이지를 살펴봐야 정확한 의미를 알 수 있을 것 같다.


IJW를 사용하는 장점은 몇가지가 있는데 다음과 같다. 일단, DLL을 명시적으로 DllImport 속성을 이용해 연결할 필요 없고 그냥 헤더 파일이랑 라이브러리를 임포트 하면 장땡이다. 뭐, 그닥 장점처럼 안보이긴 하는데... 어쨌든.. 


IJW 방법을 사용하면 아주 약간 더 빠르다. 왜냐면 프로그래머가 명시적으로 어떤 타입의 어떤 포인터로 접근하라고 명시적으로 알려주기 때문이다. 


마찬가지로 같은 맥락에서 퍼포먼스에 대한 그림이 쉽게 그려진다는 것이 IJW의 또 다른 장점이다. 위의 코드를 예로 보자면 Unicode에서 ANSI로 변환이 필요하다는 것과 그 과정에서 추가적인 메모리 할당과 해제가 필요함이 확연히 드러나게 된다. 사실 위 경우에는 _putws와 PtrToStringChars를 사용하는 것이 퍼포먼스 면에서는 더 유리하다. 


마지막으로 같은 데이터에 대해서 매번 마샬링을 호출하기보다는 한번 호출해서 메모리를 복사해두고 복사된 메모리를 사용하면 성능향상에 매우 도움이 된다. DllImport를 사용하면 같은 데이터라도 매번 오버헤드가 발생하는 변환을 해야 한다.


자, 이제 안 좋은 점에 대해서 알아보자. 장점에서 언급한 것들의 정 반대가 단점이다. ㅋㅋ


우선 마샬링이 아주 아주 명시적이므로 코드를 아주 정확하게 적성해야만 작동한다. DllImport를 사용하면 이러한 복잡한 과정을 생략할 수 있다. 위 두 코드를 딱 봐도 DllImport가 훠얼씬 더 간단하다. 


IJW의 마샬링은 죄다 Inline 함수다. 고로 어플리케이션의 흐름에 지장을 줄수도 있다. 


마지막으로 명시적인 마샬링은 IntPtr 타입을 리턴하므로 반드시 ToPointer 함수를 사용해야 한다. 이게 왜 단점일까 라고 잠시 생각했다. 생각해보니 이것은 데이터 타입을 void로 바꾸는 것과 같다. 고로 프로그래머가 정확한 타입을 명시해 주어야 한다. 위 코드에서처럼 (char*)타입으로 명시적으로 캐스팅 하는 과정이 추가로 필요하다는 말. 


고로 위의 이야기를 요약하자면 IJW를 사용한 것이 더 효율적이 효과적인 프로그래밍이 가능하지만 만약 내가 작성하는 코드가 가끔씩 Unmanaged API를 호출하는 정도라면 둘 중 어떤 것을 사용해도 무방하다.


그 다음으로 MSDN에서 설명하고 있는 것이 PInvoke를 Windows API와 사용하는 것인데 일단 코드를 보자.



// platform_invocation_services_4.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HWND;
[DllImport("user32", CharSet=CharSet::Ansi)]
extern "C" int MessageBox(HWND hWnd, String ^ pText, String ^ pCaption, unsigned int uType);

int main() {
   String ^ pText = "Hello World! ";
   String ^ pCaption = "PInvoke Test";
   MessageBox(0, pText, pCaption, 0);
}

사실 Managed 코드라면 얼마든지 MessageBox를 잘 불러다 사용할 수 있지만 이건 예를 들기 위해 이렇게 한거다. user32 DLL을 DllImport를 사용해 명시적으로 연결해 주고 메인 함수 내부에서 실제적으로 사용하고 있다. 아하, 두번째 인자에 대한 비밀이 풀렸다. 위의 예제들에서 보면 DllImport 속성자 다음에 오는 첫번째 인자는 DLL 파일의 이름이라는 것은 알았을테고 두번째 인자에 대해서 저건 왜 필요하지에 대해서 생각했었는데 바로 해답은 아래와 같다.


실제로 user32.dll 파일 안에는 MessageBox라는 함수는 없다. 왜냐면 user32.dll에는 MessageBoxA (ANSI 타입)과 MessageBoxW (Unicode 타입) 둘 만 존재한다. 그래서 저 CharSet = CharSet::Ansi 인자를 통해서 MessageBoxA를 사용하라고 알려주는 것이다. 재미 있는 점은 되도록이면 Unicode 버전을 쓰라는 건데 왜 위의 모든 예제 코드에서는 죄다 Ansi를 쓰는지 모르겠네. 된다는 걸 보여주기 위해서 인가? Unicode를 사용하면 오버헤드가 더 줄어든단다. 


중요한 게 한가지 나오는데 언제 PInvoke를 사용하면 안되느냐 하는 것이다.


예를 들어 다음과 같은 함수를 Unmanaged code로 작성해서 Dll로 만들었다고 치자. 

char * MakeSpecial(char * pszString);


그렇다면 이 함수를 불러다 쓰려면 다음과 같은 형태를 취해야 한다.

[DllImport("mylib")]

extern "C" String * MakeSpecial([MarshalAs(UnmanagedType::LPStr)] String ^);

이 함수 호출의 문제점은 MakeSpecial로 호출되어 리턴되어지는 메모리를 해제 할 수가 없다는 점이다. PInvoke를 통해서 리턴된 포인터는 사용자가 해제할수 없는 내부 메모리로 저장이 되기 때문이란다. 고로 이런 경우에는 반드시 IJW를 써야 한다. 


PInvoke의 한계점은 입력으로 들어오는 변수를 출력으로 리턴할 수 없다는 데 있다. 입력으로 들어가는 변수가 PInvoke에 의해서 마샬링을 거치게 되면 이 변수는 리턴될 때 메모리 커럽션 에러가 발생한다. 방금 위에서 말한 것과 거의 같은 이유인것 같다.


__declspec(dllexport)
char* fstringA(char* param) 
{
   return param;
}

또 다른 간단한 예를 보자면 아래 코드와 같은 user32.dll에 있는 CharLower이라는 함수를 호출해서 사용하는 경우인데 이 함수도 입력으로 들어오는 입력 변수의 메모리 공간을 사용해서 다시 출력으로 돌려 보내는 함수이다. 고로 이렇게 하게 되면 입력으로는 제대로 된 변수와 메모리가 들어가지만 출력으로 돌아오는 변수와 메모리는 해제되어 버린 값이 된다. 간단하게 생각하면 PInvoke를 통해서 호출된 함수는 리턴하는 즉시 해제되어 버린다고 보면 되는건가? 변수와 메모리를 유지하는 일반적인 범위에서는 일어날 수 없는 일이지만 PInvoke는 마샬링 서비스를 통과하기 때문에 잠깐 동안 함수의 범위에서 벗어났다가 들어오기 때문인가..


// platform_invocation_services_5.cpp // compile with: /clr /c using namespace System; using namespace System::Runtime::InteropServices; #include <limits.h> ref struct MyPInvokeWrap { public: [ DllImport("user32.dll", EntryPoint = "CharLower", CharSet = CharSet::Ansi) ] static String^ CharLower([In, Out] String ^); }; int main() { String ^ strout = "AabCc"; Console::WriteLine(strout); strout = MyPInvokeWrap::CharLower(strout); Console::WriteLine(strout); }


PInvoke를 쓰더라도 같은 형태의 데이터 타입에 대해서는 마샬링이 필요가 없는데 예를 들자면 int 타입과 Int32 타입이다. 하지만 같은 형태의 데이터 타입이 아닌 경우에는 당연히 마샬링이 필요하다. 아래 테이블을 보면 다양항 형태의 타입에 대해서 어떤 마샬링이 맵핑 되는지 보여준다. 젠장 테이블이 안맞아서 걍 링크를 건다. http://msdn.microsoft.com/en-us/library/ms235282.aspx


마샬링을 수행하는 '마샬러' (젠장, 한국어로 바꾸려니 말 참 이상해지네..)는 Unmanaged 함수로 넘어가는 변수의 주소를 자동적으로 런타임 힙 메모리 공간에 Pin (꼳아 놓는다) 시켜 놓는다. 즉, 홀딩 시킨다. 이렇게 해야 가비지 컬렉터가 이 메모리 공간을 비워버리지 않는다. 


위에서 본 예제에서 CharSet이라는 두번째 변수를 이용해서 DllImport 속성자에 명시했던 것을 기억하는가? 이것이 바로 Managed String이 어떻게 마샬링 되어야 하는 가를 알려주는 단서다. 위의 예제에서는 Native 함수가 ANSI로 되어 있다고 알려주었다. 


이러한 마샬링에 필요한 정보를 각각의 Native 함수의 입력 변수마다 정의 할 수 있다. String * 입력 변수에 대해서 마샬링 할 수 있는 것들로는: BStr, ANSIBStr, TBStr, LPStr, LPTStr. 이 있고 기본 타입은 LPStr 이다. 


아래의 예제 코드를 보면 string이 2 바이트 유니코드 char string (LPWStr)으로 마샬링 하는 것을 보여주고 있다. 출력으로 나오는 놈은 문장 전체가 아니라 Hello World!의 첫 글자만 나온다. 왜냐면 마샬링된 문자열의 두번째 바이트가 null이므로 문장 종료로 인식되기 때문이다. 고로, 어떤 마샬링을 해야하는지 알려주는 과정이 굉장히 중요하고 또한 프로그래머의 책임이 뒤따른다.


// platform_invocation_services_3.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

[DllImport("msvcrt", EntryPoint="puts")]
extern "C" int puts([MarshalAs(UnmanagedType::LPWStr)] String ^);

int main() {
   String ^ pStr = "Hello World!";
   puts(pStr);
}

PInvoke는 x86 에서 실행시 각 호출마다 약 10 에서 30 의 오버헤드가 발생한다. 그리고 추가로 마샬링을 수행함으로써 또 다른 오버헤드가 발생한다. 다만 같은 형태의 타입에 대해서는 마샬링 오버헤드가 없다. 


고로 보다 나은 퍼포먼스를 위해서는 보다 적은 PInvoke를 사용하고 동시에 많은 데이터를 마샬링 하도록 하는 것이 많은 수의 PInvoke를 사용하며 동시에 적은 데이터를 마샬링 하는 것보다 더 빠르다.






'프로그래밍 > MSDN' 카테고리의 다른 글

ClickOnce를 이용한 윈도우 폼의 배포  (3) 2013.09.27
Implicit Platform Invoke  (0) 2013.09.24
Mixed, Pure, Verifiable  (0) 2013.09.20
[MSDN] .NET Framework 구조  (4) 2013.05.03
Pure C++: Hello, C++/CLI  (0) 2013.04.25
posted by 대갈장군
2013. 9. 20. 23:48 프로그래밍/MSDN

Visual C++은 .NET 프로그래밍 환경에서 세가지 다른 형태의 CLR (Common Language Runtime) 컴파일 옵션을 제공하는데 고놈들이 바로 Mixed, Pure 그리고 Verifiable이다. 


  • Mixed (/clr)

이 녀석은 일반적인 기본 옵션이다. 말 그대로 섞여 있다는 말인데 무엇과 무엇이 섞여 있는냐 하면은, Managed 코드와 Unmanaged 코드가 섞여 있다는 의미다. 이 옵션을 켬으로써 Managed 코드와 Unmanaged 코드의 결혼을 성사시킨다는 말이다... 이것을 이른바 C++ Interop이라 부른다. 아마도 Interoperable의 약자가 아닐까?


  • Pure (/clr:pure)

Pure 옵션의 경우는 Managed 와 Unmanaged 데이터 타입을 둘 다 가질 수 있지만 함수는 오직 Managed 형태만 소유 할 수 있다. Mixed와 마찬가지로 Pure Assembly는 P/Invoke를 통해서 Native DLL과의 Interop을 허락하지만 C++ Interop Feature는 불가하다. 잠깐만, 그러니까 이미 컴파일된 Managed 코드 DLL은 P/Invoke를 통해 함수를 호출해 사용할 수 있지만 코드 내에서 직접적으로 함수를 선언해서 호출하는 것 (다른 말로 C++ Interop)은 허락되지 않는다는 말인감? 그리고 한가지 더 중요한 점은 Pure Assembly는 Native 함수로 부터 호출가능한 함수를 Export할 수없는데 그 이유는 Pure Assembly는 __clrcall 호출 규약을 사용하기 때문이다. 



  • /clr:pure 사용의 장점
    • 더 나은 성능: Pure Assembly는 오직 MSIL만 포함하고 있으므로 Native Function을 내포하고 있지 않다. 고로 Managed/Unmanaged Transition이 없다. 왔다리 갔다리 안한다는 말. (다만 P/Invoke를 이용해서 호출되는 경우는 예외다. 여전히 왔다리 갔다리 한다는 말)

    • AppDomain Awareness: Managed 함수들과 CLR 데이터 타입은 전부다 Application Domain에 상주하고 있으므로 이런 함수와 데이터 타입에 대한 접근성이나 안정성이 Mixed assembly 경우보다 훨씬 더 낫다는 점. 그리고 .NET 프레임워크 내에서 다른 언어와의 호환성에서도 더 유리하다는 점.

    • Non-disk loading: Pure Assembly들은 메모리 상에 로딩 될 수도 있고 심지어는 스트리밍이 될 수도 있다. Mixed 경우에는 반드시 디스크 상에 물질적으로 존재해야만 한다는 조건이 따라 붙는다. 

    • Reflection: Mixed 된 실행 파일은 Reflection이 허용되지 않지만 Pure의 경우에는 완전 서포트 해준다.

    • Host Controllability: MSIL만 포함하고 있기 때문에 Mixed보다 예측가능하고 더 유연성 있게 코드를 작성할 수 있단다.

  • /clr:pure 사용의 한계
    • Pure Assembly는 Unmanaged Function으로부터 호출 되어 질 수 없다. 고로 Pure Assembly는 COM interface를 구현할 수 없고 또는 Native Callback을 드러낼 수도 없다. Pure Assembly는 __declspec(dllexport)나 .DEF 파일을 통해 함수를 Export할 수 없다. 

    • ATL과 MFC 라이브러리들은 지원되지 않는다. 

    • #import 지원 안됨

    • 익셉션 핸들링과 얼라인먼트를 위한 플로팅 포인트 옵션 조정 불가. 고로 fpieee.h 와 같은 파일은 pure 옵션으로 컴파일 안됨

    • GetLastError 함수는 pure 옵션에서 Undefined Behavior라는 에러로 처리됨. 헐퀴.

  • Verifiable (/clr:safe)
가장 강력크한 놈이 나타났다. 이 옵션을 키고 컴파일을 때리면 작성되는 코드는 CLR 환경에서 100% 호환되게 작동되며 어떤 보안 옵션도 어기지 않고 말 잘듣는 코드로 생성된다. 예를 들자면 실행전에 디스크에 파일을 생성하고 지울수 있는 권한이 있는지 미리 확인하고 체크하여 사용자에게 알려주는 그런 기능. 당연히 이 옵션을 키면 CRT 서포트는 없다. 고로 Managed 코드를 섞어 쓸수 없다는 말이다. 

이 옵션을 사용하면 일단 당연히 보안성이 높아지고 특정 개발 환경에서 요구되기도 하며 미래에 나올 윈도우즈 환경에서도 잘 작동하는 프로그램을 작성하게 되는 것이다. 단점이라면 C++ interop 을 사용할 수 없다는 점.

마지막으로 P/Invoke를 사용하면 어떤 옵션으로 컴파일 하던간에 Native DLL을 임포트 할 수 있지만 런타임 환경에서 잘 작동할거라는 보장은 없다. 당연한 이야기지...... ㅋㅋ 

다음으로 P/Invoke에 대해서 알아봐야 겠군.



posted by 대갈장군
2013. 5. 24. 23:18 프로그래밍

Visual Studio 2010의 Setup Project로 만든 이쁜 셋업 파일을 막상 다른 컴퓨터에 설치해 보면 관리자 권한으로 실행하기가 기본 옵션이 아니다.


일반적으로 프로그램은 많은 경우에 관리자 권한을 요구하게 마련이다. 그런데 참 재미 있는 것은 Visual Studio는 이런 권한 설정을 바꾸어 주는 옵션을 제공하지 않는다. 왜? 왜 그랬을까?


아무튼, 생성된 msi 파일을 Orca로 수정해야만 생성되는 단축 아이콘에 관리자 권한이 설정이 된다. 


1. ORCA 설치 - Orca는 Windows Platform SDK에 기본으로 포함되는 옵션 프로그램이다. 아래 그림처럼 찾아가보면 Orca.msi가 있을 것이다. 이 프로그램은 기본적으로 MSI 파일을 수정하는 프로그램이다.



2. Orca 설치가 끝났다면 수정하고자 하는 MSI 파일을 Orca로 열자. 그리고 Property 옵션에가서 새로운 열을 하나 생성한다. 그 새로운 열의 이름은 바로 DISABLEADVTSHORTCUTS그리고 값은 1로 주자. 참고로 새로운 열을 생성하는 것은 더블클릭으로 가능하다. 아니면 우클릭! 



3. 이제 왼쪽 Tables 에서 Shortcut으로 이동해 보면 내가 만든 단축키들이 그 모습을 드러낸다. 여기서 오른쪽 화면 중간 쯤 보면 Target 이라는 행이 있는데 여기에 기본 값으로 'DefaultFeature'이라는 것이 들어가 있는데 이 것을 [TARGETDIR]\내실행파일이름.exe로 바꾼다. 그리고 세이브 때리면 이제 이 파일이 설치된 컴퓨터의 데스크탑 화면 단축 아이콘에는 관리자 권한 실행 요구 표시인 방패 문양이 뜰것이다. 






posted by 대갈장군
2013. 5. 3. 00:05 프로그래밍/MSDN
MSDN에 나와 있는 .NET Framework에 대한 설명을 읽고 있자면 '지루하다'. -_-

일단 모르는 단어들이 너무 많이 튀어 나와서 처음 읽는 사람으로 하여금 급거부감을 주게된다.

모르는 단어를 클릭해서 링크를 따라 들어가다 보면 어느새 어딘지 알수 없는 곳에서 또 다시 모르는 단어를 클릭 하려는 내 모습을 발견하고는 깊은 한숨과 함께 '프로그래밍은 나랑 잘 맞지 않습니다...'를 연발하게 된다. 슬픈 현실이다... ㅡ.ㅜ

개인적으로 영어를 쓸 일이 많다보니 아무래도 한국어로 된 MSDN보다 영어로 된 MSDN을 좋아하는데 원래 프로그래밍 언어가 영어인데다가 영어는 불분명한 의미가 없는 정확한 표현을 쓰기 때문에 오히려 한국어로 된 설명보다 이해가 더 쉽게 될때가 많다.

그래서 MSDN은 영어로 읽는 것이 더 나을 것이라고 말하고 싶지만 영어를 격하게 싫어하시는 분들은 아마도 한국어가 나을 듯하다...

이제 .NET Framework에 대해서 이야기 해보자. 이 놈은 내가 Visual Studio 를 이용하면서 부터 꾸준히 내 머리 속에 맴돌던 놈인데 단 한번도 제대로 들어다 본적은 없었다.

왜냐면 일단 내가 차후 설명하게 될 Common Language Runtime(CLR)을 전혀 사용하지 않았기 때문이고 둘째로 이 .NET Framework는 알게 모르게 내 컴퓨터의 구석에서 내가 하는 일들을 돕고 있었다.

비록 그 실체를 몰랐어도 저놈이 있다는 것은 알고 있었다. 요즘들어서 내 프로그램을 다른 컴퓨터에 돌릴일이 많아지다 보니 이놈에 대해서 자연스럽게 접근하게 되었고 몇몇 MSDN 문서를 읽게 되었다.

아마존 닷 컴에서 Ivor Horton's Beginning Visual C++ 2008 이라는 책을 검색해 보면 저 책의 1장을 볼수 있는데 여기에 많은 설명이 잘 되어 있다. (공짜다.. ㅋㅋ)

사용자 삽입 이미지


내가 개인적으로 .NET Framework를 잘 설명하고 있다고 생각하는 그림이 바로 위 그림이다. 일반적으로 C나 C++을 이용해서 간단히 작성해 오던 방식이 그림 제일 왼편에 있는 그림이다. 이것을 보게되면 Native C++코드를 컴파일러(gcc나 각종 컴파일러들)로 Object 파일로 생성하고 Link를 통해 실행 파일을 만들어서 돌리게 된다.

이후 MFC가 나오면서 가운데 한단계가 더 추가되었다. (중간 그림) 사실 이 MFC는 한번도 사용해 본적은 없지만 아주 쉽고 간편하면서도 다양한 형태의 함수를 제공했다는 걸 알수 있었다.

.NET Framework가 나오고 나서도 물론 MFC가 지원되지만 .NET Framework가 MFC보다 한수 위인 이유는 정확히는 몰라도 .NET Framework가 가지는 강력한 Cross-PlatformCross-Language 특성 때문이 아닌가 싶다.

마지막으로 제일 오른편이 바로 .NET Framework를 제대로 사용하는 경우인데 뭔지 모를 2단계가 들어가 있는데 이것이 바로 .NET Framework를 구성하는 2개의 구성요소인 Common Language Runtime과 .NET Framework Library다.

간단하게 설명해서 .NET Framework Library는 걍 함수들의 집합이다. 이놈들은 전부 객체 지향적이므로 독립적으로 사용가능하며 독립적이므로 안전하고 아주 좋다... -_-

사실 CLR이 참 재밌는 놈인데 마지막 단어인 Runtime에 주목할 필요가 있다. 즉, 실행중이란 의미인데 내가 프로그램을 더블 클릭해서 실행에 들어가는 직후부터 프로그램이 죽는 순간까지를 Runtime이라 보면 된다.

이 CLR은 그 런타임에 작동을 하는 놈으로써 다양한 작업을 하는데 예를 들면 보안을 높이고 메모리를 관리하면 타입을 관리하고 각종 시스템의 핵심 서비스들을 제공하기까지 한단다...

나도 프로그램에 입문한지 얼마 되지는 않았기 때문에 정확한 것은 알 수 없지만 .NET Framework의 CLR이 '이것은 새롭다! 이것은 좋다!'라고 Microsoft가 외치는 이유가 바로 Runtime 이라는 저 단어 하나 때문이 아닌가 싶다.

기존의 프로그램 컴파일과 실행의 과정을 보게 되면 컴파일을 수행하는 단계에서 이미 현재 컴퓨터에 종속적 (dependent) 이게 되고 Linker를 통해 실행파일을 만드는 과정에서도 역시 종속적이게 되는 것 같았다. (내 생각일 뿐임... 물론 자바가 이 원리를 깨뜨렸다.)

헌데 이 새로나온 .NET Framework에서는 중간에 CLR와 JIT(Just-In-Time Compiler)를 실행 파일을 만드는 과정에 집어 넣음으로써 큰 변화를 시도했다는 점이 다른 것 같다.

우선 .NET Framework에서 생성되는 실행 파일은 실행 파일을 더블 클릭 한 뒤의 시점(일명 Runtime)에 여러가지 일을 수행하는데 이 일들이 바로 플랫폼에 종속적이지 않도록 만들어주는 일이다.

즉, 이전에는 활용하지 못했던 Runtime이라는 새로운 시간적 공간을 이용해서 각종 다양한 추가 기능 및 편리한 기능을 제공하고 있다는 점이 .NET Framework가 뛰어나다라고 말하는 이유인 것 같다. 자바가 먼저인지 .NET이 먼저인지는 몰라도 자바의 Virtual Machine의 개념도 이와 같다.

물론 이건 내 개인적인 생각이지 이게 이유라고 설명한 글은 본적이 없다... -_-

사실 .NET Framework를 제대로 설명하려면 Common Language Runtime과 더불어 MSIL (Microsoft Intermediate Language), CTS (Common Type System), JIT (Just-In-Time Compiler)를 다 설명해야 하지만 이걸 다 설명하려면 너무 길어질 뿐더러 읽는 사람의 가독력과 흥미가 너무 반감될것 같아 여기서는 말 안해야 겠다....

줄여서 결론을 말하자면 .NET Framework는,

프로그래머가 프로그램을 짤때 사용할 수 있는 새로운 '뼈대 (Frame)' 이다.


그리고 이 새로운 뼈대의 장점들은 무수히 많지만 대표적인 것들이

1. 플랫폼 Independent (플랫폼에 종속되지 않는다.)
2. 프로그램에 대한 다양한 형태의 접근제어컨트롤 방법을 제공한다. 즉, 내용물을 보여주되 복사하거나 그 소스를 못보게 하는 등의 접근제어를 말한다.
3. 강화된 보안을 제공한다. (접근제어도 일종의 보안 방법중 하나지요.)
4. Cross-Language를 제공한다. CLR에 근거하여 작동하는 모든 언어는 서로간에 정보 교환이 자유 자재로 된다는 의미. C#을 작성한 파일을 Visual Basic에서 불러와 사용가능하다는 말. (물론 코딩시 제약이 있다.)
5. Web 개발을 혁신적으로 개선했다. 이건 뭐 읽어보지 않았지만 ASP .NET이 바로 CLR에 기반을 두고 클라이언트가 서버의 컴퓨터에 접속하여 서버의 컴파일러에 의해 작동된다는 뭐 그런소리... 내가 사용할 일은 거의 없을 거라는...

또 무언가 장점이 있을텐데 기억이 안나는 관계로 여기까지만 적자.

Update
시간이 많이 흘러서 다시 이글을 보니 좀 부족한 설명이 있는 것 같아 추가로 몇자 적어 놓는다.

우선 가장 기본이 되는 CLI를 언급해야 할 것 같다. CLICommon Language Infrastructure의 줄임말로 하나의 '규약'을 명시해 놓은 '조립 설명서' 같은 거다.

이 CLI는 고레벨의 프로그래밍 언어 (C++/ C# 같은거)들이 다른 오퍼레이팅 시스템 환경에서 재 컴파일이나 소스 코드를 바꾸지 않고도 문제 없이 균일하게 같은 방식으로 동작할 수 있도록 만들어주는 Virtual Machine을 위한 '규약 설명서'이다. 

즉, 이 CLI 규약대로만 구현을 하면 어느 컴퓨터에서나 돌아갈 수 있는 Virtual Machine을 만들어 낼수 있다는 말인데 이 Virtual Machine의 대표적인 예가 바로 CLR이다. CLR은 Common Language Runtime으로써 CLI 규약 대로 만든 윈도우즈 용 CLI의 구현이다.

이 CLI 규약을 보면 Virtual Machine (CLR 같은 놈)을 위한 중간 단계의 언어를 명시하고 있는데 이것이 바로 MSIL (Microsoft Intermediate Language)이다. 이렇게 만들어진 중간 단계의 언어는 프로그램이 시작하는 시점에 작동을 하는 JIT (Just-in-time) 컴파일러에 의해서 이해되고 분석되어 진 후 내가 현재 사용하고 있는 컴퓨터의 환경에 맞는 적합한 machine code(실행 코드)를 생성해 낸다. 고로 어떤 언어로 내가 프로그램을 작성했건 간에 그 코드는 CLI 규약에 따라 구현된 눈에 보이지 않는 .NET Framework의 CLR과 같은 놈을 통해 중간 언어 (MSIL)로 바뀌어진 다음 실행이 이루어지는 그 순간 (마우스 더블 클릭하는 순간) JIT라 불리우는 컴파일러에 의해 내 환경에 맞는 코드로 재 생성되서 나와서 실행이 이루어 진다는 말.

또한 .NET Framework에는 CTS (Common Type System)이라 불리우는 것이 있는데 이것은 말 그대로 CLI 명세를 따라 구현된 모든 언어가 공통으로 가지는 기본 타입의 구현 및 임의 타임의 구현 약속이다. 고로 C#으로 작성한 코드의 기본 타입들은 Visual Basic이나 C++과 같은 다른 언어로 작성된 기본 타입과 기본적으로 일맥상통하므로 CLR의 입장에서는 다른 언어를 이용한 두 소스 코드를 이해하는데 아무런 문제가 없다는 말.

고로 고 레벨 언어간의 융화와 결합이 용이하고 보다 높은 수준의 안전성 체크 및 형변환을 지원함으로써 한 단계 높은 수준의 프레임워크를 제공한다는 것. 게다가 CLR은 기본적으로 강화된 보안 옵션 (예를 들자면 어셈블리를 공개키와 개인키로 묶는 것)을 제공하며 눈에 띄는 변화가 바로 GC (Garbage Collector)의 등장이다. 

가비지 컬렉터의 등장으로 과거에 사용자에게 메모리 해제의 책임을 전가하는 것에서 벗어나 이제 사용자는 두다리 쭈욱 뻣고 마음껏 새로운 인스턴스를 남발해도 되는 상황이 왔다는 것. 물론 100% 그렇다는 건 아니고... 아직도 여전히 사용자에게 많은 책임이 있으므로 메모리 해제는 늘 머릿 속에 염두해두어야 할 문제다. 어쨌든 가비지 콜렉터는 착하다는 거... ㅋㅋ 


posted by 대갈장군

제목이 무척 도발적이다. C++/CLI 10분만에 배우기... 글 읽는데만 10분 넘게 걸리는데 어떻게 10분만에 배운다는 거냐...

참고로 출처는 http://www.codeproject.com/Articles/19354/Quick-C-CLI-Learn-C-CLI-in-less-than-10-minutes (저자: Elias Bachaalany)

기본적으로 이 글의 취지는 C++/CLI를 빠르게 시작하기 위한 글이다. 고로 독자들이 C++이랑 .NET 배경지식이 어느정도 있다고 가정하고 글을 쓴다고 한다. 글을 일기전에 자고있는 당신의 머리 세포들을 깨워줄 좋은 글도 있다고 추천해주는 저자의 센스! 


아래에 포함되는 예제를 실행하려면 다음과 같이 하면 된다.

"C:\Program Files\Microsoft Visual Studio 8\Common7\Tools\vsvars32.bat"

요로코롬 하면 저 명령 이후로는 Visual Studio 2005 x86을 이용하여 컴파일을 하게 된다.


그리고 어떤 파일을 컴파일 하고 싶다면 다음과 같이...

cl your_file.cpp /clr

뒤에 붙은 /clr 스위치가 정겨워 보인다...


So, What is C++/CLI?

이것에 대한 대답은 이미 다른 글에서 많이 했는데 일단 이 글을 쓴 저자의 말을 옮겨 보면, C++은 알다시피 C의 Superset (상위셋)이라고 할 수있다. C++은 C에 OOP (객체 지향성) 및 Template를 추가한 것이다. 그렇다면 CLI는 무엇인가?


CLI은 Common Language Infrastructure의 줄임말로 그 뜻은 다음과 같다.

It is an open specification that describes the executable code and runtime environment that allows multiple high-level languages to be used on different computer platforms without being rewritten for specific architectures.

CLI라 함은 실행 코드와 런타임 환경을 정의하는 오픈된 명세로써 많은 고레벨 언어가 운영체제가 다른 플랫폼에서 코드의 재작성 없이 사용될 수 있도록 해주는 것이란다.


Handles and Pointers

아마도 한번이라도 C++/CLI를 써 본 사람이라면 '^' 요 마크가 친숙 할 것이다. 변수 선언시에 꼭 따라 붙는 놈이다. 알다시피 C++에서는 포인터를 선언할 때 * 마크를 사용한다. C++/CLI에서는 ^를 이용해 핸들을 선언한다. 자, "*" 는 Native Pointer를 말하고 이것은 CRT 힙에 상주한다. 반면 "^"는 핸들을 선언하는 것이고 "Safe Pointer"라 불리며 Managed 힙 공간에 상주한다. 이 핸들이라고 하는 녀석은 단순히 생각해서 C++의 레퍼런스와 같은 개념이긴 하지만 C++과는 다른게 (Native 와는 다른게) 제대로 해제하지 않더라도 메모리 리크 (유출)이 발생하지 않는다. 왜냐면 가비지 컬렉터 (GC)가 알아서 메모리를 수거해주기 때문이다. 오우 나이스!


특정 클래스나 값 타입의 레퍼런스를 만들기 위해서는 gcnew라는 키워드를 사용해서 선언해야 한다. 다음과 같이..

System::Object ^x = gcnew System::Object();

nullptr이 흔히 사용하는 null의 CLI 버전이고 ^ 심볼과 함께 % 심볼도 사용된다. 


N* pn = new N; // allocate on native heap
N& rn = *pn; // bind ordinary reference to native object
R^ hr = gcnew R; // allocate on CLI heap
R% rr = *hr; // bind tracking reference to gc-lvalue

C++에서 *과 &이 한 묶음 이듯이 CLI에서는 ^이랑 %가 한 묶음이다. 


Let us get started: Hello World

자 이제 가장 단순한 프로그램 하나 만들어보자. 

#using <mscorlib.dll>

using namespace System;

int main(array<System::String ^> ^args)
{
  System::Console::WriteLine("Hello world");
  return 0;
}

다른 건 뭐 다 봐줄만 한데 main의 입력 함수로 들어오는 놈이 예사롭지 않아보인다. 하지만 C++ 코드를 떠올려보면 저것이 의미하는 것이 그렇게 복잡한 것만은 아니다. 


Classes and UDTs

Classes

클래스를 어떻게 선언하는지 일단 살펴보자. 해줘야 할 것은 단 한가지 뿐. Protection Modifier (Private / Public) 바로 다음에 ref 키워드만 붙이면 끄읏!

public ref class MyClass
{
private:
public:
  MyClass()
  {

  }
}

헐 이렇게 쉬울 수가? 이렇게 선언된 클래스를 네이티브 클래스로 생성하려면 원래 사용하던 방식 그대로 클래스를 선언하면 그만이다. 자, 이제 궁금한 것은 C++/CLI 도 파괴자를 가지고 있느냐 하는 것인데 정답은 Yes다. 하지만 컴파일러는 파괴자 호출을 IDispose 인터페이스를 투명하게 구현한 다음 Dispose() 함수로 유도하게 된다. 추가적으로 GC에 의해 호출되는 Finalizer라고 불리는 녀석이 있는데 이것은 !MyClass() 와 같이 C++ 파괴자 함수 스타일이다. 이 Finalizer에서 사용자는 파괴자가 호출되었는지 체크해봐야 한다.


#using <mscorlib.dll>

using namespace System;

public ref class MyNamesSplitterClass
{
private:
  System::String ^_FName, ^_LName;
public:
  MyNamesSplitterClass(System::String ^FullName)
  {
    int pos = FullName->IndexOf(" ");
    if (pos < 0)
      throw gcnew System::Exception("Invalid full name!");
    _FName = FullName->Substring(0, pos);
    _LName = FullName->Substring(pos+1, FullName->Length - pos -1);
  }

  void Print()
  {
    Console::WriteLine("First name: {0}\nLastName: {1}", _FName, _LName);
  }
};

int main(array<System::String ^> ^args)
{
  // local copy

  MyNamesSplitterClass s("John Doe");
  s.Print();

  // managed heap

  MyNamesSplitterClass ^ms = gcnew MyNamesSplitterClass("Managed C++");
  ms->Print();

  return 0;
}

Value types

Value type이라 함은 기본으로 제공되는 타입말고 추가로 만들어내는 타입들을 말한다. 모든 value type들은 System::ValueType으로 부터 파생된다. 이 value type들은 스택에 저장될 수 있고 = 연산자를 통해서 할당 가능하다.


public value struct MyPoint
{
  int x, y, z, time;
  MyPoint(int x, int y, int z, int t)
  {
    this->x = x;
    this->y = y;
    this->z = z;
    this->time = t;
  }
};

Enums

마찬가지로 열거형도 다음과 같이 정의 가능하다.


public enum class SomeColors { Red, Yellow, Blue};

혹은 엘리먼트의 타입을 정의할 수도 있다.


public enum class SomeColors: char { Red, Yellow, Blue};


Arrays

배열의 선언은 사실 복잡해 보이지만 다음과 같이 쉽다.


cli::array<int> ^a = gcnew cli::array<int> {1, 2, 3};


기존 C++과의 차이라면 array<> 키워드가 쓰이고 있고 ^ 심볼로 핸들 지정을 하며 new 대신 gcnew를 사용한다는 점.


또 다른 형태의 배열 선언 방식은 다음과 같다.


array<int> ^a = gcnew array<int>(100) {1, 2, 3};

처음 방식보다 단순화 되었는데 cli 네임스페이스 명시를 빼고 100개의 배열을 선언한 다음 처음 3개만 값 1,2,3으로 초기화 했다. 각각의 배열 요소에 접근하려면 foreach를 다음과 같이 사용할 수 있다.


for each (int v in a)
{
  Console::WriteLine("value={0}", v);
}

자, 이제 4x5x2 형태의 3차원 배열을 선언해보자... 초기값은 모두 0.


array<int, 3> ^threed = gcnew array<int, 3>(4,5,2);

Console::WriteLine(threed[0,0,0]);

오오오... 생각보다 쉽다. 


이제 스트링 클래스를 배열로 선언해보자..


array<String ^> ^strs = gcnew array<String ^> {"Hello", "World"}


이런 형태를 초기화 하기 위해서는 다음과 같이 하면 된다.


array<String ^> ^strs = gcnew array<String ^>(5);
int cnt = 0;

// We use the tracking reference to access the references inside the array
// since normally strings are passed by value

for each (String ^%s in strs)
{
    s = gcnew String( (cnt++).ToString() );
}


나와 있는 설명처럼 % 심볼이 사용되었는데 그 이유는 일반적으로 스트링의 경우 값으로 전달되기 때문에 레퍼런스를 이용해야 메모리로 직접적인 접근이 가능하기 때문이다. 



Parameter Arrays

헛, 이것도 가능했단 말인가? C에서 printf() 함수와 같이 파라미터가 들어가는 함수의 사용도 가능하다. 단, 사용할 수 있는 조건은 C와 동일하다. 


using namespace System;

void avg(String ^msg, ... array<int> ^values)
{
  int tot = 0;
  for each (int v in values)
    tot += v;
  Console::WriteLine("{0} {1}", msg, tot / values->Length);
}

int main(array<String ^> ^args)
{
  avg("The avg is:", 1,2,3,4,5);
  return 0;
}


Properties

C#에서 사용되는 형태의 프로퍼티

public ref class Xyz
{
private:
  int _x, _y;
    String ^_name;
public:
  property int X
    {
      int get()
        {
          return _x;
        }
        void set(int x)
        {
          _x = x;
        }
    }
  property String ^Name
  {
    void set(String ^N)
    {
      _name = N;
    }
    String ^get()
    {
      return _name;
    }
  }
};


Wrapping Around a Native C++ Class

자, 이제 C++ class를 어떻게 CLI에서 감싸는지 보자


// native class

class Student
{
private:
  char *_fullname;
  double _gpa;
public:
  Student(char *name, double gpa)
  {
    _fullname = new char [ strlen(name+1) ];
    strcpy(_fullname, name);
    _gpa = gpa;
  }
  ~Student()
  {
    delete [] _fullname;
  }
  double getGpa()
  {
    return _gpa;
  }
  char *getName()
  {
    return _fullname;
  }
};

위 코드는 Native class의 한 예다. 이것을 C++/CLI 클래스로 바꾸려면 다음의 가이드를 따르면 된다.

  • Managed Class를 만들고 그것의 멤버 변수가 Native Class를 가리키도록 해라
  • 생성자 혹은 다른 적절한 위치에서 Native Class를 Native Heap (new 키워드를 사용하여) 에 생성해라
  • 생성자를 호출할 때 필요한 변수와 입력 인자를 전달하라. 이때 몇몇 입력 값은 Unmanaged 에서 Manged Type으로 Marshal (변환)이 필요할 것
  • Managed Class로 부터 드러내고 싶은 함수의 Stub를 생성하라
  • Managed Class의 파괴자에 Native Pointer 를 지우는 것을 잊지 마라 (메모리 누수)

자, 이제 위 Native Class의 C++/CLI 버젼을 보자.


// Managed class

ref class StudentWrapper
{
private:
  Student *_stu;
public:
  StudentWrapper(String ^fullname, double gpa)
  {
    _stu = new Student((char *) 
           System::Runtime::InteropServices::Marshal::StringToHGlobalAnsi(
           fullname).ToPointer(), 
      gpa);
  }
  ~StudentWrapper()
  {
    delete _stu;
    _stu = 0;
  }

  property String ^Name
  {
    String ^get()
    {
      return gcnew String(_stu->getName());
    }
  }
  property double Gpa
  {
    double get()
    {
      return _stu->getGpa();
    }
  }
};


중간에 좀 후덜덜한 부분이 있긴 하지만 그래도 생각보다 어렵진 않다.


Wrapping Around C Callbacks

여기서는 예제용으로 EnumWindows() API 함수를 Wrap 할 건데 다음이 코드의 핵샘이다.

  • Manged class를 하나 만드는데 Delegates와 함께 만들거나 Native Callback에 도달하면 호출되는 함수랑 만들거나.
  • 방금 만든 Managed Class로 향하는 레퍼런스를 가지는 Native Class 만든다. 이것은 vcclr.h 헤더에 있는 gcroot_atuo를 이용하면 가능하다
  • 이제 Native C Callback 함수를 만들고 넘길때 Context Parameter (우리 예제에서는 lParam) 로 넘긴다. (Native Class로의 포인터)
  • 이제 Native Callback 내부에서 Native Class 인 Context를 가지고 있으므로 이제 Managed Class로 향하는 레퍼런스를 얻어올수 있고 필요한 함수를 호출 할 수 있다.

using namespace System;

#include <vcclr.h>


// Managed class with the desired delegate

public ref class MyClass
{
public:
  delegate bool delOnEnum(int h);
  event delOnEnum ^OnEnum;

  bool handler(int h)
  {
    System::Console::WriteLine("Found a new window {0}", h);
    return true;
  }

  MyClass()
  {
    OnEnum = gcnew delOnEnum(this, &MyClass::handler);
  }
};


이제 Native Class를 만들어보자. 이 클래스 내부에는 Managed Class 레퍼런스 (m_clr)이 존재하고 직접적인 Callback 프로시져가 있다.


class EnumWindowsProcThunk
{
private:
  // hold reference to the managed class

  msclr::auto_gcroot<MyClass^> m_clr;
public:

  // the native callback

    static BOOL CALLBACK fwd(
    HWND hwnd,
    LPARAM lParam)
  {
      // cast the lParam into the Thunk (native) class,
      // then get is managed class reference,
      // finally call the managed delegate

      return static_cast<EnumWindowsProcThunk *>(
            (void *)lParam)->m_clr->OnEnum((int)hwnd) ? TRUE : FALSE;
  }

    // Constructor of native class that takes a reference to the managed class

  EnumWindowsProcThunk(MyClass ^clr)
  {
    m_clr = clr;
  }
};

마지막으로 실질적으로 사용하는 코드는,


int main(array<System::String ^> ^args)
{
  // our native class

  MyClass ^mc = gcnew MyClass();

    // create a thunk and link it to the managed class

  EnumWindowsProcThunk t(mc);

    // Call Window's EnumWindows() C API with the pointer
    // to the callback and our thunk as context parameter

  ::EnumWindows(&EnumWindowsProcThunk::fwd, (LPARAM)&t);

  return 0;
}


The Other Way Round: From Manged to C Callbacks

이 것은 더 쉽다. 왜냐면 이미 Managed Class 내부에 Native Class로 향하는 포인터를 가지고 있기 때문이다. 다음과 같이 하면 된다.

  • Native Callback를 트리거 (발생)시킬 적절한 Delegates를 가지는 Managed Class를 만든다
  • Native Class (Callback 을 포함하는)와 이전의 Managed Class (이벤트 발생시키는 놈)을 연결해줄 Managed Class를 만든다
  • Callback을 핸들링 할 Native Class를 만든다
예제용으로 TickGenerator라는 Managed Class를 만들었는데 이 TickGenerator라는 클래스는 OnTick이라는 이벤트를 만들어내고 INativeHandler 클래스 (인터페이스) 가 Managed Class인 TickGeneratorThuck로 부터 호출된다. MyNativeHandler 클래스는 사용자 자신의 핸들러을 어떻게 셋팅하는지 보여주기 위한 INativeHandler의 간단한 구현이다.


Tick Generator Delegate는 다음과 같다


public delegate void delOnTick(int tickCount);

그리고 Managed tick generator class는 다음과 같다.


ref class TickGenerator
{
private:
  System::Threading::Thread ^_tickThread;
  int _tickCounts;
  int _tickFrequency;
  bool _bStop;

  void ThreadProc()
  {
    while (!_bStop)
    {
      _tickCounts++;
      OnTick(_tickCounts);
      System::Threading::Thread::Sleep(_tickFrequency);
    }
  }

public:
  event delOnTick ^OnTick;

  TickGenerator()
  {
    _tickThread = nullptr;
  }

  void Start(int tickFrequency)
  {
    // already started

    if (_tickThread != nullptr)
      return;

    // p.s: no need to check if the event was set,
    // an unset event does nothing when raised!

    _tickCounts = 0;
    _bStop = false;
    _tickFrequency = tickFrequency;

    System::Threading::ThreadStart ^ts = 
      gcnew System::Threading::ThreadStart(this, &TickGenerator::ThreadProc);
    _tickThread = gcnew System::Threading::Thread(ts);
    _tickThread->Start();
  }
  
  ~TickGenerator()
  {
    Stop();
  }

  void Stop()
  {
    // not started?

    if (_tickThread == nullptr)
      return;
    _bStop = true;

    _tickThread->Join();
    _tickThread = nullptr;
  }
};


#pragma unmanaged
// Create a simple native interface for handling ticks

// Native classes implement this class to add custom OnTick handlers

class INativeOnTickHandler
{
public:
  virtual void OnTick(int tickCount) = 0;
};


class MyNativeHandler: public INativeOnTickHandler
{
public:
  virtual void OnTick(int tickCount)
  {
    printf("MyNativeHandler: called with %d\n", tickCount);
  }
};


#pragma managed
// Create the managed thunk for binding between the native
// tick handler and the tick generator managed class

ref class TickGeneratorThunk
{
private:
  INativeOnTickHandler *_handler;
public:
  TickGeneratorThunk(INativeOnTickHandler *handler)
  {
    _handler = handler;
  }

  void OnTick(int tickCount)
  {
    _handler->OnTick(tickCount);
  }
};


int main(array<System::String ^> ^args)
{
  // Initiate the native handler

  MyNativeHandler NativeHandler;

  // Create the tick generator class

  TickGenerator ^tg = gcnew TickGenerator();

  // Create the thunk and bind it with our native handler

  TickGeneratorThunk ^thunk = gcnew TickGeneratorThunk(&NativeHandler);

  // Bind the ontick event with the thunk's onclick event

  tg->OnTick += gcnew delOnTick(thunk, &TickGeneratorThunk::OnTick);

  // Start the tick generator

  tg->Start(1000);

  // Wait for user input

  Console::ReadLine();

  // Stop the generator

  tg->Stop();

  return 0;
}


글을 다 쓰고 보니 좀 글의 내용과 제목이 어울리지 않는다는 생각이 든다. 애초에 C++/CLI를 빠르게 습득하는 것을 타이틀로 했다면 특정 언어간의 호출에 치중하기 보다는 기본적인 타입의 사용과 다양한 예제를 통해 보다 일반적이고 기본적인 개념 설명을 하는 것이 더 좋았을듯 하다... 아무튼, 일단 글을 퍼오기는 했으니 저장은 하지만 썩 마음에 들지는 않는다. 물론 초반에 간단간단하고 명료한 설명은 참 좋았다...




posted by 대갈장군
2013. 4. 25. 01:30 프로그래밍/MSDN

출처: http://msdn.microsoft.com/ko-kr/magazine/cc163681(en-us).aspx


젠장, 이건 한국어 선택해도 한국어로 안나오는 페이지네... 결국 해석해야 할듯...


C++/CLI is a self-contained, component-based dynamic programming language that, like C# or Java, is derived from C++. Unlike those languages, however, we have worked hard to integrate C++/CLI into ISO-C++, using the historical model of evolving the C/C++ programming language to support modern programming paradigms. You can say that C++/CLI is to C++ as C++ is to C. More generally, you can view the evolution leading to C++/CLI in the following historical context:
C++/CLI는 'Self-contained (스스로 모두 내포하고 있는 - 즉, 완전체)'하고 컴포넌트에 기반한 동적 프로그래밍 언어다. 마치 C# 또는 자바처럼 C++에 근간을 두고 있는 언어들 처럼 말이다. 하지만 그런 언어들 (C#이나 Java)과는 달리 우리 (Microsoft사)는 C++/CLI를 ISO-C++ (표준 C++을 말함) 형태로 만들기 위해서 무척이나 노력했단다... 뭐 쉽지 않았을 것 같다. C++ 에서 C++/CLI로 가는 것은 C에서 C++로 가는 것 만큼이나 큰 변화였다는 이야기. 다음 문서들을 보게 되면 얼마나 큰 변화가 있는지 알거다... 허허.. 그걸 언제 다 읽어봐.... 제일 마지막꺼만 읽어보면 괜춘할듯.
  • BCPL (Basic Computer Programming Language)
  • B (Ken Thompson, original UNIX work)
  • C (Dennis Ritchie, adding type and control structure to B)
  • C with Classes (~1979)
  • C84 (~1984)
  • Cfront, release E (~1984-to universities)
  • Cfront, release 1.0 (1985-to the world )—20th birthday
  • Multiple/Virtual Inheritance (MI) programming (~1988)
  • Generic Programming (~1991) (templates)
  • ANSI C++/ ISO-C++ (~1996)
  • Dynamic Component programming (~2005) (C++/CLI)

What is C++/CLI?
C++/CLI represents a tuple. C++ refers, of course, to the C++ programming language invented by Bjarne Stroustrup at Bell Laboratories. It supports a static object model that is optimized for the speed and size of its executables. However, it doesn't support run-time modification of the program other than heap allocation. It allows unlimited access to the underlying machine, but very little access to the types active in the running program and no real access to the associated infrastructure of that program. Herb Sutter, a former colleague of mine at Microsoft and the chief architect of C++/CLI, refers to C++ as a concrete language.
CLI refers to the Common Language Infrastructure, a multitiered architecture supporting a dynamic component programming model. In many ways, this represents a complete reversal of the C++ object model. A runtime software layer, the virtual execution system, runs between the program and the underlying operating system. Access to the underlying machine is fairly constrained. Access to the types active in the executing program and the associated program infrastructure—both as discovery and construction—is supported. The slash (/) represents a binding between C++ and the CLI. The details surrounding this binding make up the general topic of this column.
So, a first approximation of an answer to what is C++/CLI is that it is a binding of the static C++ object model to the dynamic component object model of the CLI. In short, it is how you do .NET programming using C++ rather than C# or Visual Basic®. Like C# and the CLI itself, C++/CLI is undergoing standardization under the European Computer Manufacturers Association (ECMA) and eventually under ISO.
The common language runtime (CLR) is the Microsoft version of the CLI that is specific to the Windows® operating system. Similarly, Visual C++® 2005 is the implementation of C++/CLI.
As a second approximation of an answer, I would say that C++/CLI integrates the .NET programming model within C++ in the same way as, back at Bell Laboratories, we integrated generic programming using templates within the then existing C++. In both of these cases your investment in an existing C++ codebase and in your existing C++ expertise are preserved. This was an essential baseline requirement of the design of C++/CLI.
자, 이제부터 시작이다. 머리 싸매고 읽어보자. C++/CLI는 튜플이다. 여기서 튜플이라 함은 내 생각에 서로 다른 타입이 하나의 오브젝트로 뭉쳐져 있다는 의미로 해석된다. 즉, C++과 CLI 라는 서로 다른 두 타입을 하나로 합쳐놓은 짬뽕이라는 것. C++/CLI이라는 단어에서 C++ 부분은 당연히 유명하신 Bjarne Stroustrup께서 Bell 연구소에서 만드신 것을 지칭하는 것이다. 이거 모르면 당신은 간첩? 이 C++은 기본적으로 실행파일의 속도와 크기를 위해 최적화 되어 있는 'Static Object Model'을 지원한다. Static 이라는 단어가 좀 뜬금없이 들릴수 있다. 사실 내 생각에 이 Static이라는 단어는 상대적인 의미라고 본다. 과거에 없었던 현재 시점에 어떤 기술에 비해 Static (정적)하다는 것이지 C++이 나왔을 당시에는 대박 혁명적 언어였다...

아무튼, 조금 있다 설명할 CLI를 이해하면 왜 C++이 기분나쁘게 '정적'이라고 불리는지 알게 될 것이다. 자, C++의 문제점은 Heal Allocation (힙 메모리 할당)을 제외하고는 Run-time에 변경을 허락하지 않는다. 자, Run-Time이라 함은 프로그램이 실행된 직후부터 멈추는 순간까지를 말한다. 고로 C++은 컴퓨터 자체 하부 구조에는 이른바 'Unlimited Access' 즉 무한 접근을 허락하지만 실행중인 프로그램에는 아주 미미하고 제한적인 접근만을 허락하며 프로그램이 작동하기 위해 사용하는 Infrastructure (예를 들자면 Windows 같은거)에는 실질적인 접근을 제공하지 않는다. Herb Sutter 이라는 내 친구가 있는데 이 친구가 C++/CLI를 설계를 책임진 사람이란다. 아무튼 이 친구가 말하기를 "C++은 Concrete Language다" 라고 말했단다. 직역하자면 C++은 딴딴한 언어다....... 응?? 즉, 정적이다. 별로 유동적이 않다는 것.

이제 우리의 친구 CLI를 살펴보자.  CLI는 Common Language Infrastructure의 줄임말이다. 공용 언어 기반이란 뜻이네? 풀어서 설명하자면 동적 요소 프로그래밍(Dynamic Component Programming) 을 지원하는 멀티스레드를 하는 아키텍쳐란다. 이걸 단박에 이해한다면 당신은 이미 능력자. 이런 정의 자체가 이미 C++과 정반대된다. Runtime Software Layer 다른 말로는 가상 실행 시스템 (Virtual Execution System)은 프로그램과 프로그램을 실행하는 운영체제 가운데 낑겨 있는 녀석이다. 고로 컴퓨터 자체로 접근하는 것은 제한이 된다. (C++과 많이 다르다) CLI에서는 실행중인 프로그램의 타입으로 액티브하게 접근하는 것과 연관된 프로그램 infrastructure에 접근하는 것 모두 지원된다. C++/CLI 사이에 들어가는 /가 의미하는 것은 C++과 CLI의 결합을 의미한단다. 이것에 대해서 이 글에서 이야기 할꺼래.

자, 이제 C++/CLI가 뭐요? 라는 질문의 첫번째 대답을 하겠다. C++/CLI는 정적인 C++ Object Model을 동적인 Component Object Model 인 CLI와 결합한 것이다. 정적 객체 모델 C++ 더하기 동적 요소 객체 모델 CLI. 줄여서 말하자면, C#이나 Visual Basic 대신 C++ 이라는 언어를 사용해서 .NET programming을 하는 것을 말한다. 오오오... 정답! 마음에 드는 말이고 모든 것을 분명하게 선을 긋는 아주 정확한 정의다.

CLR (Common Language Runtime)은 Windows라는 운영체제를 위한 Microsoft사의 CLI라고 보면 된다. 이거 어렵게 들릴지 모르겠지만 내가 예전에 써놓은 .Net Framework에 대한 글을 보면 이해가 쉽게 될것이다. 

두번째 근접한 대답으로는 C++/CLI는 과거에 벨 연구소해서 C에서 C++로 진화할때 제네릭 프로그래밍을 C에 추가한 것 처럼 C++이라는 언어에 .NET 프로그래밍 모델을 추가한 것이다. 이로써 내가 열씸히 작성해 놓은 C++ 코드들이 다 무용지물이 되지 않게 되고 C++/CLI로 흘러감으로써 엄청난 이득을 취할 수 있겠다. 그럴까??? 정말로?? ㅋㅋ

Learning C++/CLI
There are three aspects in the design of a CLI language that hold true across all languages: a mapping of language-level syntax to the underlying Common Type System (CTS), the choice of a level of detail to expose the underlying CLI infrastructure to the manipulation of the programmer, and the choice of additional functionality to provide, beyond that supported directly by the CLI.
The first item is largely the same across all CLI languages. The second and third items are where one CLI language distinguishes itself from another. Depending on the kinds of problems you need to solve, you'll choose one or another language, or possibly combine multiple CLI languages. Learning C++/CLI involves understanding each of these aspects of its design.

CLI 언어의 디자인에는 모든 언어에 공통으로 적용되는 세가지 중요한 관점이있다.
첫째는 CTS (Common Type System)이라 불리는 .Net Framework가 지원하는 공통 타입 체계로 언어 레벨의 문법을 맵핑하는 것이고 둘째는 프로그래머에게 하부 구조인 CLI infrastructure로의 접근 디테일을 얼마만큼 드러낼 것인가이고 셋째는 CLI에 의해 직접적으로 지원되는 것을 넘어서는 추가적인 능력을 선택하는 것이다. 
일반적으로 첫번째 관점은 모든 CLI 언어에 공통적인 요소이다. 둘째와 셋째가 CLI 언어를 구분 짓게 되는 요소들인데 어떤 문제에 직면했느냐에 따라서, 또 어떤 언어를 선택하느냐에 따라서 당신은 여러개의 CLI 언어를 사용할지도 모른다. C++/CLI를 배운다는 것은 이 모든 세가지 디자인 관점을 이해하는 것을 필요로 한다. 즉, 이해력 만렙 찍어야 한다. 
참고로 CTS는 .NET Framework가 가지고 있는 한가지의 요소로써 서로 다른 언어들이 표현하는 같은 형태의 타입들을 모아모아 일관된 형태로 표현하는 일종의 명세(Specification)같은 거라고 생각하면 대략 괜찮다. 사실, 모든 발전된 형태의 언어나 프레임워크는 새로운 것이 아니라 기존의 것을 잘 포장하고 싸놓은 것이다. C++이 추상화와 상속을 통해 객체 지향을 하는 것처럼 .NET Framework도 언어들 간에 추상화와 단일화된 규약을 통해 통합을 시도한 것이라고 보면 된다. 

Mapping C++/CLI to the CTS?
It is important when programming C++/CLI to learn the underlying CTS, which includes these three general class types:
  • The polymorphic reference type, which is what you use for all class inheritance.
  • The non-polymorphic value type, which is used for implementing concrete types requiring runtime efficiency, such as the numeric types.
  • The abstract interface type, which is used for defining a set of operations common to a set of either reference or value types that implement the interface.
CTS는 다음과 같은 세가지 일반적 클래스 타입을 가진다.
- 다형적 레퍼런스 타입: 클래스 상속을 위해 사용되는 것
- 다형적이지 않은 값 타입: 이러한 녀석은 runtime 효율성을 위해서 필요한 concrete type들인데 예를 들자면 숫자 타입들 (int, float)
- 추상적 인터페이스 타입: 이것은 인터페이스를 구현하는 레퍼런스나 값 타입의 공통된 operation들을 정의하기 위해 사용되는 것
말은 참 어렵게 써놓긴 했는데 단순하게 생각하면 C++ 클래스의 상속과 추상 클래스를 떠올리면 거의 맞아 떨어진다. 

와우, 아래의 글을 좀 읽어봤는데 생각보다 내용이 깊다. 물론 CLI와 .NET Framework의 관계를 이해하는데는 엄청난 도움이 될 것이 분명하지만 이 글을 읽게된 이유는 코드 프로젝트 때문이기 때문에 일단 여기서 멈추고 나머지 부분은 차후에 번역해야 겠다.
This design aspect, the mapping of the CTS to a set of built-in language types, is common across all CLI languages although, of course, the syntax varies in each CLI language. So, for example, in C#, you would write
abstract class Shape { ... } // C#
to define an abstract Shape base class from which specific geometric objects are to be derived, while in C++/CLI you write
ref class Shape abstract { ... }; // C++/CLI
to indicate the exact same underlying CLI reference type. The two declarations are represented exactly the same way in the underlying IL. Similarly, in C#, you write
struct Point2D { ... } // C#
to define a concrete Point2D class, while in C++/CLI you write:
value class Point2D { ... }; // C++/CLI
The family of class types supported with C++/CLI represents an integration of the CTS with the native facilities, and that determines your choice of syntax. For example:
class native {};
value class V {};
ref class R {};
interface class I {};
The CTS also supports an enumeration class type that behaves somewhat differently from the native enumeration, and support is provided for both of those as well:
enum native { fail, pass }; 
enum class CLIEnum : char { fail, pass}; 
Similarly, the CTS supports its own array type that again behaves differently from the native array. And again Microsoft provides support for both:
int native[] = { 1,1,2,3,5,8 }; 
array<int>^ managed = { 1,1,2,3,5,8 };
No CLI language is closer to or more nearly a mapping to the underlying CTS than another. Rather, each CLI language represents a view into the underlying CTS object model.

CLI Level of Detail
The second design aspect that must be considered when designing a CLI language is the level of detail of the underlying CLI implementation model to incorporate into the language. What kind of problems will the language be tasked to solve? Does the language have the tools necessary to do this? Also, what sort of programmers is the language likely to attract?
Take, for example, the issue of value types occurring on the managed heap. Value types can find themselves on the managed heap in a number of circumstances:
  • Through implicit boxing, when an instance of a value type is assigned to an Object or when a virtual method is invoked through a value type that is not overridden.
  • When that value type is serving as a member of a reference class type.
  • When that value type is being stored as the element type of a CLI array.
Whether the programmer should be allowed to manipulate the address of a value type of this sort is a CLI language design consideration that must be addressed.

What Are the Issues?
Any object located on the managed heap is subject to relocation during the compaction phase of a sweep of the garbage collector. Any pointers to that object must be tracked and updated by the runtime; the programmer cannot manually track it herself. Therefore, if you were allowed to take the address of a value type that might be on the managed heap, there would need to be a tracking form of pointer in addition to the existing native pointer.
What are the trade-offs to consider? On the one hand, there's simplicity and safety. Directly introducing support in the language for either one or a family of tracking pointers makes it a more complicated language. By not supporting this, the available pool of programmers is expanded because less sophistication is required. In addition, allowing the programmer access to these ephemeral value types increases the possibility of programmer error—she may purposely or inadvertently do dangerous things to the memory. By not supporting tracking pointers, a potentially safer runtime environment is created.
On the other hand, efficiency and flexibility must be considered. Each time you assign the same Object with a value type, a new boxing of the value occurs. Allowing access to the boxed value type allows in-memory update, which may provide significant performance improvements. Without a form of tracking pointer, you cannot iterate over a CLI array using pointer arithmetic. This means that the CLI array cannot participate in the Standard Template Library (STL) iterator pattern and work with the generic algorithms. Allowing access to the boxed value type allows significant design flexibility.
Microsoft chose to provide a collection of addressing modes that handle value types on the managed heap in C++/CLI:
int ival = 1024;
int^ boxedi = ival; 

array<int>^ ia = gcnew array<int>{1,1,2,3,5,8};
interior_ptr<int> begin = &ia[0];

value struct smallInt { int m_ival; ... } si;
pin_ptr<int> ppi = &si.m_ival;
The typical C++/CLI developer is a sophisticated system programmer tasked with providing infrastructure and organizationally critical applications that serve as the foundation over which a business builds its future. She must address both scalability and performance concerns and must therefore have a system-level view into the underlying CLI. The level of detail of a CLI language reflects the face of its programmer.
Complexity is not in itself a negative quality. Human beings are more complicated than single-cell bacteria, and that is certainly not a bad thing. However, when the expression of a simple concept is made complicated, that is usually considered to be a bad thing. In C++/CLI, the CLI team has tried to provide an elegant way to express complex subject matter.

Additional Functionality
A third design aspect is a language-specific layer of functionality above and beyond what is directly supported by the CLI. This may require a mapping between the language-level support and the underlying implementation model of the CLI. In some cases, this just isn't possible because the language cannot intercede with the behavior of the CLI. One example of this is the virtual function resolution in the constructor and destructor of a base class. To reflect ISO-C++ semantics in this case would require a resetting of the virtual table within each base class constructor and destructor. This is not possible because virtual table handling is managed by the runtime and not by the individual language.
So this design aspect is a compromise between what would be preferable to do, and what is feasible. The three primary areas of additional functionality that are provided by C++/CLI are the following:
  • A form of Resource Acquisition is Initialization (RAII) for reference types, in particular, to provide an automated facility for what is referred to as deterministic finalization of garbage- collected types that hold scarce resources.
  • A form of deep-copy semantics associated with the C++ copy constructor and copy assignment operator; however, these semantics could not be extended to value types.
  • Direct support of C++ templates for CTS types in addition to the CLI generic mechanism. In addition, a verifiable version of the STL for CLI types is provided.
Let's look at a brief example: the issue of deterministic finalization. Before the memory associated with an object is reclaimed by the garbage collector, an associated Finalize method, if present, is invoked. You can think of this method as a kind of super-destructor since it is not tied to the program lifetime of the object. This is called finalization. The timing of just when or even whether a Finalize method is invoked is undefined. This is what is meant by nondeterministic finalization of the garbage collector.
Nondeterministic finalization works well with dynamic memory management. When available memory gets sufficiently scarce, the garbage collector kicks in and solves the problem. Nondeterministic finalization does not work well, however, when an object maintains a critical resource such as a database connection, a lock of some sort, or perhaps native heap memory. In this case, it would be great to release the resource as soon as it is no longer needed. The solution that is currently supported by the CLI is for a class to free the resources in its implementation of the Dispose method of the IDisposable interface. The problem here is that Dispose requires an explicit invocation, and therefore is not likely to be invoked.
A fundamental design pattern in C++ is the aforementioned Resource Acquisition is Initialization, which means that a class acquires resources within its constructor. Conversely, a class frees its resources within its destructor. This is managed automatically within the lifetime of the class object.
Here's what reference types should do in terms of the freeing of scarce resources:
  • Use the destructor to encapsulate the necessary code for the freeing of any resources associated with the class.
  • Have the destructor invoked automatically, tied with the lifetime of the class object.
The CLI has no notion of the class destructor for a reference type. So the destructor has to be mapped to something else in the underlying implementation. Internally, then, the compiler performs the following transformations:
  • The class has its base class list extended to inherit from the IDisposable interface.
  • The destructor is transformed into the Dispose method of IDisposable.
That represents half of the goal. A way to automate the invocation of the destructor is still needed. A special stack-based notation for a reference type is supported; that is, one in which its lifetime is associated within the scope of its declaration. Internally, the compiler transforms the notation to allocate the reference object on the managed heap. With the termination of the scope, the compiler inserts an invocation of the Dispose method—the user-defined destructor. Reclamation of the actual memory associated with the object remains under the control of the garbage collector. Figure 1 shows an example.
ref class Wrapper {
    Native *pn;
public:
    // resource acquisition is initialization
    Wrapper( int val ) { pn = new Native( val ); } 

    // this will do our disposition of the native memory
    ~Wrapper(){ delete pn; }

    void mfunc();
protected:

    // an explicit Finalize() method—as a failsafe
    !Wrapper() { delete pn; }
};

void f1() 
{
   // normal treatment of a reference type
   Wrapper^ w1 = gcnew Wrapper( 1024 );

   // mapping a reference type to a lifetime
   Wrapper w2( 2048 ); // no ^ token !

   // just illustrating a semantic difference
   w1->mfunc(); 
   w2.mfunc();

   // w2 is disposed of here
}

// 
// ... later, w1 is finalized at some point, maybe
C++/CLI is not just an extension of C++ into the managed world. Rather, it represents a fully integrated programming paradigm similar in extent to the earlier integration of the multiple inheritance and generic programming paradigms into the language. I think the team has done an outstanding job.

So, What Did You Say About C++/CLI?
C++/CLI represents an integration of native and managed programming. In this iteration, that has been done through a kind of separate but equal community of source-level and binary elements, including Mixed mode (source-level mix of native and CTS types, plus a binary mix of native and CIL object files), Pure mode (source-level mix of native and CTS types, all compiled to CIL object files), Native classes (can hold CTS types through a special wrapper class only), and CTS classes (can hold native types only as pointers). Of course, the C++/CLI programmer can also choose to program in the CLI types alone, and in this way provide verifiable code that can be hosted, for example, as a stored procedure in SQL Server 2005.
So, returning to the question, what is C++/CLI? It is a first-class entry visa into the .NET programming model. With C++/CLI, there is a C++ migration path not just for the C++ source base but for C++ expertise as well. I find great satisfaction in that.

posted by 대갈장군

Machine Learning에 대한 설명이 매우 잘 되어 있는 사이트.


http://openclassroom.stanford.edu/MainFolder/CoursePage.php?course=MachineLearning


다만 영어라는 점...

posted by 대갈장군
2013. 4. 13. 05:34 프로그래밍/MSDN

요즘 작업중인 프로젝트는 C++/CLI를 이용한 몇개의 프로젝트인데 중간 중간 C# 폼이 사용된다. 물론 Managed code와 Unmanaged code (Native code)를 함께 사용하기 위해서는 C++/CLI를 사용해야 한 다는 것 쯤은 나도 알고 있지만 왜 그럴까에 대한 진지한 생각은 해본적이 없다.


Stackoverflow 사이트를 돌아다니다 보니 좋은 글이라고 링크해 놓았길래 한번 번역해본다.


출처 - http://msdn.microsoft.com/en-us/magazine/dd315414.aspx


1. 언제 Managed-Native Interop이 사용되어야 하는가?

뭐 주절주절 써놓았는데 중간에 딱 한 문단이 마음에 확 와닿는다. 


These three applications address the three most common reasons to use interop: to allow managed extensibility of preexisting native applications, to allow most of an application to take advantage of the benefits of managed code while still writing the lowest-level pieces in native code, and to add a differentiated, next-generation user experience to an existing native application.


세가지 예시를 들면서 언제가 Managed 코드와 Native 코드를 섞어서 쓰는게 좋은가에 대해서 설명한 다음에 위와 같이 정의했다. 


"이러한 세가지 어플리케이션은 interop (Managed code와 Native code를 섞어쓰는 것을 말함)를 사용하는 가장 흔한 세가지 이유를 나타낸다: 원래 존재하던 native 어플리케이션에 managed 확장 가능성을 허락해 주는 것이 첫째이고, 둘째는 native 코드의 저레벨 코딩을 계속 쓰면서 동시에 managed code의 이점을 취할 수 있는 것이고, 마지막으로 셋째는 현존하는 native 어플리케이션에 차별화된, 또는 차세대 사용자 경험을 추가할 수 있기 때문이다."


영어를 한국어로 번역하다보면 적절한 한국어 찾기가 뜻을 이해하는 것보다 훨씬 더 어렵다. 젠장. 


2. Interop Technologies: 세가지 경우


There are three main interop technologies available in the .NET Framework, and which one you choose will be determined in part by the type of API you are using for interop and in part by your requirements and need to control the boundary. Platform Invoke, or P/Invoke, is primarily a managed-to-native interop technology that allows you to call C-style native APIs from managed code. COM interop is a technology that allows you either to consume native COM interfaces from managed code or export native COM interfaces from managed APIs. Finally there is C++/CLI (formerly known as managed C++), which allows you to create assemblies that contain a mix of managed and native C++ compiled code and is designed to serve as a bridge between managed and native code.


.NET 프레임워크에서는 세가지의 interop 기술들이 사용가능한데 다음과 같다.

첫째는 Platform Invoke 혹은 P/Invoke라고 불리는 기술인데 이것은 주로 managed code에서 C-style의 native APIs (C 함수들)를 불러다 쓰기 위한 용도다. 

둘째는 COM interop인데 이것은 native COM 인터페이스를 managed code로 부터 사용하거나 혹은 native COM 인터페이스를 managed APIs(함수들)로 부터 Export 하기 위해서 쓴다. 마지막으로 대망의 C++/CLI가 있는데 (과거에는 managed C++로 명명되었죠) 이것은 manged code와 native C++ code가 공존하는 형태로써 managed와 native 코드 간의 '다리'역활을 위한 용도로 디자인 되었다.


시간만 많으면 세가지 용도 모두에 대해서 쓰고 싶지만 일단 중요한게 C++/CLI이니까 그것만 자세히 살펴보겠다.


3. Interop Technologies: C++/CLI


C++/CLI is designed to be a bridge between the native and managed world, and it allows you to compile both managed and native C++ into the same assembly (even the same class) and make standard C++ calls between the two portions of the assembly. When you use C++/CLI, you choose which portion of the assembly you want to be managed and which you want to be native. The resulting assembly is a mix of MSIL (Microsoft intermediate language, found in all managed assemblies) and native assembly code. C++/CLI is a very powerful interop technology that gives you almost complete control over the interop boundary. The downside is that it forces you to take almost complete control over the boundary.
C++/CLI can be a good bridge if static-type checking is needed, if strict performance is a requirement, and if you need more predictable finalization. If P/Invoke or COM interop meets your needs, they are generally simpler to use, especially if your developers are not familiar with C++.
There are a few things to keep in mind when considering C++/CLI. The first thing to remember is that if you are planning to use C++/CLI to provide a faster version of COM interop, COM interop is slower than C++/CLI because it does a lot of work on your behalf. If you only loosely use COM in your application and don't require full fidelity COM interop, then this is a good trade-off.
If, however, you use a large portion of the COM spec, you'll likely find that once you add back the pieces of COM semantics you need into your C++/CLI solution, you'll have done a lot of work and will have performance no better than what is provided with COM interop. Several Microsoft teams have gone down this road only to realize this and move back to COM interop.
The second major consideration for using C++/CLI is to remember that this is only intended to be a bridge between the managed and native worlds and not intended to be a technology you use to write the bulk of your application. It is certainly possible to do so, but you'll find that developer productivity is much lower than in a pure C++ or pure C#/Visual Basic environment and that your application runs much slower to boot. So when you use C++/CLI, compile only the files you need with the /clr switch, and use a combination of pure managed or pure native assemblies to build the core functionality of your application.

C++/CLI는 원래 native와 managed 세계를 이어주는 브릿지 (다리) 역활 용으로 디자인 되었다. 그리고 이것은 managed 와 native C++ code 둘 다 동시에 하나의 공통 어셈블리 (심지어는 같은 클래스로) 컴파일 될 수 있게 해주었고 standard C++ 호출을 어셈블리의 두 영역간에 할 수 있게 해주었다. C++/CLI를 사용할때 당신은 어셈블리의 어떤 부분이 managed인지 혹은 native인지 결정할 수 있다. 이로써 결과물로 나오는 어셈블리는 MSIL (Microsoft intermediate language)와 native assembly 코드의 짬뽕이 된다. (MSIL은 managed code를 위해 사용되는 .NET Framework의 기반 언어 기술이다.) C++/CLI는 매우 강력한 interop 기술로써 당신에 거의 완벽에 가까운 interop 경계 컨트롤을 제공한다. (내 맘대로 왔다리 갔다리 할수 있다는 말). 문제는 모든 컨트롤을 제공하므로 일일이 내가 알아서 다 컨트롤 해야 한다.... 젠장 장점이 단점이네?ㅋㅋ


C++/CLI는 만약 어느 정도의 퍼포먼스가 요구되고 보다 예측 가능한 결과가 필요할 때, 그리고 static-type 체킹이 필요한 경우 좋은 다리 역활을 한다. 만약 P/Invoke나 COM interop이 내가 요구하는 것을 충족한다면 C++/CLI보다는 P/Invoke나 COM이 더 사용하기 쉽다. 특히 C++을 잘 모른다면... 더더욱.


C++/CLI을 사용할 때 명심해야 할게 몇가지 있다. 첫째는 C++/CLI가 COM interop보다는 빠르다는 점이다. (왜 같은 의미의 문장을 두번 이어서 썼는지 모르겠지만 암튼 C++/CLI가 COM보다 빠르다는 이야기) 왜냐면 COM은 사용자를 대신해서 많은 업무를 수행하기 때문이다. 만약 COM을 아주 약하게 사용한다면 (쬐끔만), 그러면 COM 쓰는 것도 괜찮다.


하지만 만약 COM의 많은 부분을 가져다 쓰게 되면 C++/CLI interop은 사실상 무용지물이 된다. 속도를 위해서 C++/CLI를 선택했으나 COM이 전체적인 퍼포먼스를 낮추기 때문에 애시당초 걍 COM interop으로 가는게 더 좋다는 말이네. 


C++/CLI를 사용할때 두번째 주의할 것은 이 interop은 다리 역활로 만들어진 것이지 프로그램을 그렇게 작성하시오라고 만든 것이 아니라는 점이다. 뭐 이건 당연한 이야기다. C++로 다 짤수 있는 프로그램을 굳이 C++/CLI로 짜서 다른 managed code랑 썪어 버릴 필요가 없다는 말이다. 이 점이 사실 뼈아프게 들리는 이유는 C++로 작성된 거대한 프로그램에서 몇가지 추가하고 싶은 기능이 있는데 그것이 C#과 같은 managed code로 작성하는 것이 내 입장에서는 더 편하게 느껴질때가 종종 있다. 이때 좀 갈등을 하는데 사실 그냥 좀 불편하더라도 Windows API를 이용하는 것이 C#을 이용하는 것보다 나은것 같다.


C#의 문제점은 폼안에 존재하는 각각의 컨트롤이 폼이 소유하고 있는 스레드를 제외하고는 스레드에 안전하지 않다는 점이다. 즉, C#은 설계 당시부터 다른 언어 (특히 C++)와의 섞어 쓰기에 대해 큰 고려를 안했던 것 같다. 물론 돌아가는 방법을 제공하기는 하지만 좀 완벽하지 못하다.


오늘은 여기까지만 쓰고 다음에 나머지 부분에 대해서 마무리하도록 해야겠다.

posted by 대갈장군

DWORD DoSomething() 

{

__try

{

// Do something

}


__finally

{

// Release Global Resource

if(!AbnormalTermination())

{

// __try went well 

// No problem

}

else

{

// Global or local unwinding happended due to error

// Global unwinding: Memory access violation

// Local unwinding: continue; break;

}

}

}

posted by 대갈장군
2012. 12. 18. 03:51 프로그래밍/C++


int _tmain(int argc, _TCHAR* argv[])

{

       TCHAR * str = NULL;

       __try

       {

              str = (TCHAR *)malloc(20);

       }

       __finally

       {

              if(str != NULL)           

              {

                     free(str);

                     str = NULL;

              }

       }

       return 0;

}


예외 처리는 안전한 소프트 웨어를 만들기 위한 기본인데 몇가지 주의해야 한다.


우선 __try 내부로 들어온 다음 __finally 를 무조건 통과해서 나가는 것이 기본적인데, 주의해야할 점은 어떤 알 수 없는 오류로 인해 __try 내부의 메모리 할당이나 리소스 할당이 실패하는 경우, __finally 내부에서 무조건 메모리나 리소스를 해제 하려고 하면 '선언'은 되었지만 '초기화'가 되지 않은 포인터의 경우에는 "Run-Time Check Failure #3 - The variable 'str' is being used without being initialized." 라는 에러가 발생한다.


초기화 되지 않았으므로 알 수 없는 값을 가지고 있어서 그렇다. 이런 문제를 막으려면 애당초 포인터를 선언 할 때, NULL 값을 넣어주면 __finally 내부에서 if 로 NULL인지 아닌지를 체크하는 문법이 문제가 없이 잘 동작한다.


예외 처리를 잘 해야 하는 또 다른 이유는 하드웨어나 예상치 못한 문제로 프로그램이 이상 작동을 할 때 프로그래머의 잘못이 아님을 보여줄 수 있는 최후의 방패이기 때문이기도 하다.





posted by 대갈장군
2012. 12. 18. 03:08 프로그래밍/Windows API

나중에 Copy & Paste 신공을 위해서 코드를 적어둠...


DWORD err;                    // Error Code

TCHAR errMes[1024];      // Error Message Buffer


// Do something here that can fail


err = GetLastError();


FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, err, 0, errMes, 1024, NULL);


wsprintf(str, "Error Code = %d, Message = %s", err, errMes);


MessageBox(hWnd, str, "ERROR", MB_OK");


posted by 대갈장군
2012. 12. 15. 04:59 프로그래밍/C++

http://msdn.microsoft.com/en-us/library/dd293575(v=vs.100).aspx


오, 생각보다 간단하게 어플리케이션 재배포 패키지를 만드는 방법이 있넹? 뭐 아주 완벽하지 않지만 간단하고 빠르게 설치파일을 생성해 낸다는 점에서 꽤 괜찮은듯.  


우선, 내가 만든 프로그램 이름이 MyMFCApplication.exe라고 치자. 그러면 우선적으로 만들어야 할 파일이 setup.bat 파일이다. 이 파일에 다음과 같은 문장을 때려 넣자.

@echo off
vcredist_x86.exe
mkdir "C:\Program Files\MyMFCApplication"
copy MyMFCApplication.exe "C:\Program Files\MyMFCApplication"


뭐 간단히 예기하자면 도스 모드에서 실행될 명령어를 주욱 적어 놓은 것인데, 우선 vcredist_x86.exe파일을 실행하라는 말이고 이어서 "C:\Program Files\MyMFCApplication" 폴더를 생성하라는 말이고 다음으로는 내가 만든 어플리케이션을 복사해서 거기다 집어 넣으라는 말이다.


이게 왜 필요한지는 조금 있다 알게된다.


일단 cmd 윈도우를 열고 iexpress.exe를 치고 엔터! 그러면 아래와 같은 녀석이 나타난다.



Create new Self Extraction Directive File을 선택한 후 Next!


그 다음에는 Extract files and run an installation command 선택 후 Next!


그 다음에는 내 프로그램의 이름을 묻는데 내가 원하는 이름을 적어 놓고 Next!


그리고 Confirmation prompt 페이지에서는 No Prompt를 선택하고 Next!


다음으로 License agreement인데 Do not display a license를 선택 하고 Next!


이제 Packaged files 페이지에 도착했는데 여기서 이제 내가 만든 프로그램과, vcredist_x86.exe 파일과 아까 만들어 둔 setup.bat 파일을 넣어준다. 참, vcredist_x86.exe 파일은 Program Files 폴더 아래에 \Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\vcredist_x86  에 가면 찾을 수 있다.



 

 위 그림에 보면 내가 만든 프로그램은 빠져 있는데 넣는걸 잊어 먹지 말자.. ㅋㅋ


그리고 다음을 눌러보면 Install Program to Launch 페이지에 도착하는데 거기서 Install Program 텍스트 박스에다가 다음과 같이 넣어준다.


cmd.exe /c "setup.bat"





뭐, 저 명령은 cmd (도스 윈도우) 띄워서 setup.bat 파일 실행하라는 말이다. 


그 다음으로 Show Window 페이지에서는 Default 선택 후 Next!


Finished message 페이지에서는 No message 선택 후 Next!


그 다음인 Package Name and Option Pages에서는 내가 원하는 Setup 파일 이름을 적되, 아래 옵션에서 Store files using Long File name inside Package 옵션을 반드시 켜고 둘째로 파일 이름의 끝에 Setup을 꼭 넣어줘라. (예를 들자면 MyMFCApplicationSetup.exe)





이제 거의 끝이 보인다... 그리고 Configure restart 페이지에서는 No restart 선택 후 Next! (설치후 재시작 할꺼냐는 말)


Save Self Extraction Directive 페이지에서는 Save Self Extraction Directive (SED) file 선택 후 Next!


그리고 최종적으로 Create package 페이지에서 Next 클릭!


이제 내가 iexpress.exe를 실행했던 폴더를 가보면 뭔가 이상한 파일이 하나 만들어져 있을 것이다. 





두둥. 이것이 인스톨 파일이다! 짜짠...


이걸 이제 내가 설치하고자 하는 컴퓨터에 복사해서 더블 클릭해보면 우선 vcredist_x86.exe가 실행되면서 해당 컴퓨터에 Visual C++ redistribution 패키지 (DLL파일들)를 자동 설치하게 된다.


그 다음에는 프로그램 폴더에 자동으로 폴더를 생성해서 내가 만든 어플리케이션을 거기다 복사해 준다. 필요한 파일들이 더 있다면 복사 명령을 더 setup.bat에 더 써넣고 패키지에 필요 파일들을 더 넣어주면 될것 이다.


왜 이런 방법이 있는지 몰랐지? ㅋㅋ 역시 MSDN은 길을 잃고 헤멜 필요가 있는 미로다. 




posted by 대갈장군
2012. 12. 15. 04:17 프로그래밍/C++

http://msdn.microsoft.com/en-us/library/8kche8ah(v=vs.100).aspx


이글은 Visual Studio 2010 C++을 겨냥한 것임을 미리 밝혀둔다.


알다시피 내 컴퓨터에서 작성한 프로그램이 다른 컴퓨터에서 잘 작동할 것이라는 핑크빛 꿈을 꾸는 경우가 많은데 현실은 정반대다. 그렇다면 내가 내 컴퓨터에서 작성한 코드가 다른 사람 컴퓨터에서 잘 돌아가게 하려면 무슨짓을 해야 할까? 정답은 DLL을 프로그램이랑 같이 포장해서 보내줘야 한다는 것인데, 어떤 DLL이 필요한지는 어떻게 알까? 


그중에 한 방법이 Dependency Walker를 이용해 내가 만들 어플리케이션의 필요 DLL들을 검색해보는 것이다.


사진을 첨부하려 했는데 첨부가 안되네~ 아무튼, Dependency Walker를 사용하면 해당 프로그램에 필요한 DLL 목록이 주루루룩 뜬다. 뭐 경고 중에는 가볍게 무시해도 괜찮은 것들도 많지만 Delay-load Dependency나 Explicit (Dynamic) Dependency가 아닌데 못찾겠다고 나오는 DLL들은 뭔가 문제가 있을 확률이 높으니 눈여겨 봐야 할 필요가 있다.

 

본인이 작성한 프로그램이라면 어떤 LIB 파일이 프로그램에 링크 되었는지 알고 있으므로 운영제체나 .NET 프레임워크에서 공통적으로 지원하는 공유 DLL을 제외하고 어떤 기타 DLL이 반드시 필요한지도 일반적으로는 알고 있어야 정상이다.

 

다음 표를 보면 어떤 DLL이 어떤 경우에 필요한지에 대해서 간단하게 설명하고 있다.

 

Visual C++ Library

Description

Applies to

atl100.dll

Active Template Library (ATL).

Applications that use ATL.

msvcr100.dll

C Runtime Library (CRT) for native code.

Applications that use the

 C Run-Time Libraries.

msvcp100.dll

Standard C++ Library for native code.

Applications that use the 

Standard C++ Library.

mfc100.dll

Microsoft Foundation Classes (MFC) Library.

Applications that use

 the MFC Library.

mfc100u.dll

MFC Library with Unicode support.

Applications that use the

 MFC Library and require 

Unicode support.

mfcmifc80.dll

MFC Managed Interfaces Library.

Applications that use the 

MFC Library with

 Windows Forms Controls.

mfcm100.dll

MFC Managed Library.

Applications that use the

 MFC Library with

 Windows Forms Controls.

mfcm100u.dll

MFC Managed Library with Unicode support.

Applications that use the

 MFC Library with 

Windows Forms Controls and

 require Unicode support.

 

그런데 마지막 문단에 보면 이런 말이 있다. 일반적으로 시스템이 제공하는 DLL들 예를 들자면 Kernel32.dll, User32.dll, Ole32.dll 같은 놈들은 원래 재배포가 필요가 없다. 왜냐면 윈도우즈를 쓰는 모든 컴퓨터에는 기본적으로 저놈들이 있게 마련이기 때문이다. 하.지.만! 문제는 윈도우가 여러 버젼이 있다는 점이다. 윈도우 7과 윈도우 7 서비스팩 1과는 많은 차이가 있다. 고로 이런 경우 같은 운영체제이다고 하더라도 프로그램이 안돌아 갈수 있다는 점! 고로, 반드시 업데이트를 통해서 업그레이드 하거나 아니면 마이크로소프트에서 제공하는 각종 리디스트리뷰션 패키지 (재배포 설치 파일들)를 통해서 필요한 부분을 설치해야 한다!

 

저거 생각보다 중요한 점이라는 점!










posted by 대갈장군
2012. 12. 14. 03:48 프로그래밍/Dependency Walker

정상적인 경우의 아이콘들


bullet

에러가 없는 정상적인 모듈들


bullet

Duplicate module. 이 모듈은 이미 어디선가 처리되어 임포트 되어 있는 모듈. 


bulletForwarded module. 다른 모듈을 호출하는 Forwarded module

bullet

Delay-load module. 런타임에 실질적으로 함수가 호출되면 로드 되는 '지연 로드 모듈'


bullet

Dynamic module. 다이나믹 모듈. 소스코드에서 LoadLibrary()를 통해 호출되는 경우를 말한다. 누가 호출했는지 모를경우에는 아마도 Profiling을 하면 찾을 수 있는듯.


bullet

다이나믹 모듈이긴 한데 LoadLibraryEx 함수를 이용하되 DONT_RESOLVE_DLL_REFERENCES플래그나 LOAD_LIBRARY_AS_DATAFILE 플래그가 셋 된 상태로 호출된 경우를 말한다. 


bullet

64-bit module. 64 운영 체제용 모듈이라는 말.



에러나 경고


bulletMissing module. 모듈이 없음. 검색 경로를 검사해 봤지만 모듈이 없는 경우. 

bullet

Invalid module. 유효하지 않은 모듈. 


bullet

Module warning. 경고! 이 모듈은 부모 모듈에서 요구하는 하나 혹은 그 이상의 호출 함수가 없는 경우이거나 또는 CPU 타입이 틀렸거나, 또는 로드 될때 초기화에 실패한 경우다. Parent Import Function List View를 이용하는 빠진 함수가 어떤 놈인지 확인 가능하다.


bullet

Delay-load module warning. 이 모듈은 부모 모듈에서 요구하는 한개 이상의 호출 함수가 없는 경우이거나 잘못된 CPU 타입인 경우이다. 마찬가지로 Parent Import Function List View를 이용하면 빠진 함수가 어떤 녀석인지 확인 가능하다. 


bulletDynamic module warning. 이 놈도 마찬가지인데 다만 이것은 그냥 경고에서 그칠 경우가 많다. 왜냐면 다이나믹하게 연결하기 때문에 실행중에는 문제없이 돌아갈 수 있다는 점. 

 

Dependency Walker는 수많은 에러와 경고 메세지를 출력해 내는데 몇몇은 아주 중요하고 나머지는 무시해도 되거나 문제가 되지 않는 놈들이다. 대부분의 에러 메세지는 두 개중에 하나다. Load-time failure 혹은 Run-time failure.


Load-time failure이라 함은 어플리케이션이 시작도 못해보고 실패한다는 말이다. 어플리케이션을 실행하기 위해 꼭 필요한 모듈들이 없거나 문제가 있어서 시작도 못했다는 것. 정확하게 말하자면 implict나 forward 의존을 하는 모듈들이 존재 하지 않거나 잘못되어 운영체제가 함수를 호출하는 entry point를 전혀 얻어내지 못한 경우다. 또 다른 형태의 load-time failure로는 윈도우가 기반이 아닌 모듈을 호출하거나 CPU 타입이 다른 모듈을 호출하거나 하는것이다. 


자, 몇몇 에러 메세지를 보자. 


bulletThe dynamic link library BAR.DLL could not be found in the specified path...

bulletThe procedure entry point FOO could not be located in the dynamic link library BAR.DLL.

bulletThe application or DLL BAR.DLL is not a valid Windows image.

bulletThe application failed to initialize properly.

bulletInitialization of the dynamic link library BAR.DLL failed. The process is terminating abnormally.

bulletThe image file BAR.EXE is valid, but is for a machine type other than the current machine.


대부분의 load-time 문제들은 Dependency Walker를 통해 즉각적으로 탐지 가능하다. Dependency Walker를 이용하면 implicit, forward 그리고 delay-load dependency를 바로 체크하여 문제가 있으면 알려준다. Implicit와 Forward 의존의 경우 실행이 아예 불가능하고 delay-load 의존의 경우에는 load-time에 필요하지는 않아서 일단 프로그램이 돌아가더라도 문제가 있는 모듈을 호출하는 순간 프로그램이 뽁 날 것이다. 


Run-time 의존적 모듈들은 프로그램이 실행된 후에 연결 (로딩) 된다. 일반적으로 이런 경우는 소스 코드 내에서 LoadLibrary 같은 함수를 호출함으로써 이루어진다. 일단 모듈이 로딩 된 후에는 어플리케이션이 GetProcAddress 함수를 이용해서 특정한 함수의 위치를 얻어낸다. 만약 어플리케이션이 아주 안전하게 작성이 되어서 실패를 핸들링 할 수 있다면 경고는 무시된다.


Run-time 의존을 사용하는 이유는 많이 있다. 우선 load-time 퍼포먼스가 증가한다. 즉, 실행하려고 더블클릭 하는 순간의 시간을 단축할 수 있다는 말. 왜냐면 필요할때 연결하면 되니까. 예를 들면 프린팅 하는 함수를 가진 모듈은 사용자가 정말로 프린트를 하고 싶을때 연결하면 된다. 그리고 또 다른 장점으로는 모듈이나 함수가 존재하지 않는 경우에 사용할 수 있다. 무슨말이냐 하면, 윈도우 NT 특유의 함수를 호출하고자 한다고 치자. 만약 이 모듈을 암묵적으로 포함하게 되면 이 어플리케이션이 윈도우 XP에서 실행되는 경우, 해당 모듈을 없으므로 에러가 터지고 실행이 안된다. 하지만 런타임으로 연결하면 일단 해당 컴퓨터에 필요로 하는 모듈이 있는지 없는지가 실행중에 파악이 된다. 고로 없으면 없는대로 다른 메세지나 처리를 통할수 있다. 암묵적으로 연결하면 아예 실행이 안된다.


런타임 의존은 두 가지 타입이 있다. 하나는 Explicit dependency (혹은 Dynamic dependency) 그리고 나머지 하나는 Delay-load dependency 이다. 명시적 의존은 (Explicit Dependency) 어플리케이션이 실행되는 도중에 아무때나 로딩될 수 있는 것이다. 그래서 어떤 명시적 의존이 어플리케이션에서 사용되는지 확인 하는 방법은 프로그램을 실행해보는 방법밖에 없다. (혹은 profiling) 명시적 의존은 어플리케이션이 LoadLibrary 함수와 GetProcAddress 함수를 코드 내에서 호출할 때 비로소 발생한다.


Delay-load 의존들은 사실 Explicit 의존처럼 구현되지만 헬퍼 라이브러리와 링커가 대부분의 일을 한다. 대부분의 윈도우 모듈들은 자신의 모듈안에 import table이라 불리는 호출 함수들의 리스트를 가지고 있다. 이 테이블은 링커에 의해서 작성되고 운영체제에 의해 사용되는데 주어진 모듈의 어떤 모듈들이 implicit인지 혹은 forward 의존인지를 알아내는데 사용된다. 이 리스트에 있는 모듈중에 찾을 수 없는 모듈은 당연히 어플리케이션의 실패를 초래한다. 만약 내가 링커에게 하나의 모듈을 delay-load 의존으로 연결하라고 지시하면, 그 모듈의 정보를 import table에 저장하는 대신, 그 모듈의 정보를 분리된 delay-load import table에 저장한다. 그리고 실행 시 (Run-time)에 만약 하나의 모듈이 delay-load 의존 모듈을 호출하게 되면 이 호출은 헬퍼 라이브러리에 의해 처리된다. 헬퍼 라이브러리는 LoadLibrary 함수를 이용해 모듈을 로드하고 GetProcAddress 함수를 이용해 해당 모듈로의 함수 진입점을 불러온다. 일단 이 과정이 끝나면 해당 함수를 호출한 모듈은 무슨일이 일어난건지 전혀 모르는 채 계속 하던일을 자연스럽게 하게 된다. 이러한 일련의 과정이 한바탕 치러지고 나서 나중에 호출되는 같은 함수나 혹은 같은 모듈 내의 다른 함수들은 헬퍼 라이브러리를 거치지 않고 바로 즉각적으로 해당 함수로 접근이 가능하다. 


Delay-load 헬퍼 라이브러리는 만약 실패가 있거나 오류가 있으면 유저에게 알려주는 메커니즘을 가지고 있다. Explicit 의존 (Dynamic 의존) 처럼 만약 어플리케이션이 이러한 오류나 실패에 이미 준비를 하고 있는 상황이라면 문제가 없다. 


요약하자면 ImplicitForward Dependencies 경우에는 반드시 모듈이 찾을 수 있는 위치에 존재해야 하며 또한 에러나 경고가 없어야 한다. ExplicitDelay-load Dependencies의 경우에는 모듈이 반드시 찾을 수 있는 위치에 있어야 하는 것도 아니고 또한 부모 모듈이 사용하고자하는 모든 함수를 다 Export 해야 하는 것도 아니다. 하지만, 만약 어플리케이션이 없는 Explict 모듈이나 Delay-load 모듈을 실행중에 사용하려고하면 이것은 Run-time Failure, 즉 실행중 프로그램 정지 (Crash)로 나타날 수 있다. Dependency Walker는 사용자가 소스 코드 내부에서 이러한 오류에 대한 방지책을 마련했는지 알 수 없으므로 모든 발생 가능한 오류에 대해 알려준다. 만약 어플리케이션이 아무 문제없이 잘 돌아가면 대부분의 경고 메시지는 무시해도 된다. 하지만 만약 어플리케이션이 실패하는 경우, 경고는 아마도 무엇이 실패를 가져오게 했는지에 대한 고찰을 하게 도와 줄 수 있을 것이다.


오호, Dependency Walker는 First and Second chance Exception에 대해서도 경고를 해준단다. 엑세스 바이얼레이션 (잘못된 메모리 참조) 같은 오류가 어플리케이션에서 발생하면 어플리케이션은 해당 예외를 처리할 한번의 기회를 얻는다. 이것을 흔히 First Chance Exception이라 한다. 만약 어플리케이션이 그 예외를 잘 처리하고 문제가 없으면 오케이지만 만약 어플리케이션이 이 예외를 처리하지 못하면 이것이 Second Chance Exception이 된다. 이 경우에는 주로 어플리케이션이 완전히 뽀싸진다. 크래쉬! 일반적으로 Second Chance Exception이 발생하면 운영체제는 사용자에게 다이얼로그 박스를 열어서 '야, 프로그램 엉망이라서 닫는다!'라고 알려주고 종료해야 한다고 말한다. 나에게는 종종 있는 일이다 ㅋㅋㅋ


Dependency Walker는 항상 이 Second Chance Exception을 기록한다. 물론 First Chance Exception도 기록할 수 있다. 많은 어플리케이션이 First Chance Exception을 일으키도록 작동하고 또 처리하고 한다. 고로 이것은 '완전 나쁜' 징후로 보지는 않는다. 









posted by 대갈장군
2012. 12. 14. 00:47 프로그래밍/Dependency Walker

의존성 검사 프로그램인 Dependency Walker는 다음과 같은 타입의 의존성을 검사할 수 있다.


1. Implicit Dependency (다른 말로는 load-time dependency): 굳이 직역하자면 '암묵적 의존'이라고 할 수 있겠다. 모듈 A가 컴파일 혹은 링크시에 모듈 B를 사용하기 위해 하나의 라이브러리 파일 (LIB)을 암묵적으로 내포했다면 모듈 A의 소스코드는 실질적으로 모듈 B의 함수들을 불러다 사용하는 것이다. 고로 모듈 B는 모듈 A의 '로드 타임 디펜던시'이다. 한국어로 설명하자면 모듈 B는 모듈 A를 실행하는 시점에 반드시 로딩 되어야 하는 필수적인 놈이다라는 말... 고로 모듈 B에 있는 함수를 실행중 (Run-time)에 사용하고 말건 간에 모듈 A는 반드시 모듈 B를 로딩해야 한다. 모듈 B는 모듈 A의 import table에 포함된다. 


2. Delay-load Dependency: '지연 로딩 의존'. 이 녀석은 1번과 거의 동일한데 한 가지 중요한 차이점이 있다면 모듈 A가 진짜로 모듈 B의 함수를 호출하면 그제서야 모듈 B를 로딩한다는 점! 고로 이름이 'Delay-load' (늦게 로딩)이다. 이 경우에는 모듈 A의 'Delay-load import table'에 모듈 B가 포함된다. 


3. Forward Dependency: '전진형 의존' (가수 전진 아님) ㅋㅋ 한국어로 바꾸자니 참 어이없네. 이 녀석의 경우 모듈 A가 모듈 B를 컴파일 혹은 링크시에 LIB 파일로 연결하고 모듈 A의 소스코드가 모듈 B의 함수를 호출하는 경우를 말하는데, (여기까지는 1과 2와 같다) 모듈 B의 함수가 실질적으로는 또 다른 모듈인 모듈 C의 함수를 연결해서 호출하는 경우를 말한다. 모듈 B와 모듈 C 둘 다 실질적으로 모듈 A의 필요한 라이브러리들이지만 모듈 B만 모듈 A의 import table에 이름을 올린다.


4. Explicit Dependency (혹은 다른 말로 다이나믹 의존 or 런타임 의존): '명시적 의존' 모듈 A가 모듈 B와 컴파일 혹은 링크 타임에 연결되지 않았으나 런타임에 모듈 A가 동적으로 모듈 B를 LoadLibrary() 같은 함수를 통해 소스코드 내부에서 임의로 호출하여 연결하는 경우를 말한다. 자, 이런 경우에는 모듈 B는 모듈 A의 'Run time dependency'이다. 즉, 모듈 B는 모듈 A를 실질적으로 실행하는 시점에 요구되는 것이라는 말. 하지만 모듈 A의 import table에는 모듈 B가 당연히 없다. 왜냐면 소스 코드 내부에서 임의로 사용자 맘대로 호출하니까. 이런 타입의 대표적인 것들이 OCXs, COM object 그리고 Visual Basic 어플리케이션들이다. 


5. System Hook Dependency (다른 말로 '주입된 의존'): 이 타입의 의존성은 다른 어플리케이션이 특정 이벤트를 프로세스 내에서 '후킹'할때 발생한다. 그 프로세스가 해당 이벤트를 발생시킬 때, 운영체제는 해당 이벤트를 처리하기 위해 해당 프로세스로 모듈을 'Inject' 즉, '주입'하게 된다. 이렇게 주입된 모듈을 다른 어떤 모듈에 의존적이진 않지만 주입된 프로세스의 주소 공간에 머무른다. 


1,2 그리고 3번의 경우는 쉽게 찾아낼 수 있다. 왜냐면 import table에 자신들의 이름을 당당히 밝히기 때문이다. 4번과 5번은 찾아내기 쉽지 않은데 Dependency Walker 2.0이후 버전의 Runtime Profiling을 이용하면 검색 가능하단다. 


http://www.dependencywalker.com/help/html/dependency_types.htm

posted by 대갈장군
2012. 12. 14. 00:09 프로그래밍/MSDN

http://msdn.microsoft.com/en-us/library/dd293574(v=vs.100).aspx


Visual C++ 2010에서 작성한 C++ 프로그램을 다른 컴퓨터에서 실행하기 위해서는 작성한 어플리케이션, 그리고 어플리케이션을 돌리기 위해 필요한 라이브러리 파일을 같이 제공해야만 한다. 요렇게 하기 위한 방법이 세가지 있다.


1. Central Deployment 

중앙 배포 방식이다. 컴퓨터의 운영체제가 설치된 하드 디스크의 Windows 폴더 안에 보면 System32 폴더가 있다. 요 폴더에 들어가면 각종 DLL들이 무수히 많은데 바로 여기가 윈도우즈 운영체제가 사용하는 '중앙 통제 센터'이다. 여기다가 어플리케이션을 돌리는데 필요한 DLL들을 복사해 넣으면 내가 만든 어플리케이션이 문제 없이 작동한다. 바로 이때 사용되는 것이 VCRedist_archtecture.exe 파일인데 인터넷에서 Visual C++ 2010 Redistribution Package 라고 치면 좌롸롸롹 뜨는 것들이다. 단순히 exe 파일이므로 그냥 다운 받아서 더블 클릭하면 알아서 설치한다. 


이 방식의 장점은 중앙의 통제된 한 곳에 모든 것을 다 집어 넣기 때문에 업데이트나 갱신이 용이하다는 점이다. 보안 취약점이 발견되었을때 단순히 DLL 파일 하나를 업데이트 함으로써 그 DLL을 사용하는 모든 프로그램의 보안 취약점을 한 방에 해결할 수 있다.


2. Local Deployment

지역 배포 방식이다. 어플리케이션이 존재하는 폴더 내부에 필요한 파일들을 복사해서 넣는 방법이다. 


이 방식은 쉽고 간단하지만 문제점으로는 업데이트가 자동으로 안되므로 유지 보수를 위해서는 DLL을 업데이트 하는 방법을 제공해야 한다는 점...


3. Static Linking

이건 정적 링크 방식이다. 어플리케이션 자체에 라이브러리를 심어버리는 방식이다. 장점은 당연히 DLL 문제에서 완전 해방 된다는 점이다. 다만 문제점은 마이크로소프트 업데이트가 만약 내가 링크한 라이브러리를 업데이트 해버리면 어플리케이션이 업데이트된 라이브러리를 사용할 수 없다는 점이다. 결론적으로 보안 취약성이 문제이군.


참, 그리고 재미있는 부분이 있는데, Visual C++ 2008과 2010의 가장 큰 차이점을 설명하고 있는데, 재미있는군.


우선, Visual C++ 라이브러리는 더이상 manifest에 의존하지 않는다. 그리고 더이상 WinSxS 폴더에 설치되지 않는다. 헐, 그래? 그러고 보니 2010으로 컴파일한 실행 파일 폴더에 manifest 가 없네? 


또한 작성된 응용 프로그램이 더이상 manifest 정보를 요구하지 않는단다. 흠, 그렇다면 필요한 라이브러리의 정보가 어디에 포함되는 걸까? 명세서 (manifest) 파일이 존재 하지 않는다면 아마도 실행파일 내부에 어딘가 설명되는가 본데?

posted by 대갈장군
2012. 12. 13. 05:40 프로그래밍

앞서 커널 오브젝트에 대해서 알아보았는데 이중에 가장 사용하기 쉬운 녀석이 이벤트 오브젝트이다. 


우선 이벤트 오브젝트를 설명하기 앞서 대기 함수에 대해서 설명하자. 대기 함수라 함은 인자로 전달한 커널 오브젝트가 시그널 상태 (파란불)가 될 때까지 (혹은 타임 아웃 제한 시간동안) 이 함수를 호출한 스레드를 대기상태로 빠뜨린다. 


바로 WaitForSingleObject() 함수이다. 원형은 다음과 같다.

DWORD WINAPI WaitForSingleObject(
  _In_  HANDLE hHandle,
  _In_  DWORD dwMilliseconds
);

첫번째 인자로 전달된 hHandle (커널 오브젝트의 핸들)이 시그널 상태가 될때까지 기다리되 두번째 인자로 전달된 시간만큼 최대한 기다렸다가 만약 제한 시간안에 소식이 없으면 그냥 포기하고 대기 상태를 푼다.


커널 오브젝트가 정상적으로 시그널 상태가 되어 대기 함수를 깨우는 경우에는 WaitForSingleObject() 함수의 리턴 값은 WAIT_OBJECT_0가 된다. 만약 기다리다 지쳐 포기하고 리턴하는 경우네는 WAIT_TIMEOUT이 리턴된다. 


고로 다음과 같은 형식의 코드로 작성이 가능하다.


DWORD dwWaitResult;

dwWaitResult = WaitForSingleObject(ghReadyEvent, 5000);

 

if (dwWaitResult == WAIT_OBJECT_0) 

{


}


자 그러면 이제 대표적인 커널 오브젝트인 이벤트 오브젝트를 이용하는 방법을 보자. 우선 이벤트 오브젝트를 생성하는 방법은 다음과 같다.

HANDLE WINAPI CreateEvent(
  _In_opt_  LPSECURITY_ATTRIBUTES lpEventAttributes,
  _In_      BOOL bManualReset,
  _In_      BOOL bInitialState,
  _In_opt_  LPCTSTR lpName
);


여기서 주의해야 할 부분은 두 번째 인자와 세 번째인데 이것은 '수동 리셋 이벤트 (TRUE)' 로 이벤트 오브젝트를 만들지 아니면 '자동 리셋 이벤트 (FALSE)'로 만들지 설정하는 부분이다. 세 번째 인자는 초기 상태를 시그널 상태 (TRUE)로 만들지 아니면 논시그널 상태 (FALSE)로 만들지 설정하는 부분이다.


수동 리셋 이벤트는 시그널 상태가 되면 대기중인 모든 스레드가 시그널 상태가 되는 반면 자동 리셋 이벤트를 대기중인 스레드 중 한개만 신호상태가 되어 CPU를 점유하고 나머지는 계속 대기한다.


일반적으로 시작하면서 바로 파란불로 만드는 경우는 드물기 때문에 세 번째 인자는 FALSE로 간다고 보고 두 번째 인자는 사용자의 의도에 따라 TRUE 혹은 FALSE로 셋팅해야 한다.


그리고 이제 중요한 함수인 시그널 상태 혹은 논시그널 상태로 이벤트 오브젝트를 만들어주는 함수인데 바로 SetEvent() 와 ResetEvent() 함수이다.


SetEvent()와 ResetEvent() 함수 둘 다 커널 오브젝트인 이벤트 오브젝트의 핸들을 인자로 받아 상태를 변경한다. 


이름이 쪼금 헷갈리게 되어 있다. 왜 SetEvent 대신 GoEvent() 라던가 ReadyEvent() 라든지 좀 더 직관적으로 이해 되기 쉬운 함수 이름을 사용 안했는지 모르겠다.


암튼, SetEvent() 함수를 호출하게 되면 전달된 이벤트 오브젝트를 시그널 상태 (즉, 파란불: 준비되었다)로 변경한다. 반면 ResetEvent()는 이벤트 오브젝트를 논시그널 상태 (즉, 빨간불: 아직 멀었어!)로 변경한다.


이제 실제 코드 예제를 보자.


1. 이벤트 오브젝트 핸들 선언

HANDLE ghHDFaceReadyEvent;       


2. 이벤트 오브젝트의 생성 (자동 리셋, 초기 논시그널 상태)

ghHDFaceReadyEvent = CreateEvent(NULL, false, false, TEXT("FRE")); 


3. 작업 스레드에서 해당 이벤트 오브젝트의 시그널 상태를 기다림 (무제한 시간동안)

dwWaitResult = WaitForSingleObject(ghHDFaceReadyEvent, INFINITE); 


4. 필요한 시점에서 해당 이벤트 상태를 시그널 상태로 변경

SetEvent(ghHDFaceReadyEvent);


5. 작업 스레드가 시그널 상태가 된 것을 확인하고 바로 작업에 착수한다. 그리고 모든 일이 끝나면 이벤트를 논시그널 상태로 바꾼다. 물론 자동 리셋 이벤트의 경우 리셋 굳이 안해도 된다. 수동인 경우는 해줘야 다음 루프에서 대기 없이 바로 작업에 착수하는 것을 막을 수 있다.

if (dwWaitResult == WAIT_OBJECT_0) 

{

    ResetEvent(ghHDFaceReadyEvent);

}


스레드의 동기화가 필요한 프로그래밍에서는 이벤트를 이용한 동기화가 제법 효과적이다. 테스트를 해보진 않았지만 프로세스간에 커널 오브젝트가 공유 되므로 서로 다른 프로그램끼리 같은 이벤트 핸들을 이용해서 서로 작업이 완료되었거나 준비되었음을 알려주면 아주 멎진 멀티-프로세스 프로그램을 만들수 있을 것 같다. 하지만 굳이 그렇게 분리된 형태의 프로그램을 작성해야 할 필요가 있나 하는 의문이 들긴 하지만... 뭐 어쨌든 된다고 하니까 어딘가 필요하지 싶다. 


다만 주의해야 할 것은 대기함수를 호출할때 대기 시간을 무제한으로 하게 되면 코딩의 오류나 어떤 문제로 인해 이벤트 오브젝트가 시그널 상태가 영원히 되지 않으면 대기함수를 호출한 스레드는 영원히 잠자게 된다. 데드락은 아니지만 대략 좋지 않은 상태다... 이걸 주의 해야 한다.


posted by 대갈장군
2012. 12. 13. 04:43 프로그래밍

http://msdn.microsoft.com/en-us/library/windows/desktop/ms724485(v=vs.85).aspx


Kernel Object (줄여서 KO) 핸들은 프로세스 한정적이다. 즉, 각각의 프로세스마다 고유하다는 말인데, 그말인 즉슨, 임의의 프로세스는 하나의 커널 오브젝트 핸들을 얻기 위해서는 반드시 오브젝트를 생성하거나 혹은 이미 존재하고 있는 오브젝트를 열어야 한다는 말이다. 하나의 프로세스 당 만들수 있는 최대의 커널 핸들의 갯수는 2의 24승이다. 뭐, 무제한이라고 보면 되겠네. 그러나, 핸들들은 Paged Pool에 저장되므로 결론적으로 사용가능한 메모리에 따라 생성할 수 있는 핸들의 갯수는 제한된다. 32 비트 윈도우즈 시스템의 경우 생성할 수 있는 핸들의 갯수는 2의 24승 보다 훠어얼씬 적다. 


프로세스는 오브젝트의 이름과 접근 권한을 가지고 있다면 이미 존재하고 있는 커널 오브젝트에 새로운 핸들을 할당하는 것이 가능하다. 심지어 그 커널 오브젝트가 다른 프로세스에 의해 만들어 졌다고 해도 가능하다. 커널 오브젝트 핸들은 그 자체에 접근 권한을 설정하는 속성을 가지고 있어서 접근을 허용 또는 차단을 할 수 있다. 보안 속성이라고도 한다. 각각의 커널 오브젝트는 고유의 접근 권한을 가지는데 예를 들자면 이벤트 핸들 같은 경우에는 'Set' 혹은 'Wait'를 가질 수 있고 파일 핸들 같은 경우에는 'Read' 혹은 'Write' 같은 접근 권한을 가질 수 있다.


아래의 그림을 보면 어플리케이션이 하나의 이벤트 오브젝트를 생성하는데 CreateEvent 함수가 이벤트 오브젝트를 생성하고 오브젝트 핸들을 리턴하고 있다.



Application creating an event object


이벤트 오브젝트가 생성된 후, 어플리케이션은 이벤트 핸들을 이용해 이벤트를 Set 혹은 Wait 시킬 수 있다. 이 핸들은 어플리케이션이 핸들을 닫거나 어플리케이션 자체가 종료될 때 까지 유효하다. 


대부분의 커널 오브젝트들은 하나의 오브젝트를 위해 다수의 핸들을 제공할 수 있다. 예를 들자면 아래 그림처럼 CreateEvent로 핸들을 하나 생성한 후 다시 OpenEvent로 같은 이벤트 오브젝트를 위한 핸들을 생성할 수 있다는 말이다. 


Application creating an event object with multiple handles


이런 방법으로 어플리케이션은 여러개의 핸들에게 다른 속성의 접근 권한을 부여 할 수 있다. 예를 들자면 Handle 1은 Set과 Wait 모두를 할 수 있게 접근 권한을 주고 Handle 2는 오로지 Wait만 가능하도록 설정 가능 하다는 말이다.


만약 다른 프로세스가 어떤 이벤트의 이름과 그 이벤트로의 접근 권한을 가지고 있다면 그 프로세스는 자신만의 이벤트 오브젝트 핸들을 OpenEvent 함수를 이용해서 생성 할 수 있다. DuplicateHandle을 이용하면 같은 프로세스 내에서 핸들을 복사 하여 생성할 수도 있고 다른 프로세스로 복사해서 전달 할 수도 있다.


하나의 오브젝트는 최소한 한개의 오브젝트 핸들이 존재하는 한 메모리에 계속 상주한다. 아래 그림처럼 어플리케이션이 CloseHandle함수를 사용해서 모든 오브젝트 핸들을 닫게 되면 비로서 오브젝트는 메모리상에서 사라지게 된다. 

Application closing event object handles to remove object from memory


흠, 이것은 마치 COM 오브젝트가 스스로를 관리하는 카운터를 가지고 있다가 0이되면 자폭하는 것과 같은 원리다. 제법 능동화된 메모리 관리 기법이지만 제대로 해제하지 않으면 어플리케이션이 종료되기 전까지는 절대 사라지지 않는 메모리 누수를 가지는 위험성이 존재한다.


윈도우 시스템은 다른 커널 오브젝트와는 조금 다른 방식으로 파일 오브젝트를 관리한다. 파일 오브젝트는 파일 포인터를 가지고 있다. 이 포인터는 파일내부의 읽혀질 혹은 쓰여질 다음 바이트의 위치를 가리키는 포인터이다.  어플리케이션이 새로운 파일 핸들을 생성할 때 시스템은 새로운 파일 오브젝트를 생성한다. 그러므로 하나 이상의 파일 오브젝트는 디스크 상의 한개의 파일을 가리킬 수 있는데 다음 그림과 같다. 


Multiple file objects referring to a file on disk


Duplication 이나 Inheritance (상속)을 통하면 한개 이상의 파일핸들이 같은 파일 오브젝트를 가리킬 수 있다. 다음 그림처럼.



Two file handles refer to same file object



사실 위 두개의 그림의 차이가 뭔지 잘 느낌이 안온다. 아마도 같은 파일 오브젝트를 공유하는 경우에 좀 더 자원경쟁에 안정적인 구현이 가능하다는 점이 다를까? 여러개의 다른 파일 오브젝트가 동시 다발적으로 한개의 파일에 접근하는 경우를 예방할 수 있다는 것이 장점일까? 


다음 도표를 보면 다양한 커널 오브젝트들이 나열되어 있다. 보면 생성 함수와 파괴 함수가 나열되어 있다. 시스템은 커널 오브젝트를 가리키는 최후의 핸들이 닫히면 자동으로 메모리에서 커널 오브젝트를 날려 버린다.


내가 내 생각대로 정리하자면, 커널 오브젝트는 보안 속성 (접근 제한)과 상태를 가지고 시스템에 의해 관리를 받는 일종의 구조체이다. 이런 커널 오브젝트가 중요한 이유는 시스템의 모든 중요 업무는 이런 커널 오브젝트를 통해서 이루어지고 있기 때문에다. 왜냐면 시스템이 이런 커널 오브젝트를 체계적으로 관리하여 사용자의 삽질을 막고 동시에 안전성을 높인다. 하지만 나에게 지금 커널 오브젝트가 중요한 이유는 바로 '상태' 때문이다. 커널 오브젝트가 상태를 가지기 때문에 스레드 간의 동기화에 요긴하게 쓰인다. 다름 글에서 어떻게 사용하는지 써보겠다.


Kernel objectCreator functionDestroyer function
Access tokenCreateRestrictedToken,DuplicateTokenDuplicateTokenEx,OpenProcessToken,OpenThreadTokenCloseHandle
Change notificationFindFirstChangeNotificationFindCloseChangeNotification
Communications deviceCreateFileCloseHandle
Console inputCreateFile, with CONIN$CloseHandle
Console screen bufferCreateFile, with CONOUT$CloseHandle
DesktopGetThreadDesktopApplications cannot delete this object.
EventCreateEventCreateEventEx,OpenEventCloseHandle
Event logOpenEventLog,RegisterEventSource,OpenBackupEventLogCloseEventLog
FileCreateFileCloseHandleDeleteFile
File mappingCreateFileMapping,OpenFileMappingCloseHandle
Find fileFindFirstFileFindClose
HeapHeapCreateHeapDestroy
I/O completion portCreateIoCompletionPortCloseHandle
JobCreateJobObjectCloseHandle
MailslotCreateMailslotCloseHandle
Memory resource notificationCreateMemoryResourceNotificationCloseHandle
ModuleLoadLibraryGetModuleHandleFreeLibrary
MutexCreateMutexCreateMutexEx,OpenMutexCloseHandle
PipeCreateNamedPipeCreatePipeCloseHandle,DisconnectNamedPipe
ProcessCreateProcessOpenProcess,GetCurrentProcessCloseHandle,TerminateProcess
SemaphoreCreateSemaphore,CreateSemaphoreEx,OpenSemaphoreCloseHandle
Socketsocketacceptclosesocket
ThreadCreateThread,CreateRemoteThread,GetCurrentThreadCloseHandle,TerminateThread
TimerCreateWaitableTimer,CreateWaitableTimerEx,OpenWaitableTimerCloseHandle
Update resourceBeginUpdateResourceEndUpdateResource
Window stationGetProcessWindowStationApplications cannot delete this object.

 







posted by 대갈장군
2012. 12. 13. 03:44 프로그래밍

WinForms는 작업 (예를 들자면 윈도우 디스플레이, 이벤트 처리 등등)을 하기 위해서 Native Windows System Assembly들을 사용한다. 


Thread Safe 하다는 것은 Multiple Threads 들이 하나의 오브젝트의 상태를 동시에 접근하여 변경하려고 하더라도 그 오브젝트의 '상태 (State)' '일관성있게' (Consistent) 유지한다는 것을 의미한다. 


Windows 폼 컨트롤의 "State" (상태)라고 하는 것은 텍스트 박스 안의 텍스트 같은 것을 말한다. 체크 박스를 예로 들자면 체크 상태를 말하는 것이다.


Multithreaded 환경에서 스레드에 안전하지 않은 (Non-Thread-Safe) 오브젝트를 사용하는 것은 상태의 오염 (State Corruption)을 가져 올 수 있다. 


이런 점을 피해가기 위해서 등장한 것이 바로 "Thread Apartment"라고 불리는 컨셉이다. 이것은 두가지 종류가 있는데 하나는 Single Threaded (STA)이고 다른 하나는 Multithreaded (MTA) 이다. A는 Apartment의 약자이다. .NET의 기본적인 디폴트 값은 MTA이다.


프로세스가 STA (Single Threaded Apartment) 모드로 작동한다고 가정하면 그 코드는 오직 하나의 스레드에서만 돌아간다는 말이다. 이런 STA 모드는 UI Application의 경우 아무런 효용 가지가 없다. 왜냐면 백그라운드 스레드로 작업을 할 수가 없기 때문이다. 


그래서 .NET은 WinForms Application을 Multithreaded로 생성하는 대신 폼의 내부에 속한 각각의 컴포넌트(예를 들자면 텍스트 박스)에 대해 그 컴포넌트가 생성되었던 스레드를 기억해 두었다가 사용자가 생성되었을 당시 사용되었던 스레드가 아닌 다른 스레드를 이용해서 해당 컴포넌트를 호출하거나 사용하려 하면 Exception이 발생하게 된다. 


InvokeRequired() 함수는 기본적으로 컴포넌트가 생성될 당시의 스레드 ID와 현재의 스레드 ID를 비교하는 함수이다.



posted by 대갈장군
2011. 3. 12. 04:33 프로그래밍/Windows API
드디어 실제로 메모리 주소 공간을 들여다 볼 차례...

http://technet.microsoft.com/en-us/sysinternals/dd535533 에서 VMMap 프로그램을 다운 받아서 실행시키면 현재 내 컴퓨터에서 돌아가고 있는 프로세스의 주소 공간을 들여다 볼 수가 있다.


김상형님의 책에 있는 샘플 프로그램을 돌리고 있는 샘플 프로세스의 메모리 주소 공간을 보여준다. 알다시피 프로세스는 총 4GB의 주소 공간을 가지지만 실질적으로 사용자가 사용하는 주소 공간은 0x00010000 부터 0x7FFEFFFF 이다. 대략 2GB 이다. 0x00000000 부터 0x0000FFFF 까지는 준비된 영역으로 접근시 Access Violation이 일어난다. 그리고 2GB 이후의 공간은 운영체제가 필요로 하는 주소 공간이다. 뭔 2GB 씩이나 쓰냐고 하겠지만 2GB도 작아서 3GB로 확장 하게 만들어 놨을 정도다.

아무튼, 위 그림에서 보면 아래쪽에 좌악 주소 공간의 크기 순으로 정렬되어 있는데 타입이 여러가지가 있다. 우선 Free는 할당 되지 않은 공간을 말하고 Private Data의 경우는 시스템의 페이징 파일에 맵핑 되어 있는 것을 말한다. 그리고 Image의 경우는 exe나 DLL 같은 이미지 파일을 맵핑 하고 있는 것을 말하고 Mapped File은 당연히 메모리 맵에 맵핑된 데이터를 말한다. 그리고 Heap도 보이는데 이것도 사실은 Private Data로 분류되고 있다.  

이렇게 펼쳐놓고 보니 메모리 별거 아니다... ㅡ.ㅡ 라고 믿고 싶다. 일단 메모리의 구조에 대한 이해가 끝났다면 다음으로 고고고~
posted by 대갈장군
2011. 3. 12. 04:15 프로그래밍/Windows API
마지막으로 힙과 메모리 맵이 남았다. 힙은 프로세스의 주소 공간 상의 예약 영역으로 가상 메모리 (VirtualAlloc 함수) 방식에 비해서 훠어얼씨인 더 효율적이다. 다만 '예약' 상태가 없기 때문에 편리성은 쬐에끔 떨어진다.

간단하게 결론만 말해서 작은 데이터의 연속적인 생성과 삭제 그리고 접근이 필요하다면 당연히 힙을 이용해야 한다. 만약 거대 메모리를 할당해야 한다면 VirtualAlloc 함수를 이용하여 적극적인 '예약'을 통해 보다 효율적으로 주어진 주소 공간을 사용하면 된다. 그리고 마지막으로 무지막지하게 큰 데이터를 불러와야 하거나 프로세스 간에 서로 메모리를 공유해야 한다면 최선의 방식은 메모리 맵이다.

힙의 할당과 해제는 다음과 같은 명령으로 수행 가능하다.
ptrHeap = HeapAlloc(GetProcessHeap(), 0, sizeof(int));
HeapFree(GetProcessHealp, 0, ptrHeap);

사실 힙은 사용하는 방법은 간단한 편인데 왜 힙이 좋은 녀석인가에 대한 논의가 좀 필요하다. 우선 힙이 어떤 원리를 가지는가 알아보면, 힙은 프로세스 실행시 기본으로 1MB의 영역을 예약하게 된다. 중요한 것은 이 주소 공간은 프로세스가 가지는 다수의 스레드에서 접근 가능하므로 운영체제는 힙에 대해 순차적 접근 및 처리를 기본적으로 하게 된다. 다수의 스레드에 의한 경쟁 상태를 방지 하기 위해서 이다.

고로 바로 이 순차적 처리 부분에서 성능의 저하가 약간 있을 수 있다. 하지만 힙은 여러개 생성할 수 있으며 생성시에 HEAP_NO_SERIALIZE 플래그를 주면 동기화 문제를 고려하지 않고 즉각적인 접근과 변경을 허용하게 된다.

만일 다수의 스레드가 접근해서 읽기와 쓰기를 하는 경우라면 반드시 순차적 접근을 하도록 내버려 두어야 하지만 만약 사용자가 단일 스레드를 위한 하나의 힙을 생성하고 그 스레드만 접근한다는 보장이 된다면 순차적 접근을 무시하도록 하여 보다 빠른 성능을 내게 할 수 있다.

또한 하나의 통일된 힙에 모든 자료와 데이터가 섞이게 되는 경우 자료 A의 오류로 인한 자료 B로의 침범이 일어났을때 에러가 발생하면 운영체제는 자료 B에 의한 에러로 오판 할 수 있다. 

적절한 새로운 힙의 생성은 메모리 공간을 보다 효율적으로 사용하게 해주는 방법이 되기도 한다. 다양한 크기의 자료가 뒤섞여있는 힙에서는 조각화 현상이 나타나게 되므로 실제로 비어있는 여유 공간이 추가적인 데이터를 삽입하기에 충분하더라고 데이터의 배열 문제로 공간이 없다고 생각하게 된다. 이런 문제는 같은 사이즈를 가지는 데이터 별로 힙을 생성해 관리하면 쉽게 해결된다. 

그런 식으로 함으로써 추가적으로 유사한 데이터들이 한 주소 영역에 뭉치는 결과를 가져오며 이것은 시스템 램과 페이징 파일 사이의 스와핑을 줄여준다. 사실 이 스와핑이야 말로 성능 저하의 가장 큰 주범인데 이것을 예방 할 수 있다면 아주 효과적이라고 할 수 있겠다.

그리고 앞서 말했듯이 단일 스레드가 단일 힙을 엑세스 한다는 조건만 만족시켜주면 순차적 접근을 할 필요가 없으므로 동기화 비용을 줄일 수 있다. 

마지막으로 빠른 해제도 가능하다. 해제해야 할 데이터가 한군데 모여 있기 때문에 해제가 매우 빠르다. 마구 마구 섞여 있는 경우에는 해제도 번거롭다.

이런 여러가지 이유 때문에 힙은 아주 유용하게 쓰일 수 있는 놈이다. 

이런 장점을 이용해 클래스의 오브젝트 생성시 힙을 따로 할당하는 방법을 제프리 리처의 WIndows via C/C++ 책에서는 제시하고 있다. 작은 데이터 구조를 여러개 관리하는 프로그램에서는 매우 효과적인 클래스 관리법이 될 수 있을 것 같다.

메모리 맵은 하드디스크 공간에 존재하는 파일을 메모리로 맵핑 시키는 기법이다. 이로써 서로 다른 프로세스가 하나의 맵 파일을 공유할 수 있고 프로세스 간에 자료 교환이 가능하다. 


posted by 대갈장군
prev 1 2 3 4 next