블로그 이미지
대갈장군

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

Notice

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 대갈장군
prev 1 next