본문 바로가기
Programming/Tips(C++,C#)

Chapter 02. COM 컴포넌트 사용

by 곰네Zip 2014. 10. 29.

*이 내용은 개인 학습 내용을 정리하는 것이 목적입니다.

 

2.1 COM컴포넌트 등록

   - 컴포넌트 사용을 위해서는 컴포넌트가 클라이언트가 실행되는 PC 레지스트리에 등록되어야 함

   - regsvr32를 이용해서 등록하자

 

2.2 COM클라이언트 어플리케이션 생성과정

   1) COM라이브러리 초기화

   2) COM객체의 CLSID구하기

   3) COM객체의 인스턴스 생성하기

   4) COM객체가 지원하는 인터페이스 포인터를 구해, 해당 메소드를 호출하다,

   5) COM객체의 사용이 모두 끝나면 COM라이브러리 초기화 해제

 

2.3 COM라이브러리 초기화

  2.3.1 CoInitialize / CoInitializeEx 함수

     - 클라이언트에서 COM사용을 위해 COM라이브러리를 초기화 해야 한다. 이 라이브러리는 ORB이며 컴

      포넌트 관리 서비스를 제공함.

     - COM라이브러리는 COM을 사용하는 애플리케이션에 대해 기본적인 API를 제공한다.

        -> Co로 시작하는 함수들이 많음

     - CoInitialize() : 초기화 함수. DCOM이전의 COM을 사용할 때 쓰임

       CoInitializeEx() : 초기화 함수. DCOM을 사용할 수 있는환경이어야 함.

 

  2.3.2 HRESULT

     - 함수를 호출한 결과는 항상 HRESULT로 받는다. 자주 사용되는 HRESULT값은 아래 링크 참조

        링크 : http://gomnezip.tistory.com/326

 

2.4 COM객체의 CLSID구하기

  2.4.1 OLE/COM개체 뷰어

     - COM객체의 CLSID가져오는 방법

      1) CLSID정의 코드파일을 사용한다

        -> 해당 파일을 구할 수 없는경우는?

      2) VS .NET개발환경에서 제공하는 OLE/COM개체 뷰어를 이용한다.

 

  2.4.2 프로그램ID (ProgID)

     - 일반적인 ProgID는 아래와 같이 정의됨 

 <컴포넌트 or 라이브러리명>.<객체명>.<버전>

       이 이름을 알 수 있으면, 해당 이름을 가지고 HKEY_CLASSES_ROOT에서 해당 키를 찾으면 된다.

      CLSID는 서브키로 포함되어 있다.

 

  2.4.3 CLSIDFromProgID() 함수

    - 프로토타입 

 HRESULT CLSIDFromProgID(

      LPOLESTR lpszProgID,     //ProgID문자열 값

      LPCLSID pclsid                // 반환된 CLSID값을 저장할 CLSID포인터

);

    - ProgID에서 CLSID를 찾아와 준다.

 

2.5 COM에서의 문자열 사용

   - 각국 언어 지원을 위해 COM은 unicode를 사용한다.

     예로, 2.4.3에서 LPOLESTR은 다음과 같이 정의되어 있다. 

 typedef wchar_t WCHAR;

 typedef WCHAR OLECHAR;

 typedef OLECHAR *LPOLESTR;

 typedef const OLECHAR *LPCOLESTR;

     LPOLESTR은 wchar_t* 타입이다.

   - ANSI <-> Unicode간의 변환

    1) MultiByteToWideChar() : Win32 API함수, ANSI -> Unicode변환

    2) WideCharToMultiByte() : Win32 API함수, Unicode -> ANSI변환

    3) wcstombs() : C런타임라이브러리함수, Unicode -> ANSI변환

    4) 문자열 상수를 unicode변환시 

        i) 상수 앞에 'L'을 붙임

        ii) OLSTR() 매크로를 사용하여 변환

   - TCHAR : _UNICODE의 define여부에 따라 TCHAR는 char로 읽을지, wchar로 읽을지 결정됨.

 

2.6 COM객체 인스턴스 생성

  * CoCreateIntance()에 대한 부가적인 설명은 http://gomnezip.tistory.com/328 글에 있음

  2.6.1 CoCreateInstance 함수

    - 함수 원형 

 STDAPI CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext,

                                       REFIID riid, LPVOID* ppv);

 

  2.6.2 COM객체 인스턴스 생성 과정

    - CoCreateInstance()내부에서는 CoGetClassObject()를 호출한다.

    - CoGetClassObject()의 동작

     1) clsid로 넘겨받은 CLSID를 레지스트리에서 검색하여 해당되는 COM객체의 정보를 찾음.

     2) dwClsContext인수의 정보를 참조해서 COM객체의 경로명을 찾는다.

       > CLSCTX_INPROC_SERVER : in-process server이므로, CLSID서브키 밑의 InprocServer32서브키

         에서 COM컴포넌트의 경로명을 구함

       > CLSCTX_LOCAL_SERVER : out-of-process server이므로, 서브키 중 LocalServer32 서브키에서

         COM컴포넌트 경로명을 구한다.

       > CLSCTX_ALL : CoCreateInstance()에서 위치를 지정해 주어야 한다면, 위치투명성을 위반한다. 따

        라서 위 키워드를 이용하여, 어느 종류의 컴포넌트인지 알지 못해도 사용할 수 있게 만들기 위함

     3) In-process Server인 경우 CoLoadLibrary()를 호출

       > 인자로 경로명이 넘어가야 하는데, 이 경로는 2)에서 얻어온 경로를 사용

       >Out-of-process Server는 추후에 다시 다룸

     4) GetProcAddress()를 호출하여 3)에서 로드한 DLL에서 DllGetClassObject()의 포인터를 구한 후, 컴

       포넌트에 rclsid, riid, ppv를 인자로 넘김

     5) DllGetClassObject()는 4)과정에서 얻어온 클래스팩토리의 인터페이스 포인터를 CoGetClassObject

       ()에 반환하고 CoGetClassObject()는 이를 CoCreateInstance()로 넘겨준다

     6) CoCreateInstance()는 받아온 IClassFactory CreateInstance()를 호출하여 COM객체 생성

 

2.7 IUnknown인터페이스

  2.7.1 IUnknown인터페이스가 필요한 이유

    - 코드 예

 //IHello.h

 class IHello{

 public:

    virtual char* sayHello(char* name) = 0;

 };

 extern IHello* CreateInstance();

 //A.h

 class CHello : public IHello{

 public:

    char* sayHello(char* name){ ... }

  };

 //A.cpp

 IHello* CreateInstance(){ return (IHello*)new CHello ; }

 //main.cpp

 int main(){

    IHello* pA = CreateInstance();

    ...

 }

      > 위와 같이 클래스를 직접 사용하지 않고 인터페이스를 사용하여 접근하게 한다면, 클라이언트에서는

      A가 어떻게 구성되었는지 알 수 없다. 클라이언트와 상관없이 A는 자유로운 수정이 가능

 

   - 만약 클래스 A가 두개의 인터페이스에서 상속받는 경우. 

    > 하나의 인터페이스를 CreateInstance로 얻어올 수 있으나, 다른 형태의 인터페이스로 캐스팅을 사용할 수 없다. C++에서 클래스 타입간의 변환은 클래스가 상속 관계에 있을 때, 클래스가 변환생성자 또는 변환함수를 제공할 때만 가능하다. (dynamic_cast가 가능해야 한다.)

  자세한 소스 예제

    > IGoodBye*를 반환하는 전역함수를 제공하여 해결 가능하다. 

 IGoodBye* QueryInterface(IHello* p){

    CHello* pA = (CHello*)p;

    return dynamic_cast<IGoodBye*>(pA);

 }

 //단 위 코드에서 CHello는 IHello의  child이다. 부모->자식으로 캐스팅 할 때에는  static_cast보다는  dynamic_cast를 사용하는 것이 좋다. (위험한 downcasting에서는 런타임시 타입체크를 해주므로.. 물론 그런 코드도 필요하겠지만.) 그래서 변경하였는데 매우 잘 실행됨

      이제 위 함수를 이용하여 IGoodBye*를 구하여 사용할 수 있다.

 

   - 메모리 관리 문제.

     > 위의 코드에서 CreateInstance()를 호출할 수 만큼 객체를 delete해주지 않으면 메모리 누수가,

       delete위치가 잘못되면 액세스 바이올레이션이 발생하기 충분하다. CreateInstance를 바꿔보자

 IHello* pA = 0;

 IHello* CreateInstance(){

     if( pA == NULL){ pA = (IHello*)new CHello; }

     return pA;

 }

     이렇게 CreateInstance()가 구현되면 언제든, 클래스 A의 인스턴스는 하나만 존재한다. (이것을 싱글톤이라고 한다.)

      *싱글톤

        - 싱글톤 패턴 목적의 두가지

           i) 클래스의 인스턴스를 하나만 생성한다. 

           ii) 어디서든 해당 인스턴스에 접근 가능하다.

       - 싱글톤이 필요한 이유

           i) 하나만 존재해야 좋은 것들이 있다. (ex. 스레드풀, 설정, 캐시 등등)

           ii) 늦은 초기화를 가능하게 한다. (사용하지 않을 때, 객체 생성에 소비되는 리소스를 아낄 수 있다.)

        (참조 : http://www.javajigi.net/display/SWD/ch05_singletonpattern)

 

     > 그러나 아직 액세스 바이올레이션 해결은 못하였다. A클래스의 인스턴스가 스스로를 참조하지 않을

      때, 스스로 소멸하는 기능을 제공해야 한다. 다음 코드를 예로 보자.


 class IUnknown {

 public:

    virtual bool QueryInterface(IID iid, void** ppv) = 0;

    virtual int AddRef() = 0;

    virtual int Release() = 0;

 }

 class CHello: public IHello, public IGoodBye{

 public:

   bool QueryInterface(IID iid, void** ppv);

   int AddRef();

   int Release();

 private:

    m_ref; //참조 카운터

 }

 

 int CHello::AddRef(){

    return ++m_ref;

 }

 int CMyClass::Release(){

    if( --m_ref == 0){

       delete this;

    }

    return m_ref;

 }

 bool CHello::QueryInterface(IID iid, void** ppv){

   *ppv = 0;

   if( iid == IID_IUnknown){ *ppv = (IUnknown*)(IHello*)this; }

   else if( iid == IID_IHello){ *ppv = (IHello*)this; }

   else if( iid == IID_IGoodBye){ *ppv = (IGoodBye*)this; }

 

   if( *ppv != NULL){

     ((CHello*)*ppv)->AddRef();

     return true;

   }

   return false;

 }

     - 위 코드에서 CHello가 제공해야 할 기능을 IUnknown인터페이스에 정의했다. QueryInterface()를

      이용하여 다른 인터페이스 포인터를 제공하고, AddRef()/Release()를 이용하여 자신이 참조되고 있는

      지를 관리한다.

     - 클라이언트가 직접 사용하는것은 클래스가 아니라 IHello, IGoodBye다. 클래스를 직접 사용하지 않기에

      해당 내용을 클래스에 직접 정의하면 다른 전역함수가 필요하다. 그래서 IHello, IGoodBye 인터페이스에

      IUnknown의 기능을 정의할 필요가 있고, 이 기능은 공통적으로 필요하므로 IUnknwon에서 파생되도록

      만들었다.

   혹시나  void**라 하여 다음과 같이 사용하면 안된다.

 IHello** pIHello = NULL;

 pUnk->QueryInterface( IID_IHello, (void**)pIHello);

   위와 같이 포인터를 사용하면 QueryInterface에서 런타임 에러가 발생한다. pIHello의 위치는 NULL, 즉, 0x00000000이다. 이 위치에 값을 기록하려고 하면, 당연히 AV발생. 이것은 잘못된 포인터 사용이다.

  *함수의 인자로 이중 포인터를 넘겨주는 경우는, 포인터가 가리키는 위치에 값을 기록하고 그 결과를 받아올 때, 사용한다. 변수에 값을 얻어오고자 할 때, 포인터로 넘겨준다. 이중포인터는 포인터 위치에 값을 얻어오기 위해 포인터의 포인터를 호출하여 주는 것이다.

 

  2.7.2 QueryInterface메소드

    - COM 객체는 여러 인터페이스를 제공할 수 있다. 그러나 한 인터페이스로는 해당 인터페이스에 속한

     멤버만 접근 가능하다. 다른 인터페이스의 포인터를 제공해 주어야 함. 

 HRESULT __stdcall QueryInterface(REFIID riid, void** ppv);

     > ppv는 **이므로 원하는 인터페이스 포인터에 &붙여서 해당 포인터의 주소를 넘겨줄것.

 

  2.7.3 AddRef, Release메소드

    - 클라이언트가 인터페이스 포인터를 변수에 저장한다 -> 객체의 인스턴스를 참조하고 있다.

    - 객체가 참조중인데 다른 클라이언트에서 객체를 삭제하면 문제 발생 -> 객체 스스로 참조되는 상황을

     관리해서 스스로 소멸되는 시점을 결정해야 함.

    - QueryInterface로 객체를 가져올 경우 해당 함수 내에서 AddRef()가 호출되지만, 클라이언트에서 객체

     를 복사할당 할 경우 왼쪽의 인터페이스 포인터에 대해 AddRef()가 호출되야 한다.

       > AddRef, Release함수가 있지만 참조 카운트를 관리하는 주체는 아직 클라이언트

 

  2.7.4 스마트포인터 클래스

    - 위와 같은 인터페이스 사용은 조금 번거로움. 스마트 포인터를 사용 해결

      예) 스마트포인터 템플릿

 template<class I>

 class CSmartPtr{

 public:

    CSmartPtr(I* pI = NULL) : m_pI(pI){ if( m_pI != NULL){ m_pI->AddRef(); } }

    CSmartPtr(const CSmartPtr<I>& rI): m_pI(rI.m_pI){ if( m_pI != NULL){ m_pI->AddRef(); } }

    ~CSmartPtr(){ if( m_pI != NULL){ m_pI->Release(); } }

    CSmartPtr<I>& operator=(I* pI){

        if( m_pI != pI){

            if( m_pI != NULL){ m_pI->Release(); }

            m_pI = pI;

            if( m_pI != NULL){ m_pI->AddRef(); }

       }

       return *this;

    }

    operator I*(){ return m_pI; }

    I* operator->(){ return m_pI; }

    I** operator&(){ return &m_pI; }

    BOOL operator==(I* pI) const { return (m_pI == pI); }

    BOOL operator!=(I* pI) const{ return (m_pI != pI); }

    BOOL operator!() const{ return !m_pI; }

   

 protected:

    I* m_pI;

 };

     사용은 아래와 같이 할 수 있다. 

 void Foo(IUnknown* pUnk){

     CSmartPtr<IMyClass> pIMyClass;

     HRESULT hr = pUnk->QueryInterface(IID_IMyClass, (void**)&pIMyClass);

     CSmartPtr<IMyClass> pIMyClass2;

     pIMyClass2 = pIMyClass1;

      > CSmartPtr<IMyClass> pIMyClass

        위와 같이 스마트 포인터클래스를 생성할 때, CSmartPtr<I>(I* pI=NULL)이 호출됨.

        이 과정에서 AddRef()가 호출되어 참조를 하나 증가해 줌

      > pIMyClass2 = pIMyClass

        이 과정에서는 복사생성자가 호출되어 역시 참조 카운트를 하나 증가해 줌.

      > '->'연산자를 사용하여 클래스 인스턴스가 관리하는 인터페이스 포인터에 접근가능하다

      > '&'연산자를 사용하여 클래스 인스턴스가 관리하는 인터페이스 포인터의 주소를 구할 수 있다.

      > '==','!='를 이용하여 비교, '!'연산자를 이용하여 NULL여부를 확인할 수 있다.

 

    - 위 소스에서 스마트 포인터가 QueryInterface까지 지원할 수 있게 한다면? 

 IUnknown* pUnk;

 CSmartPtr<IMyClass> pIMyClass = pUnk;

      위 구문을 지원하게 만들어 처리할 수 있는 코드 

 template<class I, const IID* piid>

 class CSmartQIPtr{

 public:

   CSmartQIPtr(I* pI = NULL) : m_pI(pI){ if( m_pI != NULL){ m_pI->AddRef(); } }

   CSmartQIPtr(IUnknown* pUnk){ pUnk->QueryInterface(*piid, (void**)&m_pI); }

   //복사생성자, 대입연산자는 위와 동일

   CSmartQIPtr<I,piid>& operator=(IUnknown pUnk){

       if( m_pI != pUnk){

           if( m_pI != NULL){ m_pI->Release(); }

           m_pI = pI;

           if( m_pI != NULL){ m_pI->AddRef(); }

       }

       return *this;

   }

  //변환연산자, 멤버는 위와 동일

 }

        위와 같이 작성하면 스마트포인터를 가지고 IMyClasss의 인터페이스를 가져올 수 있다. 

 IUnknown* pUnk;

 CSmartQIPtr<IMyClass, &IID_IMyClass> pIMyClass = pUnk;

 

  2.7.5 인터페이스 사용시 유의 사항

    - AddRef(), Release()를 불필요하게 호출하면 어플리케이션 성능에 영향을 준다.

     예) 

 IMyClass *pIMyClass = NULL;

 ...

 IMyClass *pIMyClass2 = NULL;

 pIMyClass2 = pIMyClass;

 pIMyClass2->AddRef();

 pIMyClass2->Func();

 pIMyClass2->Release();

 pIMyClass->Release(); 

       > 위와 같이 pIMyClass2의 사용범위는 pIMyClass의 사용범위 내에 있다. 이럴경우 굳이 참조카운트

        를 변경해야 할 필요는 없다. 하지만, 사용범위가 벗어날 경우에는 명확하게 참조 카운트를 관리할것

 

2.8 COM객체서비스 사용

  - 직접 QueryInterface(), Release()호출하는 것 보다는 CoCreateInstance()가 더 효율적이다.

  - COM에서는 항상 Unicode를 사용한다.

  - Out-of-process Server 또는 리모트 서버인 경우 서로 다른 프로세스에서 실행된다. 이 경우 한 프로세

   스에서 할당한 메모리를 다른프로세스에서 해제하여야 하는 경우가 있다. 이 경우 COM라이브러리가 제

   공하는 CoTaskMemAlloc()을 이용하여 할당, CoTaskMemFree()를 이용하여 해제한다.

 

2.9 COM라이브러리 초기화 해제

  - COM을 초기화 할때 : CoInitializeEx()를 사용하여 초기화 했다.

  - COM라이브러리 해제시 : CoUninitialize()

 

출처 - CBD, Component Development with Visual C++,ATL

 

반응형

댓글