^(코딩캣)^ = @"코딩"하는 고양이;

COM의 소개(파트 2) - COM 서버의 이면 (8)

API/COM
2020. 10. 8. 19:45

COM의 소개(파트 2) – COM 서버의 이면

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 2) – COM 서버의 이면 (1)
  2. COM의 소개(파트 2) – COM 서버의 이면 (2)
  3. COM의 소개(파트 2) – COM 서버의 이면 (3)
  4. COM의 소개(파트 2) – COM 서버의 이면 (4)
  5. COM의 소개(파트 2) – COM 서버의 이면 (5)
  6. COM의 소개(파트 2) – COM 서버의 이면 (6)
  7. COM의 소개(파트 2) – COM 서버의 이면 (7)
  8. COM의 소개(파트 2) – COM 서버의 이면 (8)
  9. COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

간단한 사용자 인터페이스

실제로 클래스팩토리의 예제를 확인하기 위하여, 본 글에 수록된 샘플 프로젝트를 살펴보는 것부터 시작하겠습니다. 이 프로젝트는 CSimpleMsgBoxImpl이라는 이름의 coclass에서 ISimpleMsgBox 인터페이스를 구현하고 있는 DLL 서버입니다.

 

인터페이스 정의

우리가 만든 새로운 인터페이스의 이름을 ISimpleMsgBox로 정하겠습니다. 모든 인터페이스들과 마찬가지로 이 인터페이스도 IUnknown에서 파생되어야 합니다. 이 인터페이스에는 단 하나의 메소드인 DoSimpleMsgBox가 있습니다. 이 메소드는 표준 반환형인 HRESULT를 반환함을 숙지하시기 바랍니다. 여러분이 작성하는 모든 메소드들은 HRESULT를 반환형으로 삼아야 합니다. 그 외 다른 형식으로 호출자에게 값을 반환하고자 할 때는 포인터 파라미터를 사용해야 합니다.

struct ISimpleMsgBox : public IUnknown {
    /* IUnknown에서 정의된 메소드들 */
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface(REFIID riid, void ** ppv);

    /* ISimpleMsgBox 메소드 */
    HRESULT DoSimpleMsgBox(HWND hWndParent, BSTR bsMessageText);
};

struct declspec(uuid(“{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}”)) ISimpleMsgBox;

 

__declspec가 적혀있는 줄은 ISimpleMsgBox 심볼(symbol)에 GUID를 부여하고 있습니다. 이렇게 하면 GUID는 나중에 __uuidof 연산자를 통해 얻을 수 있습니다. __declspec__uuidof는 Microsoft C++ 확장입니다.

DoSimpleMsgBox의 두 번째 파라미터는 BSTR 형입니다. BSTR은 “바이너리 문자열(binary string)”을 의미하며, 고정 크기 바이트들의 시퀸스를 나타냅니다(역자: 즉 한 글자당 2바이트짜리가 계속 이어져 있다는 뜻입니다). BSTR은 주로 Visual Basic이나 Windows Scripting Host와 같은 스크립팅 클라이언트에서 사용됩니다.

이 인터페이스는 CSimpleMsgBoxImpl이라는 이름의 C++ 클래스로 구현됩니다. 클래스의 선언은 다음과 같습니다.

class CSimpleMsgBoxImpl : public ISimpleMsgBox {
public:
CSimpleMsgBoxImpl();
virtual ~CSimpleMsgBoxImpl();

    // IUnknown 메소드
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface( REFIID riid, void** ppv );

    // ISimpleMsgBox methods
    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );

protected:
    ULONG m_uRefCount;
};

class __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}")) CSimpleMsgBoxImpl;

 

COM 클라이언트가 SimpleMsgBox 형식의 COM 객체를 생성하고자 할 때 코드는 다음과 같이 작성합니다.

ISimpleMsgBox* pIMsgBox;
HRESULT hr;

hr = CoCreateInstance(__uuidof(CSimpleMsgBoxImpl), // coclass의 CLSID
    NULL, // 객체 통합하지 않음
    CLSCTX_INPROC_SERVER, // 인 프로세스 DLL 서버
    __uuidof(ISimpleMsgBox), // 우리가 원하는 인터페이스에 대한 IID
    (void**) &pIMsgBox); // 인터페이스 포인터의 주소

 

클래스팩토리

 

클래스팩토리 구현

SimpleMsgBox 팩토리는 C++ 클래스로 구현되어 있으며, 예상하다시피 CSimpleMsgBoxClassFactory로 명명합니다.

class CSimpleMsgBoxClassFactory : public IClassFactory {
public:
    CSimpleMsgBoxClassFactory();
    virtual ~CSimpleMsgBoxClassFactory();

    // IUnknown 메소드
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface(REFIID riid, void** ppv);

    // IClassFactory 메소드
    HRESULT CreateInstance(IUnknown* pUnkOuter, REFIID riid, void ** ppv);
    HRESULT LockServer(BOOL fLock);

protected:
    ULONG m_uRefCount;
};

 

생성자, 소멸자 및 IUnknown서 보았던 예제들과 거의 같게 작동됩니다. 여러분이 예상하듯이 IClassFactory의 메소드이며 새롭게 추가된 LockServer는 비교적 단순합니다.

HRESULT CSimpleMsgBoxClassFactory::LockServer(BOOL fLock) {
    fLock ? g_uDllLockCount++ : g_uDllLockCount--;
    return S_OK;
}

 

지금부터는 흥미로운 내용인 CreateInstance에 대해 살펴보겠습니다. 이 메소드는 새로운 CSimpleMsgBoxImpl 객체를 만드는 역할을 합니다. 이 메소드의 원형과 파라미터에 대해 자세히 보겠습니다.

HRESULT CSimpleMsgBoxClassFactory::CreateInstance(IUnknown * pUnkOuter, REFIID riid, void ** ppv);

 

pUnkOuter는 새 객체가 결합될 때에 한하여 사용됩니다. 그리고 이는 “외부(outer)” COM 객체를 가리키는 포인터입니다. 즉, 새로운 객체를 포함하게 될 기존의 객체입니다. 객체 결함은 본 글의 범위를 벗어나는 주제이므로 우리의 예제 객체는 결합을 지원하지 않을 것입니다.

riidppvQueryInterface에서와 동일하게 사용됩니다. 각각 COM 클라이언트가 요청하고 있는 인터페이스에 대한 IID이고, 인터페이스 포인터를 보관할 수 있는 포인터 크기의 버퍼입니다.

여기 CreateInstance 구현 예가 있습니다. 먼저 파라미터 유효성 검증과 초기화부터 시작합니다.

HRESULT CSimpleMsgBoxClassFactory::CreateInstance(IUnknown * pUnkOuter, REFIID riid, void ** ppv) {
    // 객체 결합을 사용하지 않을 것이므로 pUnkOuter는 항상 NULL이어야 합니다.
    if (pUnkOuter != NULL)
        return CLASS_E_NOAGGREGATION;

    // ppv가 실제 void * 형 포인터를 가리키고 있는지 검사합니다.
    if (IsBadWritePtr(ppv, sizeof(void *)))
        return E_POINTER;

    *ppv = NULL;

 

파라미터가 유효함을 확인하였기 때문에 우리는 새로운 객체를 생성할 수 있습니다.

CSimpleMsgBoxImpl* pMsgbox;

// 새로운 COM 객체를 생성합니다.
pMsgbox = new CSimpleMsgBoxImpl;

if (NULL == pMsgbox) return E_OUTOFMEMORY;

 

마지막으로 우리는 새롭게 생성되는 객체에 대해 클라이언트가 요청한 형식의 인터페이스로 QI를 합니다. QI가 실패하면 객체는 사용할 수 없는 것이므로 우리는 이를 해제합니다.

HRESULT hrRet;

// 클라이언트가 요청한 형식의 인터페이스로 새로 만든 객체에 대해 QI합니다.
hrRet = pMsgbox->QueryInterface(riid, ppv);

// QI가 실패하면 클라이언트가 사용할 수 없는 객체이므로 이를 해제합니다.
// 왜냐하면 객체로부터 원하는 인터페이스 형식을 얻을 수 없기 때문입니다.
if (FAILED(hrRet)) delete pMsgbox;

return hrRet;

 

DllGetClassObject

DllGetClassObject의 내부를 좀 더 자세히 들여다 보겠습니다. 원형은 다음과 같습니다.

HRESULT DllGetClassObject(REFCLSID rclsid, REFIID riid, void ** ppv);

 

rclsid는 COM 클라이언트가 필요로 하는 coclass의 CLSID입니다. 이 함수는 해당 coclass에 대한 클래스팩토리를 반드시 반환해야 합니다.

riidppv는 QI에서 언급한 파라미터와 같습니다. 이 경우 riid는 COM 라이브러리가 요청하는 클래스팩토리 객체에 대한 IID, 즉 IID_IClassFactory입니다.

DllGetClassObject가 새로운 COM 객체로서 클래스 팩토리를 반환하기 때문에, 이 코드는 IClassFactory::CreateInstance와 비슷해 보입니다. 유효성 검사와 초기화로 시작하는 부분을 살펴보겠습니다.

HRESULT DllGetClassObject(REFCLSID rclsid, REFIID riid, void ** ppv) {

    // 클라이언트가 CSimpleMsgBoxImpl에 대한 팩토리를 요청하고 있는지 검사합니다.
    if (!InlineIsEqualGUID(rclsid, __uuidof(CSimpleMsgBoxImpl)))
        return CLASS_E_CLASSNOTAVAILABLE;

    // ppv가 실제 void * 형 버퍼를 가리키고 있는지 검사합니다.
    if ( IsBadWritePtr ( ppv, sizeof(void*) ))
        return E_POINTER;

    *ppv = NULL;

 

먼저 if 구문이 rclsid 파라미터를 검사합니다. 우리의 COM 서버는 단 하나의 coclass만을 가지고 있기 때문에, rclsid는 반드시 CSimpleMsgBoxImpl에 대한 CLSID여야 합니다. __uuidof 연산자는 앞서 __deslcped(uuid()) 선언으로 CSimpleMsgBoxImpl 클래스에 부여된 GUID를 가져오는 작동을 합니다. InlineIsEqualsGUID는 두 개의 GUID가 서로 같은지 여부를 확인하는 인라인 함수입니다.

다음 단계는 클래스팩토리 생성 단계입니다.

CSimpleMsgBoxClassFactory* pFactory;

// 새로운 클래스팩토리를 생성합니다.
pFactory = new CSimpleMsgBoxClassFactory;

if (pFactory == NULL)
        return E_OUTOFMEMORY;

 

이 부분이 CreateInstance와 다소 다른 부분입니다. CreateInstance로 돌아가서, 우리는 QI의 호출이 실패하였을 때 COM 객체를 할당 해제하였습니다. 하지만 이 부분은 그렇지 않습니다.

우리는 스스로를 우리가 만든 COM 객체(클래스팩토리)의 클라이언트라고 생각할 수 있습니다. 그래서 우리는 이 객체의 레퍼런스 카운트를 1로 만들기 위해 AddRef를 호출합니다. 그 다음 우리는 QI를 호출합니다. QI가 성공하면 이 객체에 한번 더 AddRef가 이루어져서 레퍼런스 카운트가 2가 됩니다. QI가 실패하면 레퍼런스 카운트는 1로 감소합니다.

QI가 호출된 후 우리는 클래스팩토리 객체의 사용을 더 이상 안 할 것이기 때문에 Release를 호출합니다. QI가 실패하면 이 클래스팩토리의 레퍼런스카운트가 0이 되어 스스로 할당 해제합니다. 결국 결과는 같습니다.

// 클래스팩토리를 사용하고 있는 동안에는 AddRef()를 한 번 해줍니다.
pFactory->AddRef();

HRESULT hrRet;

// 클래스팩토리에 대해 COM 클라이언트가 요청한 형태의 인터페이스가 있는지 QI합니다.
hrRet = pFactory->QueryInterface(riid, ppv);

// 클래스팩토리의 사용을 끝냈으므로 Release합니다.
pFactory->Release();

return hrRet;

 

QueryInterface 다시 살펴보기

필자는 이전에 QI의 구현을 보인 바 있습니다. 그러나 실전에서는 COM 객체가 꼭 IUnknown만을 구현하지 않기 때문에 클래스팩토리의 QI는 다시 살펴보는 것이 좋습니다. 먼저 우리는 ppv 버퍼가 유효한지 검사하고 이를 초기화합니다.

HRESULT CSimpleMsgBoxClassFactory::QueryInterface(REFIID riid, void ** ppv) {
HRESULT hrRet = S_OK;

    // ppv가 실제로 void * 형 버퍼를 참조하고 있는 지 검사합니다.
    if (IsBadWritePtr(ppv, sizeof(void *)))
        return E_POINTER;

    // 표준 QI 초기화: *ppv를 NULL로 설정합니다.
    *ppv = NULL;

 

그 다음 우리는 riid를 검사하여 해당 클래스팩토리에서 구현하고 있는 IUnknown 또는 IClassFactory 중 하나의 것인지를 확인합니다.

// COM 클라이언트가 우리자 지원할 수 있는 형태의 인터페이스를 요청하고 있다면 *ppv 설정
if (InlineIsEqualGUID(riid, IID_IUnknown)) {
    *ppv = (IUnknown*) this;
} else if (InlineIsEqualGUID(riid, IID_IClassFactory)) {
    *ppv = (IClassFactory *) this;
} else {
    hrRet = E_NOINTERFACE;
}

 

마지막으로 riid가 가리키는 인터페이스가 우리가 지원할 수 있는 인터페이스일 때 우리는 AddRef를 호출하여 인터페이스 포인터의 레퍼런스 카운트를 1만큼 증가시킵니다. 그 다음 인터페이스 포인터를 반환합니다.

// 인터페이스 포인터를 반환할 수 있게 되었다면, AddRef 호출
if (hrRet == S_OK) {
        ((IUnknown *) *ppv)->AddRef();
}

return hrRet;

 

ISimpleMsgBox 구현

우리의 소스 코드는 ISimpleMsgBox가 가진 유일한 메소드인 DoSimpleMsgBox를 가지고 있습니다. 우리는 bsMessageTextTCHAR로 변환하기 위해 마이크로소프트의 확장 클래스인 _bstr_t를 처음으로 사용해 봅니다.

HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox(HWND hwndParent, BSTR bsMessageText) {
    _bstr_t bsMsg = bsMessageText;
    LPCTSTR szMsg = (TCHAR *) bsMsg;  // 필요하다면 _bstr_t 문자열을 TCHSR로 변환합니다.
    // 변환 후에 우리는 메시지 박스를 보여주고 값을 반환합니다.
    MessageBox(hwndParent, szMsg, _T("Simple Message Box"), MB_OK);
    return S_OK;
}

 

계속 읽기

이전 게시글: COM의 소개(파트 2) – COM 서버의 이면 (7)

다음 게시글: COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

카테고리 “API/COM”
more...