2008. 7. 9. 21:09

TN038: MFC/OLE IUnknown Implementation

IUnknown
OLE는 interface로서 IUnknown 으로부터 상속된 모든 클래스를 참조한다.
IUnknown "interface"는 구현이 되지 않은 추상 인터페이스만을 아래와 같이 정의하고 있다.

class IUnknown
{
public:
    virtual HRESULT QueryInterface(REFIID iid, void** ppvObj) = 0;
    virtual ULONG AddRef() = 0;
    virtual ULONG Release() = 0;
};

Note : __stdcall 과 같은 필수 호출 규약은 생략하였음

COM은 객체들을 추적하는데 있어 참조 카운트 스키마를 사용한다. 하나의 객체는 C++ 내에서 절대로 직접적으로 참조 될 수 없다. 대신에, COM 객체는 항상 포인터를 통해서 참조된다.

소유자(Owner)가 객체를 사용을 완료했을 때, 그 객체를 해제하기 위해서는, 그 객체의 Release 멤버 함수가 호출 된다. (그와 반대로 전통적인 C++에서는 delete 연산자를 이용한다.)

참조 카운트 메커니즘은 단일 객체에 대해 다중 참조를 관리하게 해준다. AddRefRelease는 그 객체에 대한 참조 카운트를 관리한다. 객체는 자신의 참조 카운트가 0이 될 때가지 삭제되지 않는다.

아래 AddRefRelease의 간단한 구현 예이다.

ULONG CMyObj::AddRef()
{
    return ++m_dwRef;
}

ULONG CMyObj::Release()
{
    if (--m_dwRef == 0)
    {
        delete this;
        return 0;
    }
    return m_dwRef;
}

QueryInterface는 동일한 객체에 대한 다른 인터페이스를 얻을 수 있도록 한다. 이 인터페이스들은 보통 IUnknown으로부터 상속되고 새로운 멤버 함수를 추가함으로서 부가적인 기능을 추가한다. COM 인터페이스는 인터페이스 내에 선언된 멤버 변수들을 절대로 소유하지 않으며, 모든 멤버 함수는 순수-가상 함수로 선언된다. 아래는 그 예이다.

class IPrintInterface : public IUnknown
{
public:
    virtual void PrintObject() = 0;
};

만일 IUnknown 개체만을 가지고 있는 경우에 IPrintInterface 을 얻기 위해서는 IPrintInterfaceIID를 사용하여 IUnknown::QueryInterface를 호출한다. IID는 인터페이스를 식별하는 고유의 128 비트 숫자이다. 각각의 인터페이스마다 IID가 존재한다. pUnk가 IUnknown 객체의 포인터라면, IPrintInterface 를 얻어오는 코드는 다음과 같다.

IPrintInterface* pPrint = NULL;
if (pUnk->QueryInterface(IID_IPrintInterface,
    (void**)&pPrint) == NOERROR)
{
    pPrint->PrintObject();
    pPrint->Release();  
        // release pointer obtained via QueryInterface
}

객체에 IPrintInterface와 IUnknown 인터페이스 둘다를 지원하기 위해서는 어떻게 해야 할까?
이 경우에, IPrintInterface가 IUnknown을 직접적으로 상속하고 있기 때문에 아주 단순하다. IPrintInterface를 구현함으로써, IUnknown 은 자동적으로 지원된다. 예로 :

class CPrintObj : public CPrintInterface
{
    virtual HRESULT QueryInterface(REFIID iid, void** ppvObj);
    virtual ULONG AddRef();
    virtual ULONG Release();
    virtual void PrintObject();
};

AddRef와 Release의 구현은 위에서 구현된 코드와 같을 것이다. CPintObj::QueryInterface는 아래와 같다.

HRESULT CPrintObj::QueryInterface(REFIID iid, void FAR* FAR* ppvObj)
{
    if (iid == IID_IUnknown || iid == IID_IPrintInterface)
    {
        *ppvObj = this;
        AddRef();
        return NOERROR;
    }
    return E_NOINTERFACE;
}

보는바와 같이, Interface Identifier(IID)가 인식되면, 포인터가 개체에 대입된다. 성공적인 QueryInterface 는 암시적인 AddRef를 초래한다는 것을 기억하라.

물론, CEditObj::Print를 또한 구현해야 한다. IPrintInterface는 직접적으로 IUnknown 인터페이스로부터 상속되었기 때문에 간단하다. 그러나 만약 IUnknown 인터페이스로부터 상속된 두개의 다른 인터페이스를 지원하길 원한다면, 어떨지 고려해보자.

class IEditInterface : public IUnkown
{
public:
    virtual void EditObject() = 0;
};

C++ 다중 상속을 이용하는 것과 같이 IEditInterface와 IPrintInterface 모두를 지원하는 클래스를 구현하는 방법은 매우 많다. 이 문서는 이것을 구현하기 위한 중첩클래스(Nested class)를 사용하는 것에 집중할 것이다.

class CEditPrintObj
{
public:
    CEditPrintObj();

    HRESULT QueryInterface(REFIID iid, void**);
    ULONG AddRef();
    ULONG Release();
    DWORD m_dwRef;

    class CPrintObj : public IPrintInterface
    {
    public:
        CEditPrintObj* m_pParent;
        virtual HRESULT QueryInterface(REFIID iid, void** ppvObj);
        virtual ULONG AddRef();
        virtual ULONG Release();
    } m_printObj;

    class CEditObj : public IEditInterface
    {
    public:
        CEditPrintObj* m_pParent;
        virtual ULONG QueryInterface(REFIID iid, void** ppvObj);
        virtual ULONG AddRef();
        virtual ULONG Release();
    } m_editObj;
};

전체 구현은 아래와 같다.

CEditPrintObj::CEditPrintObj()
{
    m_editObj.m_pParent = this;
    m_printObj.m_pParent = this;
}

ULONG CEditPrintObj::AddRef()
{
    return ++m_dwRef;
}

CEditPrintObj::Release()
{
    if (--m_dwRef == 0)
    {
        delete this;
        return 0;
    }
    return m_dwRef;
}

HRESULT CEditPrintObj::QueryInterface(REFIID iid, void** ppvObj)
{
    if (iid == IID_IUnknown || iid == IID_IPrintInterface)
    {
        *ppvObj = &m_printObj;
        AddRef();
        return NOERROR;
    }
    else if (iid == IID_IEditInterface)
    {
        *ppvObj = &m_editObj;
        AddRef();
        return NOERROR;
    }
    return E_NOINTERFACE;
}

ULONG CEditPrintObj::CEditObj::AddRef()
{
    return m_pParent->AddRef();
}

ULONG CEditPrintObj::CEditObj::Release()
{
    return m_pParent->Release();
}

HRESULT CEditPrintObj::CEditObj::QueryInterface(
    REFIID iid, void** ppvObj)
{
    return m_pParent->QueryInterface(iid, ppvObj);
}

ULONG CEditPrintObj::CPrintObj::AddRef()
{
    return m_pParent->AddRef();
}

ULONG CEditPrintObj::CPrintObj::Release()
{
    return m_pParent->Release();
}

HRESULT CEditPrintObj::CPrintObj::QueryInterface(
    REFIID iid, void** ppvObj)
{
    return m_pParent->QueryInterface(iid, ppvObj);
}

대부분의 IUnknown 구현은 CEditPrintObj::CEditObj 와 CEditPrintObj::CPrintObj내에 코드를 중첩하는 것보다 CEditPrintObj 클래스에 위치시키는 것을 기억하라. 이것은 코드량을 줄여주고 버그를 피할 수 있게 한다. 여기서 핵심은 IUnknown 인터페이스로부터 그 객체가 지원하는 모든 인터페이스를 가져오게 하기 위해 QueryInterface를 호출하는 것이 가능하다는 것이다, 그리고 그러한 각각의 인터페이스로부터도 모든 인터페이스를 가져오는 것 또한 가능하다. 이것은 각각의 인터페이스로터 이용가능한 모든 QueryInerface 함수는 똑같이 동작한다는 것을 의미한다. 이러한 내장 객체들이 외부객체(Outer Object)에 있는 구현을 호출하기 위해서는, Back-Pointer가 사용된다(m_pParent). m_pParent 포인터는 CEditPrintObj 생성자에서 초기화된다.

이제 CEditPrintObj::CPrintObj::PrintObject와 CEditPrintObj::CEditObj::EditObject를 구현해야 한다. 한가지 기능을 추가하기 위해 약간의 코드가 더해졌다. 운이 좋게도, 인터페이스들이 한개의 멤버 함수만을 가지고 있는 경우는 매우 일반적이지 않으며 이러한 경우, EditObject와 PrintObject는 보통 한개의 인터페이스로 합쳐질 것이다.

이러한 단순한 시나리오의 경우에 해당하는 많은 설명과 코드가 있다. MFC/OLE 클래스는 더 심플한 대안을 제공한다. MFC 구현은 윈도우 메시지가 메시지 맵에 래핑되는것과 유사한 테크닉을 사용한다. 이러한 기능은 Interface Map이라 불리우고 다음 섹션에서 논의된다.

MFC Interface Maps

MFC/OLE 는 개념(concept)과 수행(execution)에 있어 MFC의 메시지 맵과 유사한 인터페이스 맵과 디스패치 맵의 구현을 제공한다. MFC 인터페이스 맵의 핵심 기능은 아래와 같다.

* CCmdTarget 클래스내에 구현된 IUnknown의 표준 구현
* AddRef 와 Release에 의해 수정되는 참조 카운트의 관리
* QueryInterface의 데이터 주도 구현(Data driven implementation)

게다가, 인터페이스 맵은 아래의 향상된 기능을 지원한다.

* 집합체가 될 수 있는 COM 개체(aggregatable COM ojbect)를 생성하는 것을 지원
* COM 객체의 구현 안에 집합체(aggregate object)를 사용하는 것을 지원
* 구현은 후킹가능하고 확장할 수 있다.

집합(aggregation)에 대한 자세한 정보는, OLE Programmer's Reference를 보라.

MFC의 인터페이스 맵 지원은 CCmdTarget 클래스 내에 내장되어 있다. CCmdTarget은 참조 카운트 뿐만 아니라 IUnknown 구현과 연결된 모든 멤버함수를 가진다 (예를 들어 참조 카운트는 CCmdTarget내에 있다). OLE COM을 지원하는 클래스를 생성하기 위해서는, CCmdTarget을 상속하고 의도하는 인터페이스를 구현하기 위한 다양한 매크로와 CCmdTarget의 멤버 함수를 사용한다. MFC의 구현은 위에서 설명한 예제와 같이 각각의 인터페이스 구현을 정의하기 위해 중첩 클래스를 사용한다. IUnknown의 표준 구현 뿐만 아니라 몇가지 반복적인 코드를 제거하는 여러 매크로를 이용하여 더 쉽게 구현할 수 있게 해준다

Inteface Map Basic

MFC 인터페이스 맵을 사용하는 클래스를 구현하기

1. 직접 혹은 간접적으로 CCmdTarget을 상속한다.
2. 클래스 정의에 DECLARE_INTERFACE_MAP 함수를 사용한다.
3. 지원하고자 하는 각각의 인터페이스에 대해, 클래스 정의 내에 BEGIN_INTERFACE_PARTEND_INTERFACE_PART 매크로를 사용한다.
4. 구현 파일 내에, 클래싀 인터페이스 맵을 정의하기 위해 BEGTIN_INTERFACE_MAPEND_INTERFACE_MAP 매크로를 사용한다.
5. 지원된 각각의 IID에 대해, IID를 클래스의 특정 부분에 맵핑하기 위해 BEGIN_INTERFACE_MAPEND_INTERFACE_MAP 매크로 사이에 INTERFACE_PART 매크로를 사용한다.
6. 지원하는 인터페이스를 나타내는 각각의 중첩 클래스를 구현한다.
7. 부모, CCmdTarget을 상속한 객체에 접근하기 위해 METHOD_PROLOGUE를 사용한다.
8. AddRef, Release, 그리고 QueryInterface는 이와 같은 함수의 CCmdTaret 구현 (ExternalAddRef, ExternalRelease, 그리고 ExternalQueryInterface)에 위임할 수 있다.

위의 CPrintEditObj 예제는 아래와 같이 구현될 수 있다.

class CPrintEditObj : public CCmdTarget
{
public:
    // member data and member functions for CPrintEditObj go here

// Interface Maps
protected:
    DECLARE_INTERFACE_MAP()

    BEGIN_INTERFACE_PART(EditObj, IEditInterface)
        STDMETHOD_(void, EditObject)();
    END_INTERFACE_PART(EditObj)

    BEGIN_INTERFACE_PART(PrintObj, IPrintInterface)
        STDMETHOD_(void, PrintObject)();
    END_INTERFACE_PART(PrintObj)
};

위의 선언은 CCmdTarget을 상속한 클래스를 생성한다. DECLARE_INTERFACE_MAP 매크로는 프레임워크에게 이 클래스는 커스텀 인터페이스 맵을 가질것이다라는 것을 알려준다. 게다가, BEGIN_INTERFACE_PARTEND_INTERFACE_PART 매크로는 CEditObj와 CPrintObj로 이름지어진 중첩 클래스를 정의한다. (X는 "C"로 시작하는 전역 클래스와 "I"로 시작하는 인터페이스 클래스와 중첩 클래스를 구분하기 위해서만 사용된다.) 이 클래스들에 대한 두개의 중첩 멤버(nested member)가 생성된다 : m_CEditObj 와 m_CPrintObj. 매크로는 자동적으로 AddRef, Release, 그리고 QueryInterface 함수를 선언한다. 그리하여 이 인터페이스 고유의 함수들만 선언한다. (OLE 매크로 STDMETHOD는 대상 플랫폼에 적합한 _stdcall 과 virtual 키워드를 제공하기 위해 사용된다.)

이 클래스의 경우 인터페이스를 정의하기 위해서는

BEGIN_INTERFACE_MAP(CPrintEditObj, CCmdTarget)
    INTERFACE_PART(CPrintEditObj, IID_IPrintInterface, PrintObj)
    INTERFACE_PART(CPrintEditObj, IID_IEditInterface, EditObj)
END_INTERFACE_MAP()

이것은 각각 m_CPrintObj와 IID_IPrintInterface를, m_CEditObj와 IID_IEditInterface를 연결한다. QueryInterface의 CCmdTarget 구현(CCmdTarget::ExternalQueryInterface)는 m_CPrintObj와 m_CEditObj에 대한 포인터를 반환하기 위해 이 맵을 사용한다. IID_IUnknown에 대한 엔트리를 포함하는 것은 불필요하다. 프레임워크는 IID_IUnknown이 요구되었을 때 맵에 있는 첫번째 인터페이스(이 경우에, m_CPrintObj)를 사용할 것이다.

비록 BEGIN_INTERFACE_PART 매크로가 자동적으로 AddRef, Release 그리고 QueryInterface 함수를 선언했다고 할지라도, 여전히 그것들을 구현해야할 필요가 있다.

ULONG FAR EXPORT CEditPrintObj::XEditObj::AddRef()
{
    METHOD_PROLOGUE(CEditPrintObj, EditObj)
    return pThis->ExternalAddRef();
}

ULONG FAR EXPORT CEditPrintObj::XEditObj::Release()
{
    METHOD_PROLOGUE(CEditPrintObj, EditObj)
    return pThis->ExternalRelease();
}

HRESULT FAR EXPORT CEditPrintObj::XEditObj::QueryInterface(
    REFIID iid, void FAR* FAR* ppvObj)
{
    METHOD_PROLOGUE(CEditPrintObj, EditObj)
    return (HRESULT)pThis->ExternalQueryInterface(&iid, ppvObj);
}

void FAR EXPORT CEditPrintObj::XEditObj::EditObject()
{
    METHOD_PROLOGUE(CEditPrintObj, EditObj)
    // code to "Edit" the object, whatever that means...
}

CEditPrintObj::CPrintObj에 대한 구현은 위의 CEditPrintObj::CEditObj의 정의와 비슷할 것이다. 비록 이러한 함수들을 자동적으로 생성하기 위해 사용될 수 있는 매크로를 만들수 있을지라도, 매크로가 한줄 이상의 코드를 생성할 때 브레이크 포인터를 걸기가 어려워 질 것이다. 이러한 이유로, 이 코드는 수동으로 확장된다.

메시지 맵의 프레임워크 구현을 사용함으로써, 다음과 같은 작업을 할 필요가 없다.

* QueryInterface 구현
* AddRef와 Release 구현
* 두 인터페이스에 대한 각각의 built-in 메소드 선언

게다가, 프레임워크는 내부적으로 메시지 맵을 사용한다. 이것은 이미 특정 인터페이스를 지원하는 그리고 그 인터페이스에 대한 대체(replacement) 또는 추가(addition)를 제공하는 COleServerDoc와 같은 프레임워크 클래스를 얻어 올 수 있게 한다. 이것은 프레임워크가 베이스 클래스로부터 인터페이스 맵을 상속하는 것을 전적으로 지원한다는 사실에서 가능해진다 - 즉 BEGIN_INTERFACE_MAP이 두번째 파라미터로 베이스 클래스의 이름을 가지는 이유이다.

Aggregation and Interface Maps

stand-along COM 개체를 지원하는 것에 외에, MFC는 또한 집합(aggregation)을 지원한다. 집합은 여기서 다루기에는 너무 복잡하다. 집합에 대한 자세한 정보는 OLE Programmer's Reference를 참조해라. 이 문서는 간단하게 프레임워크와 인터페이스 맵 내에 집합을 구축하는 것에 대해 설명할 것이다.

집합을 사용하는 방법에는 두가지가 있다.

(1) 집합을 지원하는 COM 객체를 사용하는 것
(2) 다른 객체에 의해 집합될 수 있는(can be aggregated) 객체를 구현하는 것

이러한 기능은 "집합체 사용하기(Using an aggregate object)"와 "집합될 수 있는 객체 만들기(Making an object aggregatable)" 로 다루어 진다. MFC는 두가지 모두 지원한다.

Using an aggregate object

집합체를 사용하기 위해서는, 그 집합체를 QueryInterface 매커니즘 내에 묶는 몇가지 방법이 필요한다. 다른 말로 하면, 집합체는 객체의 고유한 일부(native part)인 것처럼 동작해야 한다. 그래서 이것을 어떻게 MFC의 인터페이스 맵 매커니즘으로 묶을수 있을까? INTERFACE_PART 매크로외에, CCmdTarget 을 상속한 클래스의 일부로 집합체를 선언할 수 있다. 그렇게 하기 위해, INTERFACE_AGGREGATE 매크로가 사용된다. 이것은 멤버 변수( IUnknown이나 상속클래스의 포인터이어야 한다)를 지정할 수 있게 한다 그리고 이것은 인터페이스 맵 매커니즘 안으로 통합된다. CCmdTarget::ExternalQueryInterface가 호출될 때 포인터가 NULL이 아니고,  그리고 요청된 IID가 CCmdTarget 개체 자신에 의해 지원되는 고유의 IID가 아니라면 프레임워크는 자동적으로 집합체의 QueryInterface 멤버 함수를 호출할 것이다, .

INTERFACE_AGGREGATE 매크로 사용하기

1. 집합체에 대한 포인터를 포함할 멤버변수(IUnknown 포인터)를 선언한다
2. 인터페이스 맵내에 INTERFACE_AGGREGATE를 포함한다. 이것은 멤버변수를 이름으로 참조한다.
3. 특정 지점(보통 CCmdTarget::OnCreateAggreagets 내에서), 멤버 변수는 NULL이 아닌 다른것으로 초기화 한다.

아래는 예:

class CAggrExample : public CCmdTarget
{
public:
    CAggrExample();

protected:
    LPUNKNOWN m_lpAggrInner;
    virtual BOOL OnCreateAggregates();

    DECLARE_INTERFACE_MAP()
    // "native" interface part macros may be used here
};

CAggrExample::CAggrExample()
{
    m_lpAggrInner = NULL;
}

BOOL CAggrExample::OnCreateAggregates()
{
    // wire up aggregate with correct controlling unknown
    m_lpAggrInner = CoCreateInstance(CLSID_Example,
        GetControllingUnknown(), CLSCTX_INPROC_SERVER,
        IID_IUnknown, (LPVOID*)&m_lpAggrInner);
    if (m_lpAggrInner == NULL)
        return FALSE;
    // optionally, create other aggregate objects here
    return TRUE;
}

BEGIN_INTERFACE_MAP(CAggrExample, CCmdTarget)
    // native "INTERFACE_PART" entries go here
    INTERFACE_AGGREGATE(CAggrExample, m_lpAggrInner)
END_INTERFACE_MAP()

m_lpAggrInnter 은 생성자에서 NULL로 초기화 된다. 프레임워크는 QueryInterface의 디폴트 구현안에서 NULL 멤버 변수는 무시할 것이다. OnCreateAggregates는 집합체를 실질적으로 생성하기 위한 좋은 위치이다. 만일 COleObjectFactory의 MFC 구현 외부에서 객체를 생성하고 있다면, 명시적으로 그것을 호출해야만 할 것이다. CCmdTarget::OnCreateAggregates내에서 집합체를 생성하는 이유뿐만 아니라 CCmdTarget::GetControllingUnknown 의 사용은 집합체를 생성하는 것에 대해 논의될 때 명백해 질 것이다.

이 기법은 객체에 집합체가 지원하는 모든 인터페이스를  고유의 인터페이스에 추가해 줄 것이다. 집합체가 지원하는 인터페이스 일부만을 원한다면, CCmdTarget::GetInterfaceHook 을 재정의 할 수 있다. 이것은 QueryInterface와 유사하게, 아주 낮은 레벨의 hookability를 제공한다. 보통, 집합체가 지원하는 모든 인터페이스를 원할 것이다.

Making an object Implemtation Aggregatable

집합될 수 있는 객체의 경우, AddRef, Release, 그리고 QueryInterface의 구현은 "Controlling Unknown"에 위임해야만 한다. 다른 말로 하면, 객체가 다른 객체의 일부가 되기 위해서는, IUnknown을 상속한 다른 객체에 AddRef, Release, 그리고 QueryInterface를 위임해야만 한다. 이 "controlling unknown"는 객체가 생성될 때 그 객체에 제공된다, 즉, COleObjectFactory의 구현에 제공된다. 이것을 구현하는 것은 약간의 오버헤드를 수반하며 어떤 경우에는 바람직하지 않을수 있다. 그래서 MFC는 이것을 선택할 수 있게 만들었다. 객체가 집합가능하게 만들기 위해서는, 객체의 생성자에서 CCmdTarget::EnableAggregation을 호출해야 한다.

그 객체가 또한 집합체를 사용한다면, 그 집합체에다가 올바른 "controlling unknown"을 전달해야만 한다. 보통 이 IUnknown 포인터는 집합체가 생성될 때 객체에 전달된다. 예를 들면, pUnkOuter 파라미터는 CoCreateInstance로 생성된 객체들을 위한 "controlling unknown"이다. 올바른 "controlling unknown" 포인터는 CCmdTarget::GetControllingUnknown을 호출함으로써 얻을 수 있다. 그러나 그 함수에 의해 반환되는 값은 생성자에서는 유효하지 않다. 이 이유로, GetControllingUnknown 의 반환값이 신뢰할 수 있는, 비록 COleObjectFactory 구현으로부터 생성된 것조차, CCmdTarget::OnCreateAggreates 를 재정의하여 집합체를 생성하도록 권유된다.

인위적인 참조 카운트를 증가시키거나 해제할때 객체가 올바른 참조 카운트를 조절하는 것 또한 매우 중요하다. 이것을 보장하기 위해, InternalReleaseInternalAddRef 대신에 ExternalAddRefExternalRelease를 항상 호출해라. 집합체를 지원하는 클래스에서 InternalRelease 또는 InternalAddRef를 호출하는 것은 거의 볼 수 없다.



참고 : MSDN 2003 Technical Note #38