프로그래밍/COM

COM 객체와 인터페이스의 구축

Subi Lee 2010. 8. 24.
반응형
본 글은 COM/DCOM 프라이머 플러스를 참고하여 작성되었습니다.

  클라이언트와 서버

  - COM의 중개 모델
  1. 기본적인 COM 클라이언트와 서버 모델  (인프로세스 서버)
    • 컴 퍼넌트 자신이 서버가 되며 사용하는 프로그램과 동일한 프로세스에 존재
    • DLL로 만들어짐
    • 클 라이언트가 필요로 할 때 COM 서브시스템에 의해 로딩됨
    • 필요로 하지 않을 때 언로딩
    • 하나의 DLL에 몇 개의 객체라도 들어갈 수 있음
  2. 다른 프로세스에 들어있는 COM 클라이언트와 서버 (로컬 서버)
    • 클 라이언트와 서버가 각각 다른 프로세스로 존재
    • DLL이 아닌 별도 실행 파일 필요
  3. 다른 컴퓨터에서 실행되고 있는 COM 클라이언트와 서버  (DCOM, 리모트 서버)
    • 각기 다른 컴퓨터에 존재하며 네트워크를 통해 서로 대화

 COM 서브시스템이란?
  윈도우 운영체제는 메인커널(NT의 경우 마이크로커널)이 그래픽, 윈도우 관리 등의 하부 기능을 호출하여 동작하는데, 그 중 COM 기능을 가진 라이브러리를 구현한 부분이 COM 서브시스템이다. 
  이 부분은 OLE32.DLL이란 파일에 들어 있는데, OLE32는 COM 관련 함수 이외에도 OLE(COM은 OLE의 일부이다) 기능도 가지고 있다. Inside COM을 보면 좀 더 많은 정보를 얻을 수 있다.

 프락시(Proxy)
   1. 컴파일러에 의해 컴파일된 C++ 프로그램에서 객체의 인스턴스에 대한 메소드 호출 시
      1) 컴파일러는 해당 메소드가 위치한 코드 주소로의 프로세스 공간 내의 점핑 코드 생성
      2) DLL의 객체에의 접근
         * 운영 체제의 프로그램 로더가 어플리케이션 로드 시에
           어플리케이션의 메소드 호출 지점과 DLL 내 메소드의 주소를 바인딩
          * 해당 방법은 인프로세스 COM서버에 대해 동일하게 적용됨
           (1) COM 서브시스템이 서버 DLL을 해당 프로세스의 주소 공간에 올림
           (2) 메소드 호출 지정과 코드 주소 바인딩 수행
   2. 로컬 서버와 리모트 서버는 다른 주소 공간에 존재하기 때문에 Proxy를 이용함
      * 클라이언트가 COM 서브시스템에 인터페이스 요청시
         1) 인프로세스 서버
            * COM서브시스템이 요구받은 인터페이스를 지원하는 객체를 클라이언트에 전달
         2) COM 서버가 로컬 타입일 때
            * 클라이언트에 특수한 종류의 객체 전달
            * 이것이 바로 프락시

   * 프락시는 클라이언트와 동일한 주소 공간에 위치하는 객체
     1. 컴포넌트에 의해 지원되는 모든 인터페이스에 대응하여 메소드 호출
     2. 클라이언트 입장에서는 프락시가 컴포넌트 역할
     * 프락시가 하는 일
        1) 클라이언트가 로컬 서버의 메소드 호출을 수행할 때 필요 매개변수를 32비트 자료구조로 변환(마샬링)하여 로컬 서버로 전송
        2) 이 때 사용하는 메소드 호출 방식이 RPC(Remote Procedure Call)

 RPC(Remote Procedure Call)
    * Open Software Foundation 의 DCE RPC 스펙에서 정의된 프로세스간 통신(IPC) 매커니즘
    * named pipe, NetBIOS, Winsock을 사용하여 멀리 떨어져 있는 시스템 사이의 통신을 수행
    * 다른 주소 공간에서 실행되는 어플리케이션 간의 통신
    * 프락시는 RPC를 이용하여 메소드의 매개변수만 로컬 서버의 주소공간으로 보냄
    * DCOM은 RPC를 로컬은 LPC(Local Procedure Call)을 이용함

서버 쪽에서의 RPC






    1. 클라의 메소드 호출은 RPC 스텁(stub)라는 코드 조각이 대신 접수
    2. stub은 프락시가 보낸 매개변수 자료구조를 원래의 매개변수들로 쪼깨어(언마샬링) 컴포넌트가 제공하는 해당 메소드 호출
   3. 메소드 호출이 끝난 컴포넌트는 메소드 반환값을 stub에 전달
   4. stub은 반환값과 매개변수 정보를 32비트 자료구조로 재구성 후 RPC를 통해 프락시에 전달
   5. 프락시는 RPC로 받은 자료구조를 다시 클라이언트로 전달
  
  장점
    * 클라이언트는 COM 컴포넌트와 서버의 위치에 대해 전혀 신경을 쓸 필요가 없음 

마샬링(Marshaling)
  인터페이스 포인터 사용 시 다른 프로세스의 주소 공간에 현재 프로세스의 매개변수를 복사해 넣어야 하는데 이 처리 과정을 마샬링이라 함
  1. 클라이언트에서 서버로의 함수 호출 전과 함수 호출 후에 끼어듬
  2. 매개변수시 사용하는 32비트 자료구조를 다른 프로세스의 주소 공간으로 복사하여 정보를 참조할 수 있게 해 줌.

서버가 갖추어야 할 기능
  1. 레지스트리
     1) 시스템 운영을 위해 계속 보존되는 데이터가 계층적으로 보관된 구조
     2) 클라이언트와 서버를 연결해주기 위해 COM이 사용하는 매커니즘
         * 레지스트리를 이용하여 시스템에 설치된 컴포넌트에 대한 정보를 추적
         * 저장되는 정보
            - 서버 DLL | EXE 파일의 위치 정보
            - 어플리케이션이 실행되기 위해 COM이 요구하는 세부 정보
     3) Server Registration
         * 서버의 정보를 레지스트리에 넣는 과정을 말함
     4) Server Unregistration
         * 레지스트리에서 서버의 모든 정보를 제거하는 과정
     5) 서버 등록 방법
         * 손이나 스크립트로 직접 넣기
         * Win32 API 호출
           - 로컬 서버는 /regserver /unregserver
           - DLL 서버는 Regsvr32.exe 사용
             - 두 방법 모두 다 DllRegisterServer, DLLUnregisterServer

클래스 팩토리
  2장 에서 나온 내용
     - 클래스 팩토리는 컴포넌트를 생성하고 소멸시킬 때 COM에 의해 사용되는 객체
     - 모든 서버는 각 컴포넌트에 대한 클래스 팩토리를 제공할 수 있어야 한다.
  * 정리: 모든 서버는 최소한 2개의 COM 컴포넌트를 지원해야 한다.
     1. 진짜 서비스를 제공하는 컴포넌트
     2. 그 컴포넌트의 클래스 팩토리

  클라가 인프로세스 서버에서 한 컴포넌트 요구시
    1. COM 서브시스템은 요청한 컴포넌트에 해당하는 인프로세스 서버의 DLL을 메모리에 올림
     2. DllGetClassObject()를 호출
       * 모든 서버가 필수적으로 구현해야 하는 함수
       * 해당 함수의 매개변수는 필요한 컴포넌트의 클래스 팩토리 이름임
    3. 서버는 클래스 팩토리 객체를 생성 후 COM 서브시스템으로 반환
    
  로컬 서버에서 컴포넌트 요구시
    1. COM 서브시스템은 EXE파일을 메모리에 강제로 올리고 함수를 호출하지 못함
    2. 로컬 서버 자신이 모든 클래스 팩토리를 COM 서브시스템에 등록해야 함
       * 이 때 사용하는 함수가 CoRegisterClassObject() 
    3. COM 서브시스템은 요청한 클래스 팩토리가 등록될 때까지 기다린 다음 그 클래스 팩토리를 사용하여 컴포넌트 생성

마무리 동작  -릴리즈
   1. 인프로세스 서버
       * COM의 요청에 따라 서버를 없앰
          - 주기적으로 COM 서브시스템은 DLL에 수동으로 구현해야 하는 DllCanUnloadNow()를 호출한다.
          - 해당 함수는 boolean으로 true 혹은 false를 반환함
   2. 로컬 서버
       * COM 서브시스템이 아니라 로컬 서버 자체적으로 릴리즈 권한 부여
       * 로컬 서버가 종료되기 직전 서버의 클래스 팩토리도 메모리에서 해제되어야 한다.
          - COM 서브시스템이 서버의 클래스 팩토리 포인터가 유효하다고 간주하는 상태에서 서버가 종료되면 오동작.
          - CoRegisterClassObject()를 통해 COM으로 전달된 클래스 팩토리는 CoRevokeClassObject()를 이용하여 해제함
       * 서버가 해제되는 조건은 내부에서 유지하는 객체 카운터와 잠금 카운터가 0이 될 때
          - 아직 어렴풋이 개념을 잡는 단계이므로 자세히는 안함
          - 1장과 2장에서 나온 레퍼런스 카운터와 관련이 있음

전역 유일 식별자(Globally Unique IDentifiers)
 {3F2504E0-4F89-11D3-9A0C0305E82C3301}
  * 모든 컴포넌트와 인터페이스 간에 이름 충돌을 막고 유일한 이름만을 제공하는 것
  * 현재의 날짜와 시간, 컴퓨터에 물려 있는 네트워크 카드의 주소 이용
  * 128비트 (16바이트) 의 상수 - 최대값(2^128)
  * 생성방법
    - UUIDGEN 을 이용
    - GUIDGEN 을 이용 - 해당 프로그램의 전체 소스는 MSDN에 있다.

  1. 소스 코드에 적용하기
    1) WTYPES.H에 프로그래밍에 사용되는 GUID에 대한 구조체가 존재함
    2) GUID를 선언하고 초기화하는 부분은 반드시 GUID구조를 알아야 작성 가능

  
  1. typedef struct _GUID  
  2.       {  
  3.           DWORD Data1;  
  4.           WORD Data2;  
  5.           WORD Data3;  
  6.           BYTE Data4[8];  
  7.       }  
    *  4개의 필드로 구성된 총 16바이트 구조체에 대응

CLSID, IID
  * 클래스와 인터페이스의 이름을 짓는 명명자
  * typedef를 사용하여 표준 GUID로 정의한 타입

  1. CLSID CLSID_yslee;  
  2. IID IID_yksong;  
  3.   
  4. static const CLSID CLSID_yslee =  
  5.   {0x86ecd437, 0x1fd9, 0x11d0,  {0x8b, 0x7c, 0xe4, 0x45, 0xc9, 0xbd, 0x31, 0xc} };  
  6. static const IID IID_yksong =  
  7.   {0x86ecd437, 0x1fd9, 0x11d0,  {0x8b, 0x7c, 0xe4, 0x45, 0xc9, 0xbd, 0x31, 0xc} };  


DEFINE_GUID 매크로
  - 중복 선언을 방지하기 위한 GUID 선언 매크로
  - INITGUID 전처리 매크로가 존재할 시에만 선언 & 정의 작업을 시행함

  1. #ifndef INITGUID  
  2. #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \  
  3.    EXTERN_C const GUID FAR name  
  4. #else  
  5.   
  6. #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \  
  7.        EXTERN_C const GUID name \  
  8.                = { l, w1, w2, { b1, b2,  b3,  b4,  b5,  b6,  b7,  b8 } }  
  9. #endif // INITGUID  

HRESULT 반환 타입
  - unsigned LONG
  - 동작의 성공 또는 실패에 관한 정보를 융통성 있게 담기 위하여 만든 타입
  - 호환성을 위해 MS가 제공하는 매크로를 사용하여 HRESULT값을 조작 & 확인하도록 하는 것이 좋다.
    * S_OK 를 사용하여 값을 비교하는 것보다 FAILED(), SUCCEED()등의 매크로를 사용하라는 말

  CLIPBRD_E_BAD_DATA

  1. severity
    * HRESULT가 오류나 성공 상태를 나타내는지를 가리킨다.
  2. facility
    * 반환된 HRESULT 값이 어떤 그룹에 속해 있는지를 가리킨다.
  3. description
    * 어떤 곳에서 어떤 일이 발생했는지 정확하게 가르쳐주는 코드
       - 어려운 말로 환경 종속(context-specific) 성격을 가졌다고 표현

  * 자세한 것은 MSDN에서  "Error codes and Error Handling"으로 찾도록 한다.


----

Using Macros for Error Handling

COM defines a number of macros that make it easier to work with SCODEs on 16-bit platforms and HRESULTs on both platforms. Some of the macros and functions below convert return values of different data types and are quite useful in code that runs only on 16-bit platforms, code that runs on both 16-bit and 32-bit platforms, and 16-bit code that is being ported to a 32-bit platform. These same macros are meaningless in 32-bit environments and are available to provide compatibility and make porting easier. Newly written code should use the HRESULT macros and functions.

The error handling macros are listed below:

MacroDescription
GetScode (Obsolete) Returns an SCODE given an HRESULT.
ResultFromScode (Obsolete) Returns an HRESULT given an SCODE.
PropagateResult (Obsolete) Generates an HRESULT to return to a function in cases where an error is being returned from an internally called function.
MAKE_HRESULT Returns an HRESULT given an SCODE that represents an error.
MAKE_SCODE Returns an SCODE given an HRESULT.
HRESULT_CODE Extracts the error code part of the HRESULT.
HRESULT_FACILITY Extracts the facility from the HRESULT.
HRESULT_SEVERITY Extracts the severity bit from the SEVERITY.
SCODE_CODE Extracts the error code part of the SCODE.
SCODE_FACILITY Extracts the facility from the SCODE.
SCODE_SEVERITY Extracts the severity field from the SCODE.
SUCCEEDED Tests the severity of the SCODE or HRESULT - returns TRUE if the severity is zero and FALSE if it is one.
FAILED Tests the severity of the SCODE or HRESULT - returns TRUE if the severity is one and FALSE if it is zero.
IS_ERROR Provides a generic test for errors on any status value.
FACILITY_NT_BIT Defines bits so macros are guaranteed to work.
HRESULT_FROM_WIN32 Maps a Win32 error value into an HRESULT. This assumes that Win32 errors fall in the range -32K to 32K.
HRESULT_FROM_NT Maps an NT status value into an HRESULT.

Note  Calling MAKE_HRESULT for S_OK verification carries a performance penalty. You should not routinely useMAKE_HRESULT for successful results.

 -----

인터페이스 해부

* 1장과 2장에서 입이 닳도록 얘기했듯이 COM의 레이아웃은 은 C++의 레이아웃을 본따 모델링되었음
* 인터페이스 제작 시에는 "모든 멤버가 순수 가상 함수인 추상 기본 클래스로 정의한다"
* COM객체와 C++ 객체가 완전히 딱 맞아떨어지지는 않음
* 실제 선언

    1. C++ 로 구현한 인터페이스
  1. class IDog : public IUnknown  
  2.   {  
  3.     public:  
  4.       virtual HRESULT Bark() = 0;  
  5.       virtual HRESULT Bite() = 0;  
  6.       virtual HRESULT Piss() = 0;  
  7.   };  


  2. C로 구현한 인터페이스
  1. typedef struct IDog IDog;  
  2. typedef struct  
  3. {  
  4.    // IUnknown  
  5.   HRESULT (*QueryInterface)(IDog *This,  
  6.                                               REFIID riid,  
  7.                                               void **ppvObject);  
  8.   ULONG  (*AddRef)(IDog *This);  
  9.   ULONG  (*Release)(IDog *This);  
  10.    
  11.   // IDog  
  12.   HRESULT (*Bark)(void);  
  13.   HRESULT (*Bite)(void)l;  
  14.   HRESULT (*Piss)(void);  
  15. } IDogVtable;  
  16.   
  17. typedef struct  
  18. {  
  19.   struct IDogVtable *lpVtable;  
  20. } IDog;  

 * 그냥 다른 언어로는 어떻게 하는지 보라는 것이다.
   - 봐야 할 포인트
      1) COM인터페이스는 무조건 IUnknown에서 파생된다.
      2) IUnknown이 가진 메소드를 같이 지원하게 하고 vtable에 적절한 포인터가 로드되도록 하면 상속을 흉내낼 수 있다.
* 앞으로는  C++만 가지고 다룰 것이다.
* 다중 인터페이스 지원 문제는 4장에서 나온다

IUnknown 파헤치기
  1. class IUnknown  
  2. {  
  3.   public:  
  4.    virtual HRESULT QueryInterface(REFIID riid, void **ppvObject) = 0;  
  5.    virtual ULONG AddRef() = 0;  
  6.    virtual ULONG Release() = 0;  
  7. };  

  - 모든 COM 인터페이스는 IUnknown 인터페이스를 상속받아야 한다.
  - 클라이언트는 컴포넌트에 대한 포인터를 직접 취할 수가 없다.
  - 클라이언트는 인터페이스 포인터로만 COM 객체와 대화할 수 있다.
  - IUnknown은 모든 COM 객체가 반드시 노출시키도록 되어 있는 유일한 인터페이스이다.

1. QueryInterface
  * 2개의 매개변수를 받는다.
  1) 인터페이스를 식별하는 GUID의 참조값
  2) 인터페이스 포인터값을 받아야할 메모리 주소

  1. HRESULT hResult = E_FAIL;  
  2. IDog *pIDog = NULL;  
  3.   
  4. // 컴포넌트에 IDog 인터페이스가 존재하는 지 알아(Query)본다.  
  5. hResult = pIUnknown->QueryInterface(IID_IDog, ((void **) &pIDog);  
  6.   
  7. if( FAILED(hResult) )  
  8. {  
  9.    switch(hResult)  
  10.    {  
  11.        // 컴포넌트가 지원하지 않는 인터페이스  
  12.         case E_NOINTERFACE:  
  13.        // 이 스레드에서 COM 이 초기화되지 않음  
  14.        case CO_E_NOTINITIALIZED:  
  15.        // 컴포넌트가 쓸 메모리가 모자람  
  16.        case E_OUTOFMEMORY:  
  17.        default:  
  18.            break;  
  19.    }  
  20. }  
  21. return hResult;  

참조 카운팅
  * 참조 카운트
    - 해당 컴포넌트가 다른 객체 혹은 클라로부터 몇번이나 참조되고 있는지를 가리키는 계수값

  1. AddRef()
  2. Release()

   A 객체와 B 객체는 C컴포넌트를 사용하고 있다.
  A와 B는 서로를 모르는 상태에서 어떻게 C 컴포넌트가 사용되지 않는 시점을 알아낼 수가 있는가?
  물론 각자가 인스턴스를 관리한다면 부분적으로는 알 수 있다.

   COM은 C가 스스로 참조된 횟수를 카운트하는 방식으로 해당 방법을 해결하였다.
     - 사용자인 A와 B는 인터페이스 포인터의 복사본을 가졌을 때는 AddRef로 카운트값을 증가해주고
     - 인터페이스 포인터가 소멸될 때는 꼭 Release() 메소드를 호출하여 내부 참조 카운트를 감소시킨다.
  컴포넌트가 참조카운트가 0이 됐을 때는 메모리에서 자신을 해제시킨다. (delete this)

  예)
  1. // 참조 카운트 1에서 시작 - IUnknown 인터페이스 포인터 값이 pIUnknown에 들어있으므로  
  2. IDog *pIDog1 = 0;  
  3. IDog *pIDog2 = 0;  
  4. IChiwawa *pIChiwawa1 = 0;  
  5.   
  6. piUnknown->QueryInterface(IID_IDog, (void **) &piDog1);  // 이 때 참조 카운트 2  
  7.   
  8. pIDog2 = pIDog1;  
  9. pIDog2->AddRef(); // 참조 카운트 3  
  10.   
  11. pIDog1->Release() // 참조 카운트 2  
  12. piDog1 = NULL;  
  13.   
  14. piUnknown->QueryInterface(IID_Chiwawa, (void**) &pIChiwawa1); // 참조 카운트 3  
  15.   
  16. piChiwawa1->Release(); //  참조 카운트 2  
  17. piDog2->Release();  // 참조 카운트 1  
  18.   
  19. piUnknown->Release() // 참조 카운트 0    컴포넌트 자동 소멸  
1) Release가 생략되면 메모리 누수가 야기된다.
2) AddRef가 빠져 있으면 클라이언트는 이미 해제된 무효 메모리를 참조하고 있는 경우로 남게 된다.
   - 어려운 말로 dangling reference
* 항상 참조 카운트 증가 및 해제 시에 주의를 기울인다.

두 인터페이스 포인터가 같은 컴포넌트에 속해 있는지 알아보는 방법
  * 프로그래밍을 하다 보면 객체가 동일한 객체에서 나온 것인지 다른 객체에서 나온 것인지 구분해야 경우가 있다.
    1. 모든 인터페이스는 IUnknown에서 상속을 받는다.
    2. IUnknown 포인터는 클라이언트 프로그램 내에서 그 객체를 유일하게 식별하는 데 사용될 수 있다.
    3. A와 B 두 인터페이스 객체가 있다면 각각 IUnknown 인터페이스를 요구하여 그 포인터를 비교한다.
        * 포인터 값이 동일하다면 같은 IUnknown에서 나온 인터페이스 객체

  1. //... IDog interface pointer is in variable pIDog ...  
  2. //... IChihuahua interface pointer is in variable pIChihuahua ...  
  3.   
  4. HRESULT result              = E_FAIL;  
  5. IUnknown* pDogIUnknown      = NULL;  
  6. IUnknown* pChihuhuaIUnknown = NULL;  
  7.   
  8. // Get the IUnknown interface pointer for the IDog  
  9. result = pIDog->QueryInterface(IID_IUnknown, (void**) &pDogIUnknown);  
  10.   
  11. if (FAILED(result))   
  12. {  
  13.    // ... Error processing ...  
  14. }  
  15.   
  16. // Get the IUnknown interface pointer for the IChihuahua  
  17. result = pIChihuahua->QueryInterface(IID_IUnknown, (void**) &pChihuahuaIUnknown);  
  18.   
  19. if (FAILED(result))   
  20. {  
  21.    // ... Error processing ...  
  22. }  
  23.   
  24. // Do both of these interface pointers belong to the same object?  
  25. if (pDogIUnknown == pChihuahuaIUnknown)   
  26. {  
  27.    cout << "The IDog and IChihuahua interfaces belong to "  
  28.            "the same component.\n";  
  29. {  
  30. else  
  31. {  
  32.    cout << "The IDog and IChihuahua interfaces DO NOT belong to "  
  33.            "the same component.\n";  
  34. }  


  * 객체는 항상 동일한 IUnknown 인터페이스 포인터를 내주어야 한다.
  * 컴포넌트가 지원하는 인터페이스는 메모리에 있는 동안 변하지 않는 상태를 유지해야 한다.

반응형

댓글