MSIL 연산 지시어 중 하나인 modopt에 관한 정보를 구글을 통하여 검색해보다가, 눈에 띄는 자료 하나를 찾게 되어 블로그에 글을 올려봅니다. C++/CLI의 역사적인 배경을 설명하고, C++/CLI와 같이 레거시 언어에서 확장된 형태의 프로그래밍 언어를 바라보는 관점을 소개하고 있어 매우 유익하다고 생각합니다.

 

출처: http://msdn.microsoft.com/ko-kr/magazine/cc163484.aspx (MSDN Magazine)

 

목차

  • 설계 원리
  • Managed Extensions의 쇠퇴
  • 미래에 대한 기대
  • 프로그래밍의 진화
  • 최종 목표
  • 작별 인사

 

이번 달에는 그 동안 즐겨 사용해 온 질문과 대답 형식에서 벗어나 필자가 온라인에서 찾아낸 훌륭한 문서에 대해 소개하려 합니다. 몇 주 전에 필자는 C++/CLI에서 다음과 같이 const 함수를 선언할 수 없는 이유를 묻는 질문을 받았습니다.

 

// reference class
ref class A {
   void f() const; // NOT!
};

 

대답은 간단합니다. 규칙이기 때문에 어쩔 수 없다는 것입니다. CLI(Common Language Infrastructure)는 const가 무슨 의미인지도 모르는 Visual Basic®, Java, 심지어는 COBOL과 같은 다양한 언어를 지원하기 위해 설계되었습니다. 즉 CLI는 const 멤버 함수가 무엇인지 모르므로, CLI에서 이 함수를 사용할 수 없습니다.


대답을 보내고 나서 문득 기억 한구석에 있던 const와 관련하여 언어에 관계없이 컴파일러 힌트로 무언가 할 수 있었다는 내용이 떠올랐습니다. 곧바로 예전 칼럼을 찾아보니 2004년 7월에 const에 대한 질문에 대답한 적이 있었습니다. 사실 C++/CLI에서 const 멤버 함수는 선언할 수 없지만 const 데이터 멤버와 매개 변수는 가능합니다. 그림 1은 const 정적 데이터 멤버를 가진 참조 클래스를 사용하는 작은 프로그램의 예입니다. 이 프로그램을 컴파일한 다음 ILDASM을 사용하여 디스어셈블하면 다음과 같은 코드를 볼 수 있습니다.

 

field public static int32 
    modopt([mscorlib]System.Runtime.CompilerServices.IsConst)
        g_private = int32(0x00000001)

 

////////////////////////////////////////////////////////////////
// To compile type:
//    cl /clr const.cpp
//
#include 

ref class A {
   int m_val;
   // const data member allowed, will generate modopt
   static const int g_private = 1;
public:
   // public const member could be modified by Visual Basic or other
   // programs that don't know const -- so use literal instead.
   literal int g_public = 1;
   A(int i) { m_val = i; }
   void print();  // const;             // NO!--const fn not allowed
};

void A::print()
{
   printf("Val is %d\n",m_val);
}

int main()
{
   A a(17);
   a.print();
}

그림 1: const.cpp

 

MSIL 선언자인 Modopt는 선택적인 한정자이므로 CLI 사용자는 이 내용을 이해할 수 있다면 좋고, 그렇지 않다면 그냥 무시해 버리면 됩니다. 그러나 modreq 함수는 필수 한정자이므로 반드시 이해해야 합니다.

 

modreq의 예로는 Volatile이 있는데, volatile 참조는 운영 체제나 하드웨어, 심지어는 다른 스레드에 의해서도 언제든 변경될 수 있으므로 CLI 사용자가 Volatile을 사용하려면 Volatile이 무엇인지 알아야 합니다. 하지만 const는 선택적인 한정자입니다. 그러나 Managed Extensions는 C++ const 개체를 CLI 리터럴로 변환하는 반면, C++/CLI는 이런 작업을 하지 않기 때문에 주의해야 합니다. 공용 const 데이터 멤버를 선언한 경우 Visual Basic과 같은 언어로 작성된 클라이언트가 그 값을 바꿀 수도 있습니다. CLI 클라이언트에서 값을 바꿀 수 없도록 하려면 그림 1과 같이 개체 리터럴을 선언해야 합니다. 그렇다면 멤버 함수는 어떨까요? const 멤버 함수는 왜 지원되지 않는 것일까요?

 

설계 원리


필자의 대답에 대해 연구하던 중 Microsoft의 Herb Sutter가 작성한 "A Design Rationale for C++/CLI"라는 훌륭한 기사를 찾아냈습니다. Herb는 C++/CLI의 설계자 중 한 사람입니다. 이 문서(앞으로는 '설계 원리'로 부름)는 Herb의 블로그에서 찾을 수 있습니다. URL이 너무 길고 복잡하므로 검색 엔진에서 "C++/CLI Rationale"을 찾아보기 바랍니다. 제목이 말해 주듯이 이 문서는 C++/CLI에 대한 대부분의 의문에 대해 설명해 주고 있습니다. 이 문서는 “왜 C++를 확장했는가”라는 질문에서 const 함수에 대한 필자의 궁금증에 이르기까지 모든 것을 답해 줍니다. C++/CLI의 설계 배경과 근본적인 개념을 알고 싶다면 이 문서를 반드시 읽어볼 것을 권합니다. 여기서는 모두 소개할 수 없으므로 중요한 부분만 요약해서 소개하겠습니다.


일단 가장 중요한 질문, 왜 C++를 확장했는가에 대해 알아보겠습니다. 간단하고 분명한 대답은 “C++를 CLI의 중요한 언어로 사용하도록 하기 위해서”입니다. Microsoft® .NET Framework는 Windows® 개발의 미래이며 COBOL 조차도 CLI를 지원합니다. 즉, C++/CLI는 C++의 지속적인 성공을 위해 설계된 것입니다.


하지만 왜 굳이 C++를 사용할까요? ^ 또는 %와 같은 새로운 개념도 있고 ref, value, property와 같은 새 키워드도 있는 다른 언어를 확장하면 어떨까요? 사실 다른 방법은 없었습니다. '설계 원리'에서는 Bjarne Stroustrup의 말을 인용해 이러한 질문에 대답하고 있습니다. "우리가 필요한 거의 모든 개념은 기본 속에서 찾을 수 있습니다. 라이브러리를 사용하는 방식이 정말로 실현 불가능할 경우에만 언어 확장 방식을 사용해야 합니다."


평범한 C++ 코드의 경우 CLI를 대상으로 하는 것은 다른 프로세서를 위한 컴파일러를 만드는 것과 같습니다. 여기까지는 문제가 없으나 CLI는 특별한 코드 생성이 필요한 새로운 개념을 도입했으며 이는 C++에서 간단히 표현할 수 있는 것이 아닙니다. 예를 들어 속성에는 특별한 메타데이터가 필요합니다. 라이브러리나 템플릿 없이 속성을 구현할 수는 없습니다. '설계 원리'에서는 속성을 비롯하여 다른 CLI 기능을 대체할 수 있는 몇 가지 구문을 소개하고 있으며, 이러한 구문이 결국 왜 사용될 수 없었는지에 대해서도 설명하고 있습니다.

 

Managed Extensions의 쇠퇴


'설계 원리'에서는 또한 Microsoft에서 Managed Extensions를 더 이상 사용하지 않기로 결정한 이유에 대해서도 설명하고 있습니다. Managed Extensions에서 관리된 개체와 네이티브 개체 모두에 *를 사용한 것은 C++와 CLI를 통합하기 위한 영리하고 용감한 시도였지만, 참조와 네이티브 개체의 중요한 차이점이 분명치 않았습니다. 이러한 두 종류의 포인터는 같아 보이지만 동작은 다릅니다. 예를 들어 소멸 구문, 복사 생성자, 생성자/소멸자 내에서의 가상 호출 등이 모두 포인터가 가리키는 개체가 무엇인지에 따라 다르게 동작하며 이는 끔찍한 일입니다. '설계 원리'에서는 "불필요한 차이점을 감추는 것도 중요하지만 필요한 차이점은 반드시 공개해야 한다.”고 설명하면서 다시 Bjarne의 말을 인용하고 있습니다. "중요한 작업은 반드시 눈에 띄어야 합니다." 네이티브 클래스와 관리되는 클래스는 근본적으로 다른 것이므로 무시하기 보다는 그 차이점을 부각시키는 것이 나은 방법일 것입니다. 이를 위해 여러 가지 메커니즘이 고안되고 버려졌으며, 결국 C++/CLI 팀은 gcnew, ^(핸들) 및 %(참조 추적)와 같은 개념을 도입했습니다. 이렇게 관리되는 클래스와 네이티브 클래스를 분리하면 뜻밖의 효과도 누릴 수 있습니다. 예를 들어 gcnew 연산자를 사용하여 관리되는 개체를 할당하면 언젠가 관리되는 힙에서 네이티브 클래스가 할당되고, 그 반대 방향의 작업도 가능할 수 있다는 가능성을 열어두게 됩니다.


Managed Extensions는 왜 __gc나 __value와 같이 보기 흉한 밑줄 키워드를 사용하는지 궁금했던 적이 있습니까? 이는 Managed Extensions가 C++ 표준을 엄격하게 따르기 때문입니다. C++ 표준에서는 새 키워드가 필요하다면 이름 앞에 밑줄 두 개를 붙이라고 지정하고 있습니다. 꼭 그래야 할까요? Microsoft에서 __gc라는 키워드를 도입했을 때, __에 대해 프로그래머들은 강력한 항의를 보냈습니다. 뭉치면 못할 것이 없습니다. 밑줄 문자 밖에는 잃을 것도 없습니다. 밑줄을 사용하면 코드를 읽기 어려울뿐더러 마치 어셈블리 프로그램과 같이 난해하게 보이기도 합니다. 결국 C++/CLI에는 밑줄이 없는 ref 및 value가 추가되었습니다. C++에 새 키워드를 추가한 셈이지만 표준을 따르지 않아도 문제될 것은 없습니다. Bjarne은 이렇게 말합니다. "제 경험에 따르면 사람들은 자신이 고안한 매우 복잡하고 어려운 개념을 사용하기 위해 새 키워드를 만드는 것을 지나치게 좋아합니다. 그 결과, 점차 새로운 키워드들은 우리가 흔히 사용하는 용어와는 전혀 다르게 변해가고 있습니다." 맞는 말입니다. 필자는 프로그래밍에 대한 Bjarne의 심리학적 설명을 매우 좋아합니다. C++/CLI에서 밑줄이 사라진 이유는 이것입니다. 예약된 키워드가 아닌 전후 관계에 따라 사용되는 키워드를 만듦으로써 이미 이 키워드를 변수나 함수 이름으로 사용하는 프로그램과의 충돌 문제도 방지할 수 있습니다.

 

궁금증이 많은 사람이라면 밑줄을 없애는 과정에서 gc가 어떻게 ref로 바뀌었는지 궁금할 수도 있습니다. '설계 원리'에서는 관리되는 클래스가 관리되는(가비지가 수집되는) 힙에 있다는 것이 중요한 것이 아니라 그 참조 구문이 중요하다고 합니다. 핸들(^)이 포인터가 아닌 참조처럼 사용되는 것도 같은 원리입니다. 이와 같이 '설계 원리'를 읽어보면 모든 것이 분명해집니다.


분명한 차이점을 드러내는 것이 중요하다면 그렇지 않은 것을 감추는 것도 중요합니다. 예를 들어 모든 연산자 오버로딩은 개체가 네이티브인지, ref인지, 또는 value인지에 관계없이 C++ 프로그래머가 예상하는 그대로 작동합니다. C++/CLI 문서에서 차이에 대해 설명한 또 하나의 예로 다음 코드를 보겠습니다.

 

// ref class R as local variable
void f()
{ 
   R r; // ref class on stack???
   r.DoSomething();
   // ...
}

여기서 r은 스택 개체처럼 보이지만 누구라도 관리되는 클래스는 스택에 물리적으로 넣을 수 없다는 것을 알 것입니다. 관리되는 클래스는 관리되는 힙에 할당해야 합니다. 그렇다면 무슨 일이 벌어질까요? 컴파일러는 이 코드를 다음과 같은 의미로 해석하게 됩니다.

// how the compiler sees it.
void f()
{ 
   R^ r = gcnew R; // allocate on gc heap
   try
   {
      r->DoSomething();
      ...
   }
   finally
   {
      delete r;
   }
}

 

결과적으로 개체는 실제 스택에 할당되지 않습니다. 여기서 중요한 것은 로컬 변수 구문이 모든 C++ 프로그래머가 예상하는 방식으로 작동한다는 점입니다. 특히 f를 벗어나기 전에 r의 소멸자가 호출되는 것은 C++/CLI에서 결정적 소멸을 복원한 이유와 같은 이유로 설명할 수 있습니다. 소멸을 위해 모든 C++ 프로그래머가 알고 사용하는 것과 동일한 구문을 따르고 있다는 것입니다. 비결정적 소멸은 Managed Extensions의 기능 중 최악의 것으로, C++/CLI는 이 문제를 해결하기 위해 소멸자를 Dispose에 매핑하고 종료자를 위한 특별한 구문인 !를 도입했습니다. 참조 개체가 사용하는 메모리는 가비지 수집기가 활동하기까지는 그대로 남아있게 되지만 그리 큰 문제는 아닙니다. C++ 프로그래머에게는 필요할 때 소멸자가 실행되는 것이 중요하지, 개체가 파괴될 때 메모리를 회수하는 것이 중요하지는 않습니다. C++ 프로그래머는 파일 핸들, 데이터베이스 잠금과 같이 메모리 리소스가 아닌 개체를 초기화/해제하는 데 생성/소멸 패턴을 자주 사용합니다. C++/CLI를 사용하면 참조와 네이티브 클래스에 대해 여러분이 기대하는 방식 그대로 친숙한 생성/소멸 패턴을 사용할 수 있습니다. 결국 이와 같은 이유로 인해 Microsoft는 Managed Extensions의 문제를 인식하고 이를 수정하는 작업을 포기하게 된 것입니다.


미래에 대한 기대


'설계 원리'의 흥미로운 단원 중에 "Future Unifications(미래의 통합)"이라는 부분이 있습니다. 이 단원에서는 C++/CLI가 미래에 어떤 방향으로 발전할지에 대해 약간의 힌트를 주고 있는데, 예를 들어 지금은 관리되는 클래스에서 네이티브 클래스를 파생할 수 없고 그 반대로도 마찬가지지만 “기본” 클래스를 데이터 멤버로 추가하고 컨테이너 인스턴스를 호출하는 것만 담당하는 통과 래퍼를 만들면 같은 결과를 만드는 해결책을 구현할 수 있습니다. 아주 좋은 방법으로 보이며 컴파일러가 이러한 작업을 지원하지 못할 이유가 없어 보입니다. 컴파일러는 모든 CLI 부분을 포함하는 하나의 CLI 개체와 모든 C++ 부분을 포함하는 하나의 C++ 개체로 구성된 혼합된 개체를 만들어 사용할 수 있을 것입니다.


이에 대한 흥미로운 일화도 소개되어 있습니다. '설계 원리'의 저자가 이러한 혼합 클래스에 대한 아이디어를 Bjarne Stroustrup에게 처음 보여주었을 때, Bjarne은 서재로 가서 책을 펴 보여주면서 “C++에서 개체를 단일 메모리 블록에 연속해 저장할 필요는 없다.”고 말했다고 합니다. 이때까지 누구도 비연속적인 개체의 이점을 알지 못했으며 .NET과 CLI가 개발될 것이라고 예상하지도 못했습니다. 비연속이라는 문을 열어야 한다는 Bjarne의 고집이 결국 혼합된 개체를 가능하게 만든 것입니다. C++/CLI의 나중 버전에서 이러한 기능을 보게 되도 놀라지 마십시오. 여기서 주는 교훈은 여러분이 새 언어나 복잡한 프로그램을 설계하여 계속 사용될 것으로 기대한다면 단지 약간의 편의를 위해 절대 불필요한 가정을 해서는 안 된다는 것입니다.


이 밖에도 '설계 원리'에서는 재미있는 몇 가지 사실도 소개하고 있습니다. Microsoft가 C++/CLI의 원래 내부 이름으로 사용한 것은 MC^2였다고 합니다. M은 “관리되는”의 의미, C는 C++를 의미하며 ^2는 앨버트 아인슈타인의 공식에서 차용한 것입니다. 하지만 결국 이름이 너무 장난스럽다는 이유로 채택되지 못했습니다. 필자도 동의하는 부분입니다. MC^2라는 이름을 그대로 사용했다면 아마도 받아들이기 힘들었을 것입니다. 이름을 C++/CLI로 결정한 데는 Bjarne이 C++의 이름에 대해 말한 것과 같은 이유가 있었습니다. Bjarne은 "이름을 C++라고 정한 것은 짧고, 쉽게 해석이 가능하며 ‘부수적인 C’와 같은 형식이 아니었기 때문입니다”라고 말했습니다. C++/CLI 역시 C++를 먼저 넣고, "부수적인 C++"라는 표현을 피한 것뿐입니다.


'설계 원리'에서는 속성, gcnew, 제네릭 및 const 등의 다른 C++/CLI 확장 기능에 대한 설명과 더불어 마무리에는 유용한 질문과 대답 단원을 소개하고 있습니다. 자세한 내용은 '설계 원리'를 다운로드하여 읽어보십시오. Const에 대해 다시 얘기해 보겠습니다. C++/CLI는 왜 const 데이터는 허용하면서 const 함수는 허용하지 않는 것일까요? 간단히 얘기하면 CLI는 modopt/modreq를 함수에 직접 넣는 것을 허용하지 않기 때문입니다. 이 정보를 메타데이터에 인코딩하는 방법이 있기는 하지만, 아직 테스트되지 않았습니다. 그리고 '설계 원리'에서는 언젠가 이러한 기능이 추가될 것이라고 조심스럽게 시사하고 있습니다.


프로그래밍의 진화


C++/CLI는 C++를 CLI의 중요한 언어로 만들었으며 '설계 원리'를 읽어보면 이러한 결정에 대해 작은 의심도 없었음을 알 수 있습니다. C++는 아직도 시스템 프로그래밍 분야에서 최고의 언어이며, 이는 다른 어떤 언어보다도 CLI에 직접적으로 액세스할 수 있고 아직까지도 널리 사용되는 예전 Win32® API를 호출해야 할 경우 C로도 확장될 수 있기 때문입니다.


C++/CLI가 얼마나 중요하며 무엇을 나타내는지 이해하기 위해서는 현재 우리가 프로그래밍의 진화 단계에 어느 지점에 있는지 생각해 보아야 합니다. 간단하게 이러한 내용에 대해 살펴보겠습니다. 예전에는 프로그래머가 토글 스위치를 사용하여 프로그램을 작성했습니다. 여기서 발전한 것이 종이 테이프였고, 각 컴퓨터가 고유한 “기계” 언어를 가지는 단계로 발전했습니다. 컴퓨터가 진화함에 따라 프로그래머도 이러한 새 기계를 대상으로 하는 프로그램을 만들어야 했고, FORTRAN, BASIC, C와 같은 고급 언어가 개발되었으며 고급 언어를 각 컴퓨터에 대한 명령으로 변환해 주는 “컴파일러”가 사용되기 시작했습니다. 그림 2에서 이러한 과정을 보여 줍니다. 이제 프로그램을 만들어 컴파일하면 다른 컴퓨터에서 쓸 수 있게 된 것입니다. C가 시스템 프로그래밍을 위한 최적의 언어로 선택된 이유는 “고급 언어 중 가장 저급 언어”였기 때문입니다. 이는 사람이 사용할 수 있는 언어 중 가장 기계와 친숙한 언어라는 의미입니다. 근래에 사용되는 대부분의 운영 체제는 C로 작성되었으며 성능이 중요하고 하드웨어 조작이 필요한 일부분만 어셈블리 언어로 코딩되어 있습니다.


오랜 세월 후에 C를 개선하여 개체 지향 방식 등 많은 새로운 기능을 도입한 C++가 등장했습니다. Bjarne은 이렇게 말했습니다. "C++는 저자나 그의 친구들이 어셈블러, C 또는 다양한 근래의 고급 언어를 사용해 프로그래밍할 필요가 없도록 하기 위해 설계되었습니다. C++의 주 목적은 모든 프로그래머가 좋은 프로그램을 편하고 즐겁게 작성할 수 있도록 하는 것입니다." C++는 훌륭한 언어이지만, 고급 언어는 서로 호환되지 않는 경우가 많습니다. C++에서 코드를 작성하면 이를 BASIC과 같은 다른 언어에서 사용할 수 없으며, 가능하다 해도 상당한 어려움이 따릅니다. 각 언어는 각자 고유한 영역 내에서 처리되기 때문입니다. 이러한 특징은 독립 실행형 응용 프로그램에는 문제가 없지만 응용 프로그램이 보다 복잡해지고 환경이 분산되면서 공유 코드에 대한 필요성이 대두되기 시작했습니다. 결국 프로그래머는 완벽히 캡슐화되어 재사용이 가능한 구성 요소를 원하게 된 것입니다. 이러한 구성 요소를 사용하면 각 조각을 만들어 결합하는 것으로 응용 프로그램을 만들 수 있을 것이며 모든 조각이 같은 언어로 작성될 필요도 없을 것입니다.


이러한 다양한 구성 요소를 통합하려는 시도는 몇 년에 걸쳐 진행되었습니다. 처음에는 C 런타임이나 printf와 같은 라이브러리를 사용한 언어가 등장했고, Windows가 사용되면서 메모리 절약을 위한 DLL의 지연 로딩(DLL에 Dynamic이란 이름이 사용된 이유임)이 제공되었습니다. 또한 Visual Basic이나 COBOL과 같은 언어에서 가져오기 구문을 사용하여 컴파일러가 C와 연계된 호출을 DLL에 보내는 방식으로 DLL 사용이 가능해짐으로써 언어의 상호 운용성이 증대되는 효과가 있었습니다. 하지만 응용 프로그램과 DLL의 연결은 너무 강했으며 부서지기도 쉬웠습니다. 각 응용 프로그램은 DLL의 각 항목에 대한 이름과 서명을 알아야 했고 DLL에서 응용 프로그램으로의 호출은 함수 포인터를 콜백으로 전달해야 했으므로 상당히 귀찮은 일이었습니다. 때문에 프로그래머들이 VBX를 개발했고, 이 것이 OCX를 거쳐 COM으로 발전했습니다. 다행히도 COM은 언어 중립적인 것으로, COM에는 “형식 라이브러리”가 있어 연결 시 함수 이름을 알 필요가 없고 런타임에 문의하는 방식이 가능했습니다. COM은 상당히 유용하지만 프로그래밍이 매우 어렵다는 단점이 있습니다. 아이러니하게도 C++에 기반을 두고 있으면서도 C++에서 프로그래밍하기가 가장 어렵습니다. COM에는 다른 문제도 있습니다. 너무 저급 언어이며 보안이나 메모리 관리에 대한 기능이 빈약하다는 것입니다.


최종 목표


현재는 2007년이며 우리에게는 .NET Framework와 그 표준 부속물인 CLI가 있습니다. CLI는 프로그래밍 언어와 컴퓨터 사이에 새로운 추상 계층을 삽입하여 재사용성 문제를 완전히 다른 방법으로 해결하였습니다. 이제 시스템 명령을 생성하는 대신 컴파일러는 MSIL 코드를 생성하며 CLI 가상 시스템/JIT(just-in-time) 컴파일러가 이를 즉시 기계어 코드로 컴파일합니다. 가상 시스템(Virtual Machine, VES 또는 Virtual Execution System)은 컴퓨터를 위한 추상 계층을 제공합니다. 가상 시스템은 새로운 개념이 아니라 사실 오랫동안 사용되어 왔습니다. Pascal이나 ZIL(필자가 한때 몸담았던 Infocom이란 회사에서 게임 개발을 위해 내부적으로 사용하던 언어, Zork Implementation Language의 약자임)과 같은 언어는 고급 언어를 P-코드(또는 Z-코드)로 컴파일하여 가상 시스템에서 해석하는 방식을 이미 사용하고 있었습니다. 하지만 CLI는 모든 언어가 사용할 수 있는 공용 가상 시스템(CLI에서 C가 의미하는 부분)을 제공한다는 점이 다릅니다. CLI는 클래스, 속성, 상속성, 리플렉션과 같은 기본적인 개념을 지원합니다. VES/VM는 메모리 관리와 보안 기능도 제공하므로 프로그래머는 악의적인 바이러스에 문을 열어 주는 버퍼 오버런이나 기타 버그를 걱정하지 않아도 됩니다. .NET Framework와 CLI가 널리 사용되면서 Visual Basic, Fortran, COBOL, C#과 같은 고급 언어는 점차 그 경계가 사라지는 분위기이며, 클래스, 상속성, 멤버 함수 등의 CLI 기반 개념을 지원해야 하기 때문에 C++ 역시 그 영향을 받고 있습니다. 각 언어는 여전히 독자적인 영역을 가지고 있으므로 프로그래머가 자신의 도구를 .NET Framework을 사용하도록 바꿀 필요는 없으며 몇 가지 새로운 개념만 배우면 됩니다.


이제 프로그래머는 원하는 언어를 선택하여 클래스를 작성할 수 있으며 언어에 관계없이 다른 프로그래머가 작성한 클래스를 사용할 수도 있습니다. 어떤 언어를 사용해도 모든 구성 요소를 프로그래밍할 수 있고, 약간의 프로그래밍 노력만 거치면 모든 구성 요소가 매끄럽게 서로 연결됩니다. 또한 CLI의 보안, 가비지 수집 등 인프라 기능(CLI에서 I가 의미하는 부분)의 혜택을 받을 수 있습니다. 이후에 Microsoft에서 새로운 CLI 기능을 추가하면 모든 언어가 그 혜택을 받을 수 있을 것입니다. Windows Vista™와 .NET Framework 3.0(10,000개의 새 클래스와 더불어 Windows Presentation Foundation, Windows Communication Foundation, Windows Workflow Foundation 및 Windows CardSpace™와 같은 새로운 기술이 연계되어 있음)이 개발되면서 Windows는 CLI 클래스로서 다시 만들어지고 있습니다. 이제는 재사용 가능하고, 언어 중립적이며 상호 운용이 가능한 구성 요소라는 목표에 도달한 것으로 보입니다. 이는 매우 큰 패러다임의 변화로, 전진적인 진화를 거쳐 놀라운 수준에 이른 것입니다. 이제는 프로그래밍이 아주 쉬워지고 있습니다.


이러한 모든 사항을 곰곰이 생각했을 때 C++가 새로운 세계에 적응하는 것은 당연한 일로 생각됩니다. C++/CLI이 등장한 이유가 바로 이것입니다. C++/CLI가 없다면 C++는 Windows 프로그램에 사용할 수 없는 유일한 근대 프로그래밍 언어로 남았을 것입니다. 또는 점차 그 수명을 다하여 역사의 변두리로 밀려났을 수도 있습니다. 하지만 C++/CLI가 있기에 그런 일은 일어나지 않습니다. C++를 좋아하는 필자와 같은 프로그래머는 걱정 없이 계속 사용해도 됩니다. 그림 2는 프로그래밍 언어의 패러다임 변화와 그 속에서 C++가 차지하는 위치, 그리고 "/CLI"가 없다면 어떨지를 간략하게 보여 줍니다.

프로그래밍의 진화 과정

그림 2: 프로그래밍의 진화 과정 (더 크게 보려면 이미지를 클릭하십시오.)

 

작별 인사


이제 작별 인사를 하려고 합니다. 그 동안 필자가 기고해온 C++ At Work 칼럼이 이번 호를 마지막으로 여러분 곁을 떠납니다. 많은 칼럼이 중단되는 와중에서도 그 동안 꽤나 오래 이 칼럼을 기고해 온 것을 자랑스럽게 생각합니다. 칼럼이 중단되는 것은 Microsoft나 MSDN®Magazine에서 C++의 위상이 약해진 것 때문은 아니며, 14년간에 걸친 164개 칼럼(이번이 165개 째)을 기고하면서 필자가 많이 지쳤기 때문입니다. 재미있는 일화를 하나 소개하면, 편집장인 Josh Trupin은 필자를 "MSDN Magazine의 Cal Ripken"이라고 부르기도 했습니다. 필자는 야구 팬은 아니지만(축구를 더 좋아합니다) Cal Ripken이 누군지는 압니다. 그래서 Cal이 몇 경기나 출장했는지 웹에서 검색해 보았더니 2,632 경기였습니다. 그리고 그의 이력에서 "많은 팬들은 Cal이 충분히 휴식을 취했다면 더 나은 플레이어가 되었을 것이다."라고 말한 내용도 보았습니다. 필자에게도 해당되는 말이라 생각합니다. 하지만 이것이 필자가 다시는 MSDN Magazine에 기고하지 않을 것임을 의미하지는 않습니다.


칼럼을 마치면서 그 동안 필자의 많은 오류와 실수를 지적해주고 격려를 통해 힘을 준 많은 독자들에게 감사의 말을 전하고 싶습니다. 어떤 독자는 필자가 칼럼을 게시하기 전에 솔루션을 테스트하는 수고를 해주기도 했습니다. 또한 그 동안 함께 일해온 MSDN Magazine의 많은 동료들에게도 감사의 말을 전합니다. 과거에서 현재로 순서 없이 나열해보면 Steve, Josh, Joanne, Eric, Etya, Terry, Laura, Joan(두 명 모두), Nancy, Val, 그리고 Milo가 떠오릅니다. 빠진 사람이 있는지 모르겠네요. 이들은 그 동안 필자를 여러모로 많이 지원해 주었으며 다양한 분야와 내용의 집필이 가능하도록 필자에게 자유로운 공간을 허락해 주었습니다. 그 중에서도 특히 Gretchen Bilson에게 감사하고 싶습니다. 몇 년 전 이미 MSDN Magazine을 떠난 그녀는 MSDN Magazine이 Microsoft Systems Journal이던 1993년에 필자를 고용한 고마운 분입니다.


여기까지입니다. 하지만 영영 떠나는 것은 아닙니다. 아놀드 슈왈츠제네거가 말했듯이 저는 돌아올 것입니다. 즐거운 프로그래밍을 계속하시기 바랍니다.

 

Gak 자료: 다양한 질문

 

딱딱한 내용은 여기서 끝내고, 그 동안 필자가 받았던 특별하고 재미있는 몇 가지 질문에 대해 소개하면서 칼럼을 마치려 합니다. 필자는 이러한 자료를 Gak라는 폴더에 수집해 두었으며 범주별로 분리해 두었습니다. 예를 들어 언어에 대한 질문은 따로 모아 두었습니다.

 

C++를 어디서 다운로드할 수 있나요?

 

많은 사람들이 C++를 높게 평가합니다. 이유가 무엇일까요? Visual Basic이나 Delphi에서는 할 수 없지만 C++에서 할 수 있는 작업은 어떤 것이 있을까요? 이에 대해서는 필자가 많은 경험을 가지고 있습니다. Visual C++®를 사용하더라도 필요한 모든 것은 항상 찾을 수 있었습니다. 중견 프로그래머들이 이 언어를 사용하는 이유가 바로 이것입니다.

 

“중견 프로그래머”에 필자는 포함되지 않았으면 좋겠네요. 다음 질문은 많은 사람이 어려워하는 포인터에 대한 것입니다.

아직도 포인터(var, object, functional)를 잘 이해하지 못했습니다. 설명을 해 주시거나 공부할 자료를 보내 주실 수 있나요?

 

C++를 처음 배우고 있으며 포인터가 무엇이고 어떻게 사용되는지 궁금합니다. 많은 책을 보았지만 너무 복잡하게 설명되어 있습니다. 포인터란 특정한 메모리 위치를 가리키는 것으로 알고 있는데 이런 개념이 왜 필요한지 모르겠습니다. 아마도 컴퓨터가 각 위치를 자동으로 추적하려는 것일까요? 답변 부탁드립니다.

 

필자는 이 질문을 동료 필자인 Matt Pietrek과 John Robbins에게 보내어 조언을 구했습니다. Matt는 이렇게 말하더군요. "그 사람의 생각이 맞다. 포인터는 오래되어 불필요한 개념이며 이제는 배울 필요가 없다." John은 한술 더 뜨더군요. "포인터는 개 이름인줄 알았다. 뭔가 다른 의미가 있나?"

 

다음은 컴파일러에 대한 질문입니다.

 

Paul씨, 컴파일하는 동안 링크 MFC 라이브러리는 어떻게 컴파일합니까? 때로는 DLL을 컴파일하기도 합니다. 지금까지 이 소스는 컴파일하지 않았는데요. 컴파일하는 방법이나 소스를 좀 보내 주십시오. 감사합니다.

 

더 자주 받는 질문은 Windows에 대한 것입니다.

 

키보드/마우스 감지 프로그램을 만들고 있는데 문제가 있습니다. 예를 들어 “시간을 세는” 프로그램이 선택된 상태에서 작동하고 있을 때 선택이 해지된 다음에도 계속 작동하도록 하려면 어떻게 해야 할까요? 여러 가지 방법을 시도해 봤지만 실패해서 이렇게 문의 메일을 보냅니다.

 

중요한 것은, 질문에서 말하는 키보드/마우스 감지 프로그램이 무엇을 말하는 것인지 모르겠다는 점입니다. 필자는 이런 프로그램을 만들어 본 적이 없습니다. 때문에 질문의 의도도 잘 모르겠네요. 아쉬운 일입니다.
다음은 연산에 관련된 질문입니다.

 

문자를 인코딩했다가 나중에 디코딩하는 작업을 수행하는 프로그램을 만들고 있습니다. 그런데 디코딩 부분에서 % 연산을 유지해야 하기 때문에 어려운 점이 있습니다. 나누는 숫자와 나머지는 알고 있는데 이것으로는 애초에 나눈 숫자가 무엇인지 알 수 없습니다. 예를 들어 연산의 결과로 나머지가 22이고 이 연산은 (숫자)%63이었다면 이 숫자는 무엇일까요? 도움을 부탁 드립니다.

 

솔직히 어려운 점이 있다고 말한 이유를 이해할 수 없습니다. n%a=b라는 연산에서 a와 b는 아는데 n을 모르겠다는 의미 아닌가요? 수학 시간에 열심히 듣지 않았던 분인가 봅니다. 다음과 같이 가끔은 “제 숙제를 대신 해주실래요?”라는 질문도 종종 받곤 합니다.

 

프로그래밍에 대해 잘 모릅니다. 20개의 숫자를 입력하고 이를 내림차순으로 정렬해야 하는데 어떻게 해야 할까요?

 

다음 두 프로그램을 만들어야 하는데 분석을 좀 부탁드립니다. 내일 정오까지 완료해야 하는데 도와주세요!

  • 사용자의 최초 잔액과 예금 인출액을 입력 받아 사용자가 -1을 입력하거나 잔액이 0이하가 될 때까지 반복하는 프로그램을 작성하시오. 사용자의 최초 잔액, 최후 잔액, 인출 횟수와 평균 금액을 출력하시오.
  • 임금 지불 내역 프로그램을 작성하고 직원의 격주 순수 임금을 출력하시오. 직원의 근무 시간과 시간당 임금을 입력 받고 초과 근무에 대해서는 가중치를 적용해야 합니다. 공제액은 총 급여의 18%로 책정하여 계산하십시오. 사용자의 급여와 연봉을 출력하십시오.

 

세 번째 것도 있는데 생략하겠습니다. 때로는 아주 긴 소스 코드가 포함된 질문도 받곤 하는데, 읽지도 않고 지워버립니다. 또는 필자의 칼럼과 전혀 관계없는 Visual Basic, Microsoft Access™, Excel® 프로그래밍 등에 대해서도 질문을 받는 경우가 있으며 “컴퓨터가 망가졌는데 어떻게 해야 할까요?” "시작 단추를 안 누르고 컴퓨터를 끄는 방법은 없나요?"와 같은 매우 사려 깊은 질문도 간혹 있습니다. 제발 이런 질문은 Raymond Chen의 블로그에 올려 주시기 바립니다. 다음과 같이 특정한 범주로 분류하기 힘든 질문도 있습니다.

 

Paul씨, 일반적인 개발자가 하루에 작성하는 Visual C++ 코드의 평균 줄 수에 대한 통계 자료가 있나요? 도움을 바랍니다.

 

존경하는 DiLascia씨, 어떻게 하면 당신처럼 될 수 있을까요?

 

안녕하세요. 그 동안 MSDN을 많이 구매했고 거기서 당신 이름을 보았습니다. "Microsoft에 취직하려면 어떻게 해야 할까요?" 저는 Visual C++ 프로그래밍을 좋아하며 기술과 가치, 미래를 위해 Microsoft에서 일하고 싶습니다. 저는 MFC로 멀티미디어 프로그램을 만들 수 있으며 학교 성적도 좋은 편입니다. 그러나 영어 실력은 그저 그렇습니다. 프로그래밍이 너무 재미있어서 5년간 여자 친구도 사귀지 않았습니다. 엉뚱한 질문이라 생각하실지 모르지만 답장을 주셨으면 좋겠습니다. Microsoft에서 일하려면 어떻게 해야 하는지 알려 주십시오. 감사합니다.

 

영어 실력은 그저 그렇지만 무엇보다도 이 청년의 열정적인 자세에 박수를 보내고 싶습니다. 필자는 “죄송합니다. 모르겠네요.”라고 보낼지라도 모든 질문에 대답하려 노력합니다. 실제로 필자가 답변을 보내면 놀라는 독자들이 많았습니다.

 

Paul씨, 당신 같은 유명한 저자가 모르는 사람을 지속적으로 도와주리라고는 생각지 못했습니다. 당신은 지식이 뛰어난 전문가일 뿐만 아니라 아주 좋은 사람이군요. 감사합니다.

 

저 역시 감사합니다. 마지막으로 재미있는 질문 하나를 소개합니다.

 

Windows 프로그래밍에서 심령의 창 (Psychic Window)이란 무엇인지요? 철자를 정확히 모르겠네요. 친구가 물어본 것이라서요. 부탁 드립니다.

 

이 질문에 대한 대답은 찾을 수 없었습니다. 심령의 창을 찾으신다면 초능력자에게 물어보시는 것이 나을 것 같네요. 누구 아시는 분이 있으면 필자에게 텔레파시를 보내 주십시오.

 

저자 소개


Paul DiLascia는 프리랜서 소프트웨어 컨설턴트이자 저명한 웹/UI 디자이너입니다. 저서로는 Windows++: Writing Reusable Windows Code in C++(Addison-Wesley, 1992)가 있습니다. Paul은 여가 시간을 활용해 MFC 클래스 라이브러리인 PixieLib을 만들어 그의 웹 사이트인 www.dilascia.com에 공개하고 있습니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

최근 고객사의 요청으로 ActiveX 컨트롤 하나를 유지보수하고 있습니다. ActiveX 컨트롤에서 탈피하려는 추세가 있지만, 별 다른 대안이 없어서 ActiveX 컨트롤을 유지보수해야 하는 경우도 아직 우리나라에서는 상당히 많은것 같습니다.

 

Internet Explorer 7.0부터는 보호 모드라는 개념이 새로 소개되었습니다. 보호 모드란, 일종의 Sand-box 개념으로 기존과 같이 현재 로그온한 사용자의 권한을 그대로 물려받아 무분별하게 실행되는 것을 방어하는 안전 장치입니다. Windows XP와는 달리 Windows Vista부터는 일반 사용자를 단순히 관리자로 분류하지 않고, UAC를 통하여 작업에 대해 허가/거절 여부를 정할 수 있게 하였습니다.

 

우리가 권한 상승이라고 이야기하는 기능은 사실 권한 상승을 사용자에게 요청하는 것입니다. 이러한 권한 상승 요청을 구현하기 위하여 이제까지 참고할 수 있는 보편적인 리소스는 EXE 파일과 함께 매니페스트 파일을 배포하는 것이 대표적인 것이었습니다. 그리고 ActiveX 컨트롤의 경우 Elevation Moniker를 통하여 권한 할당을 받는 것이 대표적입니다.

 

ActiveX 컨트롤에서 어떻게 권한 상승을 구현할 수 있는지에 대하여 잘 정리한 권용휘 MVP님의 아티클을 참고하시면 어떻게 권한 상승이 이루어지고 관리될 수 있는지에 대한 컨셉을 확인하실 수 있습니다. (http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNO=20&no=7669) 저는 권용휘 MVP님의 아티클 위에 몇 가지 내용을 더 첨언해보고자 합니다.

 

1. Visual C++ 6.0 (SP6)에서의 권한 상승 구현

 

놀랍게도, 그리고 안타깝게도, Visual C++ 6.0 개발 도구를 업그레이드하지 못하는 이슈는 도처에 널려있습니다. 기존에 개발되어있던 소프트웨어나 라이브러리가 특정 문자 세트에 완벽하게 맞추어져있지만, 업그레이드할 수 없을만한 이슈 (개발 업체의 부도 - 또는 - 계약 해지 / 담당자의 연락 두절과 같은)로 인하여, 올해부터는 더 이상 일체의 기술 지원을 받을 수 없는 (서비스 팩 다운로드도 MS 공식 홈페이지에서는 더이상 받으실 수 없습니다.) 그런 개발 플랫폼위에서 고군분투해야 하는 상황은 생각보다 자주 있습니다.

 

사실 권한 상승을 제대로 프로그래밍하려면 Windows Vista나 Windows 7 SDK가 필요합니다. 하지만 이들 SDK의 코드를 가져다 사용하려면 개발 도구를 Visual C++ 2005나 2008로 업그레이드할 필요가 있습니다. 하지만 개발 도구나 Windows SDK를 설치하지 않고 간단히 적용할 수 있다고 소개된 방법은 다행히 Visual C++ 6.0에서 온전하게 동작합니다.

 

typedef struct tagBIND_OPTS3 : tagBIND_OPTS2 {
 HWND hwnd;
} BIND_OPTS3, *LPBIND_OPTS3;

위의 코드를 자주 사용하는 헤더에 선언해두면 권한 상승을 위하여 호출하는 CoGetObject에 정확히 바인딩할 수 있습니다. 그리고, 아래의 두 함수를 이용하여, 권한 상승을 지원하는 운영 체제 (Windows Vista 이상의 운영 체제)를 식별하고 실제로 객체를 생성할 수 있습니다.

 

HRESULT CoCreateInstanceAsAdmin(HWND hwnd, REFCLSID rclsid, REFIID riid, OUT void ** ppv)
{
 BIND_OPTS3 bo;
 OLECHAR wszCLSID[50];
 OLECHAR wszMonikerName[300];
 const int CHARS_IN_GUID = 39;

 StringFromGUID2(rclsid, wszCLSID, CHARS_IN_GUID);
 swprintf(wszMonikerName, L"Elevation:Administrator!new:%s", wszCLSID);
 wprintf(L"%s\r\n", wszCLSID);
 wprintf(L"%s\r\n", wszMonikerName);

 memset(&bo, 0, sizeof(bo));

 bo.cbStruct = sizeof(bo);
 bo.hwnd = hwnd;
 bo.dwClassContext = CLSCTX_LOCAL_SERVER;

 return CoGetObject((LPCWSTR)wszMonikerName, &bo, riid, ppv);
}

 

BOOL IsUACRequiredOperatingSystem(void)
{
 BOOL bIsUACRequired = FALSE;
 OSVERSIONINFO sInfo;
 sInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

 if (GetVersionEx(&sInfo))
 {
  if( sInfo.dwPlatformId == VER_PLATFORM_WIN32_NT &&
   sInfo.dwMajorVersion >= 6 &&
   sInfo.dwMinorVersion >= 0) { // Windows VISTA or Higher
   bIsUACRequired = TRUE;
  }
 }

 return bIsUACRequired;
}

 

Visual C++ 6.0 기반의 프로젝트들은 거의 대부분 ANSI나 Multibyte Character Set 기반의 문자 세트를 기준으로 프로그램이 구성되어있습니다. 확장성을 위하여 Transition이 가능한 데이터 타입 (LPTSTR, LPCTSTR, TCHAR 등)은 거의 고려하지 않았을 확률이 높습니다. 하지만 우리가 호출해야 할 API들은 유니코드 문자열을 필요로 하기 때문에 직접 유니코드를 사용하도록 기존 코드를 수정하였습니다. 그리고 더불어서, StringCchPrintf 함수를 대신하여 sprintf 계열의 함수를 직접 이용하였습니다.

 

2. 실제 ActiveX 컨트롤에 권한 상승을 적용하는 또 다른 방법

 

권용휘 MVP님의 아티클에서 설명하는 방법은, 특정 메서드나 프로퍼티의 호출 과정에서 필요로하는 권한 상승을, 권한 상승을 외부에서 요구하고 결과를 받기 위한 프록시 멤버와, 실제 처리하는 멤버로 이원화하여 구현하는 방식입니다. 대부분의 경우 이 방법으로 해결할 수 있다고 생각합니다. 하지만 간혹, ActiveX 컨트롤의 동작이나 상태, 속성을 정의하는 작업 전체가 시스템에 종속적으로 구성된 경우도 있을 수 있습니다. 이 경우에는, 격리모드에서 실행되는 ActiveX 컨트롤과는 별도로 새로운 인스턴스를 노출시켜야 할 필요가 있습니다.

 

이에 대한 솔루션은 인터넷 검색중에 발견한 어떤 블로그의 아티클에서 찾을 수 있었습니다. (http://hbesthee.tistory.com/620) 이 아티클은 델파이 기반의 솔루션을 제시하고 있었으며 저는 이것을 Visual C++ 기반의 코드로 옮겼습니다.

STDMETHODIMP CtrusEBANK::trusElevate(VARIANT *ret)
{
 AFX_MANAGE_STATE(AfxGetStaticModuleState())

 if (IsUACRequiredOperatingSystem())
 {
  ItrusEBANK *pElevatedObj = NULL;
  HRESULT hr = CoCreateInstanceAsAdmin(NULL,
   CLSID_trusEBANK, IID_ItrusEBANK,
   (void **)&pElevatedObj);

  if (SUCCEEDED(hr))
  {
   ret->vt = VT_DISPATCH;
   ret->pdispVal = pElevatedObj;
   ret->pdispVal->AddRef();
   return S_OK;
  }
  else
  {
   ret->vt = VT_NULL;
   return hr;
  }
 }
 else
 {
  ret->vt = VT_BOOL;
  ret->boolVal = VARIANT_FALSE;
 }

 return S_OK;
}

 

STDMETHODIMP CtrusEBANK::trusNeedElevate(VARIANT *ret)
{
 AFX_MANAGE_STATE(AfxGetStaticModuleState())

 ret->vt = VT_BOOL;

 if (::IsUACRequiredOperatingSystem())
  ret->boolVal = VARIANT_TRUE;
 else
  ret->boolVal = VARIANT_FALSE;

 return S_OK;
}

 

위의 코드에서는 trusNeedElevate를 이용하여 권한 상승이 지원되는 운영 체제인지 파악하는 부분과, trusElevate를 이용하여 권한 상승이 적용될 개체를 반환하는 방법 두 가지를 보여주고 있습니다. 그리고 아래의 코드는 자바스크립트에서의 실제 사용 예시입니다.

 

var obj = new ActiveXObject("EBANK.trusEBANK");

if (obj.trusNeedElevate())
{
 var result = obj.trusElevate();
 if (result) {
  obj = result;
 }
}

// 여기서부터 obj 객체 사용

 

이와 같은 방법을 통하여, 권한 상승이 온전하게 ActiveX 컨트롤에 적용될 수 있도록 하여, 원활한 동작 환경을 구현할 수 있을 것입니다.

 

3. Internet Explorer Host Window를 Elevation하는 방법 [업데이트]

 

그리고 최근에 저는 여러가지 방법을 적용해보던 중에 가장 현실적인 타협안 하나를 발견하였습니다. 바로, Internet Explorer Host Window 자체를 Runtime 도중에 Elevation 처리하는 방법으로, 기존의 응용프로그램 기반 매니페스트와 유사하게 작동합니다. 그리고 이 방법은, Host Window를 Elevation하는 데에 사용할 수 있는것 뿐만 아니라, 대리 실행해야 하는 응용프로그램의 Manifest 보유 여부에 관계없이 Elevation에도 사용될 수 있습니다.

 

참고로, 이 방법은 http://www.softblog.com/2008-02/vista-tools/ 에서 소개한 VistaTools.cxx 파일의 코드를 일부 발췌한 것임을 밝혀둡니다.

 

typedef struct _TOKEN_ELEVATION {
    DWORD TokenIsElevated;
} TOKEN_ELEVATION, *PTOKEN_ELEVATION;

 

위의 구조체는 Elevation 상태를 점검하기 위하여 필요한 구조체로 이미 Elevation 처리가 되어있는 호스트 위에서 실행되는 경우 Elevation을 다시 실행하지 않도록 하기 위하여 필요합니다. 이 구조체는 Visual C++ 6.0 기반에서 프로그램을 업데이트할 때 수동으로 지정해야 합니다.

 

#ifndef CSIDL_PROGRAM_FILES
  #define CSIDL_PROGRAM_FILES 0x0026
#endif // CSIDL_PROGRAM_FILES

 

위의 Special Folder Constant는 %PROGRAMFILES% 폴더의 경로를 가져오기 위하여 필요합니다. 마찬가지로 Visual C++ 6.0 기반에서 프로그램을 업데이트할 때 수동으로 지정해야 합니다.

 

#include <shellapi.h>
#include <shlobj.h>

 

마지막으로 위의 두 헤더 파일을 참조하도록 선언하면 일단 준비는 끝납니다.

 

BOOL IsVistaOrHigher(void)
{
 OSVERSIONINFO versionInfo;
 versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

 if (GetVersionEx(&versionInfo) &&
  versionInfo.dwPlatformId == VER_PLATFORM_WIN32_NT &&
  versionInfo.dwMajorVersion >= 6)
  return TRUE;
 else
  return FALSE;
}

 

HRESULT IsElevated(BOOL *pElevated)
{
 HRESULT hResult = E_FAIL;
 HANDLE hToken = NULL;

 if (!IsVistaOrHigher())
  return hResult;

 if (!OpenProcessToken(
  GetCurrentProcess(),
  TOKEN_QUERY,
  &hToken))
  return hResult;

 TOKEN_ELEVATION te = { 0 };
 DWORD dwReturnLength = 0;
 const int TokenElevation = 20;

 if (GetTokenInformation(
  hToken,
  (TOKEN_INFORMATION_CLASS)TokenElevation,
  &te,
  sizeof(te),
  &dwReturnLength))
 {
  hResult = te.TokenIsElevated ? S_OK : S_FALSE;

  if (pElevated)
   *pElevated = (te.TokenIsElevated != 0);
 }

 CloseHandle(hToken);
 return hResult;
}

 

BOOL ShellExecWithVerb(HWND hWnd, LPCTSTR lpVerb, LPCTSTR lpPath, LPCTSTR lpParameters, LPCTSTR lpDirectory)
{
 SHELLEXECUTEINFO executeInfo;
 memset(&executeInfo, 0, sizeof(executeInfo));

 executeInfo.cbSize = sizeof(SHELLEXECUTEINFO);
 executeInfo.fMask = 0;
 executeInfo.hwnd = hWnd;
 executeInfo.lpVerb = lpVerb;
 executeInfo.lpFile = lpPath;
 executeInfo.lpParameters = lpParameters;
 executeInfo.lpDirectory = lpDirectory;
 executeInfo.nShow = SW_NORMAL;

 return ShellExecuteEx(&executeInfo);
}

 

BOOL ShellExecWithElevation(HWND hWnd, LPCTSTR lpPath, LPCTSTR lpParameters, LPCTSTR lpDirectory)
{
 return ShellExecWithVerb(hWnd, _T("runas"), lpPath, lpParameters, lpDirectory);
}

 

BOOL OpenUrlWithElevation(HWND hWnd, LPCTSTR lpUrl)
{
 _TCHAR lpBuffer[MAX_PATH + 1];

 if (!SHGetSpecialFolderPath(hWnd, lpBuffer, CSIDL_PROGRAM_FILES, 0))
  return FALSE;

 _tcscat(lpBuffer, _T("\\Internet Explorer\\iexplore.exe"));
 return ShellExecWithElevation(hWnd, lpBuffer, lpUrl, _T(""));
}

 

위의 코드의 내용들 중에서 강조표시된 것이 실제 Elevation을 위하여 필요한 코드입니다. 그리고 실제 사용법은 아래와 같습니다.

 

STDMETHODIMP CMyAXControl::RunElevatedWeb(VARIANT *url)
{
 AFX_MANAGE_STATE(AfxGetStaticModuleState())
 USES_CONVERSION;

 CComBSTR targetUrl;
 url->vt = VT_BSTR;
 targetUrl = CComBSTR(url->bstrVal);

 _TCHAR buffer[MAX_PATH + 1];
 _tcscpy(buffer, (_TCHAR*)OLE2T(targetUrl.m_str));
 BOOL bResult = OpenUrlWithElevation(NULL, buffer);

 if (bResult)
  return S_OK;
 else
  return E_FAIL;
}

 

그리고 위의 메서드가 실제로 실행되어야 할 때와, 그렇지 않을 때를 구분하기 위하여 아래와 같이 상태 확인을 위한 메서드를 배치하는 것도 도움이 됩니다. :-)

 

STDMETHODIMP CMyAXControl::NeedElevate(VARIANT *ret)
{
 AFX_MANAGE_STATE(AfxGetStaticModuleState())

 ret->vt = VT_I4;
 ret->intVal = 0;

 if (IsVistaOrHigher())
 {
  BOOL bResult = FALSE;

  if (SUCCEEDED(IsElevated(&bResult)))
  {
   if (bResult == TRUE)
    ret->intVal = 4; // 이미 Elevation이 완료됨
   else
    ret->intVal = 3; // Elevation이 필요함
  }
  else
   ret->intVal = 2; // 상태 정보를 조회할 수 없음
 }
 else
  ret->intVal = 1; // UAC가 지원되지 않는 운영체제로 판단함

 return S_OK;
}

 

위의 메서드를 통하여, 반환값이 1 - 또는 - 4로 반환되는 경우에 한정하여 실제로 필요한 코드를 실행하고, 그렇지 않은 경우 Elevation을 수행하도록 유도하는 코드를 웹에서 작성할 수 있을 것입니다.

 

덧) 지적하거나 수정이 필요한 부분이 있으시면 댓글로 남겨주시면 바로 반영하도록 하겠습니다. 감사합니다. :-)

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

이미 여러 블로그 아티클로도 잘 다뤄진 내용이지만 몇 가지 부수적인 내용을 덧붙여서 ActiveX 패키징 및 배포에 관한 내용을 총 정리해보았습니다. 실무에서는 아직도 ActiveX 컨트롤에 관한 유지보수를 틈틈이 수행할 필요가 있다보니 이런 문서를 작성하게 되는 것 같습니다. :-)

1. INF 파일에 대한 이해

INF 파일은 ActiveX 패키지 배포 시 내장되는 설치 정보 파일로 보통 배포되는 CAB 파일의 이름과 동일하게 맞추어서 배포합니다. 예를 들어 MyControl.cab을 배포한다면 MyControl.inf 파일을 내장하고 있어야 하는 것입니다.

그리고 INF 파일의 문법적인 구조에 대한 상세한 설명이 없어서 상당히 답답한 때가 많습니다만 간략하게 두 가지 사례를 짚어서 설명해보도록 하겠습니다.

a. Hooking 없이 설치하는 경우

Hooking이란 설치 도중에 별도의 외부 프로세스를 이용하여 설치 자체를 자동화하거나 설치 전/후 과정을 제어하기 위한 일종의 옵션 기능입니다. ActiveX에 수많은 보안 결함이 존재하지만 디자인적으로 보았을 때에도 충분히 문제가 드러납니다. 대부분 간단한 유형의 ActiveX 컨트롤은 Hooking 기능 없이 배포가 되는 편입니다. 다음의 샘플 INF 코드를 살펴보기로 합니다.

더보기

흔히 볼 수 있는 형태의 INF 파일입니다. 제일 상단에 있는 것이 INF 파일의 형식에 관한 기본 정보이고 Windows 95 이후로부터는 대체로 Advanced INF 2.0 규격을 따르는 것 같네요. (시그니처에 Windows 95의 코드 네임인 Chicago가 적혀있는 것으로 보았을 때도 그러합니다.)

하단의 [Add.Code] 섹션에서는 이 CAB 파일을 통해서 배포될 파일들을 기술합니다. 편의 상 파일 이름과 엔트리 이름을 일치시켜두는 것도 좋습니다. 그리고 각각의 파일명으로 기술된 섹션이 해당 파일을 어떤 식으로 복사되고 관리되어야 하는지를 서술하는 부분으로 이후에 설치 제거를 할 때 근거 정보가 됩니다.

각 섹션 별로 file-win32-x86 이라는 엔트리 명이 보이는데 파일의 플랫폼과 대상 아키텍처를 지정하는 부분입니다. 옛날 (NT 시절)에는 Alpha나 MIPS 계열 프로세서를 위하여, 요즈음에는 x64 (AMD64) 프로세서를 위하여 이 부분이 달리 할당될 수도 있을 것입니다. 그리고 thiscab 이라는 값은 이 CAB 파일 내에 파일이 있음을 뜻하는 부분입니다.

DestDir 엔트리는 이 파일이 어느 위치로 복사되어야 하는지를 나타내는 부분으로 많은 옵션이 있을것 처럼 보이지만 실제로 알려진 것은 세 종류인듯 합니다. 값을 10으로 지정하면 %windir% 경로 (흔히 C:\Windows)에, 11로 지정하면 %windir%\system32 (Windows 9x 계열 운영체제에서는 %windir%\system)에, 비워두면 OCCACHE 디렉터리 (%windir%\Downloaded Program Files)에 복사됩니다.

그리고 이제 OCX 파일 섹션의 내용을 살펴보기로 합니다. OCX 파일의 내용에서 꼭 확인해야 할 것은 RegisterServer=yes 항목과 Version 항목, 그리고 clsid 항목입니다. Version은 OCX 파일을 빌드할 때 추가되는 Win32 리소스의 버전 테이블 정보와 일치할 필요가 있으며, clsid 항목은 실제로 브라우저에서 인스턴스화되어 실행되어야 하는 COCLASS의 CLSID와 일치해야 합니다.

b. Hooking의 경우

더보기

ActiveX를 이용하여 대리 설치 프로그램을 기동시키기 위하여 위와 같이 작성할 수도 있지만, 컴포넌트별로 Hook을 다르게 지정할 수도 있으니 참고하시기 바랍니다. Hook을 실행하기 위하여 %EXTRACT_DIR% 라는 지역 환경 변수를 지정한 것을 볼 수 있고 프로그램 매개 변수까지 지정이 가능한 것을 보실 수 있습니다.

Hook에서 지정한 프로그램의 경우 주의 사항이 한 가지 있다면, 프로그램의 실행 종료 코드가 0으로 끝날 필요가 있다는 것입니다. 그렇지 않으면 설치가 중간에 실패한 것으로 인지되어 브라우저에는 아무것도 표시되지 않는다고 합니다. 이를 미리 확인해보려면 패키징 하기 전에 Hook을 통해서 구동하려는 프로그램을 똑같이 명령 프롬프트에서 아래와 같이 실행하면 알 수 있습니다.

START /WAIT "실행할 프로그램 및 매개 변수들"
ECHO %ERRORLEVEL%

%ERRORLEVEL% 환경 변수의 값이 0이 아닌 것으로 나타나면 Hook 프로그램으로는 사용할 수 없을 것입니다.

2. PVK 파일과 SPC 인증서 파일을 PFX 파일로 변환하기

SIGNCODE 도구에서 SIGNTOOL 도구로 새롭게 바뀐 이후부터, ActiveX CAB 파일에 서명을 하거나 EXE 파일 위에 서명을 하는 방법이 조금 바뀌었습니다. 바로 PFX 파일을 생성하는 작업이 추가 된 것인데, 기존에 가지고 있는 인증서 파일 형식이 PVK 파일과 SPC 파일 두 가지로 구성된 경우 아래와 같이 변환해야 합니다.

PVK2PFX -PVK "PVK 파일의 경로" -SPC "SPC 파일의 경로" -PFX "PFX 파일을 만들 경로" -PI "기존 인증서의 Password" -F

PVK2PFX 유틸리티는 .NET Framework SDK v2.0 혹은 Windows SDK v6.0 이상을 설치하면 같이 따라오는 유틸리티입니다. 여기서 -PVK 스위치 다음에 PVK 파일의 경로를, -SPC 스위치 다음에 SPC 파일의 경로를 서술해주어야 하고, 그 다음 -PFX 스위치 다음에 새로 만들 PFX 파일의 경로를 서술해야 합니다. 추가적으로 인증서로부터 암호를 해독하고 PFX 파일에 암호를 새로 설정하기 위하여 -PI 스위치 뒤에 인증서 암호를 기술합니다. 기본 동작이 기존 파일이 있을 경우 실패로 처리되는데 편의상 -F 스위치를 지정하여 PFX 파일을 덮어쓰도록 실행합니다.

3. CAB 파일에 서명을 할 때에는 반드시 Spanning Space를 확보할 것

CAB 파일에 서명을 추가하기 위해서는 별도의 Spanning Space를 확보해야 하는데 이를 위해서는 CABARC 유틸리티로 압축해야 합니다. (다른 압축 유틸리티에서는 지원하지 않을 수도 있습니다.)

CABARC -S 6144 N "생성할 CAB 파일의 경로" "파일1" "파일2" ... "파일n"

-S 스위치가 Spanning Space 확보를 지시하는 스위치로 이 뒤에 Magic Constant 값 6144를 지정합니다. 이것이 인증서 크기에 관한 Magic Constant로 문서화된 수치이므로 그대로 사용하면 됩니다. 새로운 파일을 만들어야 하므로 N 스위치를 지정하여 모드를 변경하고, 그 다음에는 CAB 파일이 생성될 경로, 그리고 그 다음부터는 CAB 파일에 포함할 파일들을 하나씩 서술하면 됩니다. 와일드 카드도 지원하므로 편리합니다.

4. 실제로 서명하고 확인하기

SIGNCODE 프로그램보다 기능이 다양하고 정교해진 SIGNTOOL을 이용해서 서명을 하려면 다음의 스위치 설정을 활용합니다.

SIGNTOOL SIGN /F "PFX 파일 경로" /P "PFX 파일 암호" /T "타임스탬프 URL" "대상 파일 1" "대상 파일 2" ... "대상 파일 n"

SIGNTOOL 프로그램을 서명 모드로 실행하기 위하여 SIGN 스위치를 지정하고, /F 스위치에는 2단계에서 만든 PFX 파일, 그리고 /P 스위치에는 2단계에서 지정한 인증서 암호를 입력합니다. /T 스위치 뒤에는 실제로 접속 가능한 타임스탬프 URL을 지정해야 하는데 이는 인증서 발행 업체가 제공하는 고유 URL을 이용하면 됩니다. 마지막으로 대상 파일들을 지정하면 한꺼번에 동일한 인증서로 서명할 수 있습니다.

CAB 파일을 제외한 보통의 EXE, DLL, OCX 등에 직접 서명하는 것은 별도로 빌드할 때 설정해주어야 할 특별 옵션같은것들은 존재하지 않습니다.

좀 더 자세한 내용을 보시려면 아래 Article들도 참고하시면 좋겠습니다.

http://ditongs.egloos.com/1511212
http://littletrue.egloos.com/3968904


크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

닷넷 프레임워크 기술들 중 가장 적게 알려지고 그 비중이 많이 축소되어 소개되는 부문이 바로 Visual C++ CLR일 것입니다. 생각보다 C++ CLR이 유용할 수 있음에도 불구하고 여론에 떠밀려서 거의 사장되다시피하고 있지요. 이번 블로그 포스트에서는 C++ CLR을 통해서 실용적인 코딩 하나를 해볼까합니다.

C++과 더불어 관련 라이브러리들 (STL, ATL, WTL, Boost, MFC 등)의 경우 일반적인 응용프로그램을 작성하거나, 성능 튜닝이 필요한 응용프로그램들을 작성하거나, 수학 라이브러리 등의 힘을 빌어 불필요한 오버헤드가 없는 고속 연산을 처리하는 등의 목적에 알맞게 디자인되어있습니다. 그러나 엔터프라이즈 프로그래밍의 경우에서처럼 구조화된 처리를 수행해야 하는 경우 상당히 불편한 점이 많습니다. 간단한 예로 당장 XML 문서 하나 분석하는 것도 썩 편리하지는 않지요.

Visual Studio 패밀리에서 공식적으로 제공되는 기능은 아니지만 C++ CLR도 닷넷 프레임워크 위에서 기동되는 어셈블리를 작성할 수 있습니다. 그리고 더 중요한 것은 기존의 x86 코드나 x64 코드를 MSIL 어셈블리 사이에 끼워넣을 수 있다는 점입니다. 이러한 사실을 바탕으로 하여 하이브리드 코드를 만들 수 있습니다.

그럼 하나씩 살펴보도록 하겠습니다.

1. C++ CLR 기반의 클래스 라이브러리 프로젝트 하나를 만듭니다. 프로젝트 이름으로 여기서는 "CLRxASPX"를 사용하기로 하겠습니다.

2. 여느 닷넷 프로젝트처럼 예상 가능한 종류의 파일들이 만들어집니다. AssemblyInfo.cpp 파일은 어셈블리 어트리뷰트를 정의하는 부분으로 다음과 같이 코드가 구성되어있습니다.

AssemblyInfo.cpp 보기

C#에서와 마찬가지인 어트리뷰트 사용법입니다. 다만 C++ 컴파일러의 특성상 空문장임을 뒷쪽에 명시해둔 것이 다른 부분이라고 할 수 있겠습니다. 그리고 네임스페이스의 참조 방법이 C#과 동일하게 되어있습니다. C#에서 흔히 사용하는 using은 C++의 using namespace와 같습니다. 그리고 C++에서 흔히 사용하는 using std::cout; 같은 참조는 C#에서는 using Console = System.Console; 과 같이 표현이 가능합니다.

특이한 점이 하나 더 있다면 stdafx.h의 존재입니다. MFC나 ATL 프로젝트에서 Precompiled Header라는 명목으로 많이 사용되던 것이 C++ CLR에도 그대로 적용된 걸 볼 수 있습니다. 거리낌없이 그 개념 그대로 stdafx.h에 Windows API 헤더를 추가하거나 프로젝트 전반에 걸쳐 사용하고 싶은 다른 라이브러리의 헤더를 추가하면 됩니다.

그럼 stdafx.h는 어떻게 구성되어있는지 한 번 살펴보도록 하겠습니다.

stdafx.h 보기

C++ CLR의 유용함이 느껴지는 부분입니다. 굵게 표시한 부분이 예제를 위하여 추가된 코드로, Win32 API 헤더를 추가한 것입니다. WIN32_LEAN_AND_MEAN은 컴파일러나 링커의 처리를 최소화하기 위한 것으로 통계적으로 사용 확률이 그닥 높지 않은 코드, 라이브러리를 사전에 제거해주는 옵션입니다. 예제에서는 API 참조를 위해서만 stdafx.h를 이용한 것이므로 stdafx.cpp에는 다른 서술이 되어있지 않아서 생략합니다.

이제 실제 코드를 살펴보기로 합니다.

CLRxASPX.h 보기

using namespace로 모든 네임스페이스의 요소들을 참조할 수도 있지만  필요한 항목들만 위와 같이 참조할 수도 있습니다. 그리고 아래의 클래스 선언을 보면 일반 C++ 클래스 선언과는 구별되는 내용들이 몇 가지 있습니다. 우선, class 키워드 앞에 ref 키워드가 사용된 걸 볼 수 있는데 이것이 이 클래스가 Managed Class 임을 나타내는 것입니다. 그 다음에는 상속 대상을 ASP.NET의 페이지 클래스로 정한 것이 보입니다.

Protected 멤버 메서드로 Page_Load 메서드가 선언된 것이 보입니다. 이 때 알아둘 것이 있는데 ASP.NET은 내부적으로 Reflection을 통해서 실제로 Page 클래스의 Load 이벤트에 추가한 이벤트 처리기가 아니라도 자동으로 이름을 통하여 호출 대상을 결정하기 때문에 위의 메서드는 항상 실행됩니다.

그리고 C++ CLR에서 닷넷 프레임워크의 형식들을 이용할 때에는 닷넷 프레임워크 상의 관리되는 참조 객체임을 표현하기 위하여 과거의 Managed Extensions for C++ 시절때 사용하였던 __gc * 형식 대신 ^ 기호를 사용합니다. 포인터의 * 와는 구분되는 것으로 주소값을 얻기 위한 형변환 시도는 허용되지 않습니다. 이렇게 사용되는 형변환은 주소값에 대한 형변환이 아니라 형식 자체에 대한 형변환 시도가 됩니다. 별도로 해당 형식이 암묵적/명시적 형변환 연산자를 가지고 있지 않을 경우 상속 관계와는 무관하므로 컴파일이 실패합니다. 참고로, Page_Load 메서드가 이벤트 핸들러로서의 조건을 충족하려면 반환 형식은 void, 즉 반환 대상이 없으며, 두 개의 매개 변수를 받아들여야 합니다. 하나는 이벤트를 발행한 출처의 참조, 또 하나는 이벤트 인자 개체의 참조입니다. 자세한 내용은 System::EventHandler 대리자를 참고하시면 됩니다.

CLRxASPX.cpp 보기

구현하기 나름이지만, 원래의 C++ 코드 스타일을 유지하기 위하여 선언부와 구현부를 나눈다면 위와 같은 형태로 cpp 파일에 메서드 본문을 작성할 수 있습니다. 이 때, 메서드의 시그니처에 전체 네임스페이스가 지정되어야 함을 유의합니다.

windows.h 헤더 파일 내에 포함되어있는 항목들 중 시스템 정보를 추출하는 API를 이용해보기로 합니다. 이 부분은 C#이나 기타 닷넷 환경에서는 원래 Platform Invoke를 통하여 우회적으로 마샬링해야 하는 부분이지만 C++ CLR의 특성에 따라 Native Code를 평소처럼 실행할 수 있다는 것을 보여줍니다. SYSTEM_INFO 구조체의 주소값을 전달하여 시스템 정보를 구조체에 채우도록 만들고 이를 ASP.NET 서비스에서 보여준다는 것이 예제 프로그램의 컨셉입니다.

Label 컨트롤을 생성하기 위하여 gcnew 연산자를 사용하였습니다. 역시 C++에서 원래 사용하던 new 연산자와 구분되는 것입니다. gcnew 연산자로 관리되는 객체를 만들고, Text 프로퍼티에 문자열을 지정하였습니다. 관리 객체에 지정되는 문자열은 자동으로 System::String의 인스턴스로 분류되어 처리되도록 만들어져 있기 때문에 유니코드 문장임을 나타내는 접두사 L 없이도 쓰일 수 있습니다. 또한, 아랫쪽 코드를 보면 System::String 형식에만 정의되어있는 문자열간 합치기 기능이 C++에서도 동작하는 것을 볼 수 있습니다.

위의 예제 코드를 작성한 다음, 결과를 확인하기 위하여 아래와 같이 IIS 7.0에서 사용할 수 있는 web.config 파일을 작성하여 웹 어플리케이션 디렉터리를 구축하고, 해당 웹 어플리케이션 디렉터리 바로 아래의 Bin 폴더에 이 프로젝트로 빌드한 DLL을 저장하였습니다.

web.config 보기

위의 내용에서 확인해야 할 부분이 두 가지가 있습니다. IIS 7의 system.webServer 섹션은 닷넷 프레임워크가 처리하는 것이 아니라 IIS 7이 스스로 처리하는 부분입니다. 우선 핸들러에 type이라고 되어있는 부분은 닷넷 프레임워크에서 불러들일 IHttpHandler 인터페이스와 호환되는 형식의 약식 명칭을 지정하는 부분입니다. Page 클래스를 상속함으로서 이 부분이 자동으로 구현되었기 때문에 우리가 방금 작성한 C++ CLR 클래스의 약식 명칭을 여기에 기술합니다. 네임스페이스와 클래스 이름, 그리고 Bin 폴더 내에 들어있는 클래스 라이브러리의 어셈블리 이름을 여기에 기출합니다. 그리고 preCondition에서는 이 핸들러가 통합 모드에서 실행되어야 하고 닷넷 프레임워크 2.0 런타임을 사용하여 구동되어야 함을 표기하고 있습니다.

이제 실행 결과를 살펴보도록 하겠습니다.

C++ CLR이 Native Code와 잘 연동되어 ASP.NET 서비스를 수행하고 있는 것을 볼 수 있습니다. 여기서는 공식적으로 프로젝트 템플릿이 제공된 것이 아니라서 이와 같이 간소한 형태로만 프로그램을 작성하였지만 C++ CLR로도 이와 같이 프로그램 작성을 할 수 있다는 것을 확인할 수 있었습니다. Platform Invoke로 해결하기 어려운 문제는 이와 같이 프로그램의 형태에 제한을 갖지 않고 직접 C++의 힘을 빌릴 수도 있다는 것을 기억하면 쉽게 문제를 풀어나갈 수 있을 것입니다.

참고로 C++/CLI의 ECMA 표준안 문서는 http://www.ecma-international.org/publications/standards/Ecma-372.htm 에서 확인가능합니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

ATL 기반 In-Process Server를 이용하여 코드를 프로그래밍하는 것은 경우에 따라서는 일을 간단하게 마무리지을 수도 있지만 때로는 깊은 생각을 필요로 하기도 한다. 특히, 하나 이상의 다중 스레드를 이용하여야 하는 경우가 더욱 그런데, ATL Simple Object의 FinalConstruct와 FinalRelease 메서드의 재정의를 이용하여 스레드를 관리하는 방법을 처음 써보았다. 하지만 문제점 투성이였고 이상한 버그가 계속 나타났다.

그래서 생각하게 된 것이 ATL에서 제공하는 간단한 Win32 Window Implementation 클래스를 활용하는 방법이다. 클라이언트에서 데이터를 주거나 받기 위한 목적으로 In-Process Server를 활용하는 과정이었으므로 WSAAsyncSelect를 이용하기에도 편리했다. WSAAsyncSelect로 ATL Window의 핸들을 넘겨주고 ATL Window에서는 해당 메시지에 대한 처리 핸들러만 추가하면 그 다음은 내가 종전에 일반 스레드로 구현하였던 코드를 그대로 가져가기만 하면 되는 것이라서 마이그레이션에 특별히 어려운 점도 없었다.

이렇게하여 select() 함수를 통한 I/O Multiplexing 기능 개선과 함께 더 호환성이 좋은 이벤트 시스템을 구축할 수 있었다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

1. IDispatch 인터페이스에 대한 COM Smart Pointer 만드는 법

[CODE]
_com_ptr_t<_com_IIID<IDispatch, &__uuidof(IDispatch)> > oDispatch1(/* 이곳에 Interface Pointer를 인수로 지정한다. */);
[/CODE]

2. 이것을 매크로 구문으로 만든다면...

[CODE]
#ifndef COM_PTR
#define COM_PTR(x) _com_ptr_t<_com_IIID<x, &__uuidof(x)> >
#endif // COM_PTR

COM_PTR(IDispatch) oDispatch1(/* 이곳에 Interface Pointer를 인수로 지정한다. */);
[/CODE]

* 필요한 헤더 파일: comip.h

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

Serialization을 구현하기 위하여 우선 가장 기초가 되어야할 사항은 새로운 클래스를 반드시 CObject 클래스로부터 상속받아야 하는 점입니다. 하지만, 이것이 다는 아니며 약간의 작업이 더 필요하다는 것은 잘 아실 것입니다.

어떤 목적의 클래스인지에 따라서 적절한 매크로 함수를 사용해야 할 것입니다. 그래서, 세 가지 종류로 구분이 되어있습니다.

  1. IMPLEMENT_DYNAMIC(), DECLARE_DYNAMIC(): CRuntimeClass를 통한 런타임 클래스 정보를 사용할 수 있도록 필요한 선언을 매크로 함수 한 줄로서 자동으로 추가할 수 있도록 만들어줍니다. DECLARE_DYNAMIC(현재 클래스 이름): 이것은 클래스의 선언부 (헤더 파일)에서 직접 써주시면 자동으로 관련된 내용을 클래스에 선언해줍니다. IMPLEMENT_DYNAMIC(현재 클래스 이름, 상위 클래스 이름): 이것은 클래스의 소스 코드 부분 (CPP 파일)의 상단부에 기재하여 주시면 됩니다.
  2. IMPLEMENT_DYNCREATE(), DECLARE_DYNCREATE() : 런타임 정보와 동적 개체 생성, Assert/Valid 등을 지원합니다. 사용 방법은 역시 동일합니다.
  3. IMPLEMENT_SERIAL(), DECLARE_SERIAL(): CObject 클래스에서 사용할 수 있는 모든것을 가능하도록 지원합니다. 동적 개체 생성, 직렬화 등등이 있겠군요. 사용 방법은 역시 동일합니다.  
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

Windows 기반에서 웹 그래픽을 처리하기 위해서는 이전까지는 JPEG나 GIF 이미지의 Static Library 또는 관련 DLL들을 얻어서 써야 하는 매우 번거로운 작업을 해야만 했습니다. 하지만, MFC 7.0에서는 이러한 수고스러움을 덜어주기 위하여 새로운 클래스를 도입하게 되었는데, 바로 CImage 클래스입니다.

CImage 클래스는 CBitmap 클래스의 확장 개념입니다. 기존의 비트맵 이미지 객체로도 손쉽게 불러올 수 있음은 물론이고 기본적인 수준의 GIF, JPEG, PNG, BMP 이미지를 사용할 수 있으므로 공들여서 웹 그래픽 작업을 번거롭게 하는 일은 없어졌습니다.

여러분은 이미지를 불러들이기 위하여 두 가지 함수를 사용하실 수 있습니다. Load()와 LoadFromResource()라는 함수입니다. Load() 함수는 파일 경로명을 지정하거나 IStream 인터페이스의 포인터 객체를 얻는 방법이 있습니다. 대개의 경우는 파일 경로명을 사용하겠지요. LoadFromResource()는 리소스의 이름 데이터나 리소스 ID를 지정할 수 있습니다.

이렇게 불러들인 이미지는 기존의 CBitmap 클래스처럼 사용하시면 됩니다. 이미지를 출력하기 원하는 DC에 뿌려주기만 하면 됩니다. 비트맵 핸들을 얻어야할 필요가 있다면 연산자 오버로딩이 되어져 있기 때문에 그냥 HBITMAP에 대입해서 쓰면 됩니다.

또한, 알파 블렌딩과 같은 고급 그래픽 효과도 사용할 수 있으니 참고하시기 바랍니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

Visual C++ .NET, 즉 MFC 7.0에서는 이전 버전인 MFC 6.2보다 편리하게 변경된 사항이 몇 가지가 더 있습니다. 그 중에 하나가 CObject 클래스에서의 직접 상속입니다. 물론 이전에도 아래와 같은 절차를 통하여 상속받는 것을 지원하였지만 .NET 버전에 들어와서부터는 좀 더 처리가 간결해졌다는 뜻입니다.

MFC 클래스를 작성할 때에 클래스 작성 마법사는 상속 가능한 클래스 리스트를 보여줍니다. 이 때에 CObject를 포함한 다수의 새로운 클래스를 제공합니다.

여러분은 여기서 CObject로 상속을 받아서 몇 가지의 작업을 더해주면 CObject와 동일한 기능을 수행하는 또 하나의 여러분만의 새로운 데이터 클래스를 생성할 수 있습니다.

이전에도 작성했었던 방법이지만 MFC 7.0에서도 그대로 사용할 수 있습니다. 아니, 약간 더 구체적으로 정보를 더 알려드리도록 하겠습니다. 클래스의 사용 용도에 따라서 여러분은 선언의 수준을 달리할 수 있습니다.

* DECLARE_DYNAMIC(class_name) 및 IMPLEMENT_DYNAMIC(class_name, base_class_name) : CRuntimeClass* 구조체로 조사가 가능한 일반적인 형태의 런타임 클래스로 만드는데에 필요한 약간의 데이터를 더 포함합니다. #DEFINE 선언이기 때문에, 프리프로세서가 컴파일 직전에 실제 코드로 변환하여 처리하도록 되어있습니다.

DECLARE_DYNAMIC() 선언은 클래스의 선언부에 직접 사용하시면 됩니다. 단 주의하실 점은 DECLARE_DYNAMIC() 선언 그 자체가 액세스 권한 적용 대상이 아니라는 점입니다. 즉, 아래와 같은 선언을 할 경우 컴파일 에러가 발생할 수 있습니다.

    class CExample : public CObject

    {

    public:

      DECLARE_DYNAMIC(CExample)

      CExample();

      virtual ~CExample();

    };

위와 같은 코딩을 올바르게 고치는 방법은, DECLARE_DYNAMIC() 선언을 public: 영역 밖에서 코딩해주는 것입니다.

그리고, DECLARE_DYNAMIC() 선언으로 끝나면 안되며, DECLARE_DYNAMIC()과 연관되는 멤버 함수의 선언을 위하여 IMPLEMENT_DYNAMIC()을 소스 코드 파일 (.cpp)의 프리프로세서 선언문 바로 아래에 넣으셔야 합니다.

* DECLARE_DYNCREATE(class_name) 및 IMPLEMENT_DYNCREATE(class_name, base_class_name) : 이 매크로는 앞서 거론한 DECLARE_DYNAMIC()과 IMPLEMENT_DYNAMIC()의 내용을 포함함과 동시에, CDocTemplate와 같은 클래스에서의 동적 생성 프로세스를 지원하도록 해줍니다.

기본적인 사용 방법은 위와 같습니다. 하지만, 위의 선언을 이미 하였을 경우 그 선언 대신 사용해야 합니다. 그렇지 않으면 컴파일 시 중복 선언 에러를 발생하게 됩니다. 또한 최소한 1개 이상의 기본 생성자가 필요합니다.

* DECLARE_SERIAL(class_name) 및 IMPLEMENT_SERIAL(class_name, base_class_name, wSchema) : 이 매크로는 앞서 거론한 DECLARE_DYNCREATE() 및 IMPLEMENT_DYNCREATE()의 내용을 포함함과 동시에, 직렬화 프로세싱을 지원하도록 해줍니다. 이 선언이 이루어져야 올바르게 Serialize(CArchive& ar) 함수를 재정의할 수 있습니다. wSchema에는 여러분이 임의의 버전을 입력하도록 되어있습니다. 대개 0에서부터 시작하며 프로그램의 메이저 버전을 올릴 때에 같이 올리곤 합니다.

역시 사용법은 같습니다. 중복 선언에 유의할 것과, 최소 1개 이상의 기본 생성자가 요구됨을 주의하면 됩니다.

다음 Chapter에서는 이렇게 선언한 매크로를 어떻게 사용할 수 있는지를 알아보겠습니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

여러분은 Managed Extensions for C++ 프로젝트로 콘솔 프로그램을 작성하기 시작하였을 때 전에는 볼 수 없었던 새로운 키워드와 새로운 객체를 보실 수 있습니다.

우선 오늘은 새로운 키워드에 대해서 알아보도록 할 까 합니다.

1. #using 전처리기 구문

#using은 .NET 프레임 워크의 컴파일러만이 유일하게 감지해내는 전처리기 구문입니다. 이것은 #include와 유사한 성격을 띄지만 좀 더 유연하고 포괄적인 내용입니다.

#using은 *.h 파일과 같은 순수 소스 코드를 불러들이기 위한 것이 아니라 미리 컴파일된 닷넷 프레임워크 DLL을 직접 불러다 쓰거나, 아니면 기존에 누군가가, 혹은 자신이 생성했었던 프레임워크 호환 DLL을 불러다 쓸 때에 사용하는 구문입니다.

이 DLL은 닷넷 개발 툴에서만 사용이 가능하며, 다른 델파이와 같은 개발툴에서는 호환되지 않는 형식의 이진수 체계를 사용합니다. /clr 컴파일러 옵션을 더하고 나서 여러분은 코드의 맨 위나 stdafx.h에 이와 같은 선언을 더해줄 수 있습니다.

#using <mscorlib.dll>

이는 가장 핵심적인 닷넷의 기능을 축약해 담은 내용이 컴파일된 DLL을 로드하겠다는 의미입니다. 실제로 mscorlib.dll 파일은 존재하는 파일입니다.

2. 네임스페이스 객체와 namespace 키워드

네임스페이스라는 객체는 닷넷의 유일무이한 트레이드마크입니다. 네임스페이스 그 자체는 별 의미가 없으며 단지 클래스나 또 다른 네임 스페이스를 포함하기 위한 기능을 가질 뿐입니다.

이 네임스페이스는 파일 시스템의 디렉터리 구조와도 같아서, 클래스 이름이 동일하더라도 네임스페이스의 위치가 다르면 완전히 다른 클래스로 분리가 됩니다. 이에 따라서 클래스에서도 전역 클래스와 로컬 클래스로 다시 구분되어졌습니다.

이 네임스페이스 객체를 생성하기 위해서 여러분은 단지 namespcae 이름 {으로 선언을 시작해서 }; 로 선언을 끝내기만 하면 됩니다.

3. 네임스페이스 객체에 접근하는 방법은?

네임스페이스를 별도로 객체로 선언하여 접근한다는 생각을 하셨다면 착각하셨습니다. 올바르게 액세스하기 위해서 반드시 :: (스코프 연산자)를 사용해야 합니다. 별도의 선언 없이는 네임스페이스 내부의 클래스도 :: (스코프 연산자)로 접근해야 합니다.

4. __gc 키워드

__gc가 의미하는 것은 Garbage Collected를 뜻합니다. 하지만 길게 생각할 필요는 없으며 __gc로 선언되는 것들은 모두 닷넷 프레임워크 수중안에서 관리가 이루어지므로 사용자가 수동으로 delete 키워드를 사용해서 제거할 필요는 없습니다. 이 키워드를 사용할 수 있는 대상의 제한은 없습니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)