Page #56 (Tear-Off Interfaces)

< BACK  NEXT >
[oR]

Reusing Components

Consider the following scenario. In our TV-VCR sample, component VCR supports IVideo and ISVideo interfaces. Things were working great for our VCR manufacturer, that is, until the electronics manufacturer consortium decided to publish a new interface, IComponentVideo. This new interface happens to provide superior picture quality. The following is its definition:

 [   object,    uuid(318B4AD4-06A7-11d3-9B58-0080C8E11F14),    helpstring("IComponentVideo Interface"),    pointer_default(unique)  ]  interface IComponentVideo : IUnknown  {   [helpstring("Obtain the signal value")]    HRESULT GetCVideoSignalValue([out, retval] long* val);  }; 

A bold startup company, ComponentVideo.com, went ahead and developed a new COM component that provides the IComponentVideo interface.

Our original VCR manufacturer has grown fat. As with any big company, management realizes that it cannot move fast. Its only choice is to collaborate with the startup company. Source code reuse is not an option as the two companies have different coding standards and perhaps prefer using different programming languages. So the only choice is to use the startup s component at the binary level (as an executable). The trick, however, is to hide from the customers the information that a third-party component is involved.

There are two ways to achieve this goal containment and aggregation. Let s look at each of them in detail.

Containment

An easy way to accomplish the company s goal of reusing a third-party component, while hiding the fact from the customers, is to create an instance of the third-party component and internally maintain the interface pointer. This mechanism is referred to as containment one component contains pointers to interfaces on the other component. In this respect, the containing component is simply a client of the contained component. In COM terminology, the containing component is referred to as the outer component and the contained component as the inner component. Figure 3.2 illustrates this concept.

Figure 3.2. COM containment.
graphics/03fig02.gif

The outer component supports the same interface as that of the inner component. However, the implementation explicitly forwards the method calls to the inner component. Such an implementation for our CVcr class is shown below:

 class CVcr : public IVideo, public ISVideo, IComponentVideo  { public:    CVcr(void);    HRESULT Init();    ~CVcr();    // IUnknown interface    ...    // IVideo interface    ...    // ISVideo interface    ...    // IComponentVideo interface    STDMETHOD(GetCVideoSignalValue)(long* plVal);  private:    ...    IComponentVideo* m_pComponentVideo;  };  CVcr:: CVcr()  {   ...    m_pComponentVideo = NULL;  }  HRESULT CVcr::Init()  {   HRESULT hr = ::CoCreateInstance(     CLSID_ComponentVCR,      NULL,      CLSCTX_ALL,      IID_IComponentVideo,      reinterpret_cast<void**>(&m_pComponentVideo));    return hr;  }  CVcr::~CVcr()  {   if (NULL != m_pComponentVideo) {     m_pComponentVideo->Release();    }    ...  }  STDMETHODIMP CVcr::QueryInterface(REFIID iid, void** ppRetVal)  {   ...    if (IsEqualIID(iid, IID_IComponentVideo)) {     *ppRetVal = static_cast<IComponentVideo*>(this);    }    ...  }  HRESULT CVcr::GetCVideoSignalValue(long* pRetVal)  {   _ASSERT (NULL != m_pComponentVideo);    return m_pComponentVideo->GetCVideoSignalValue(pRetVal);  } 

Note that the interface method GetCVideoSignalValue gets forwarded to the inner component s implementation. In general, for every interface method, the outer component has to define a wrapper method that simply turns around and calls the appropriate method on the inner component.

The TV client can obtain the IComponentVideo interface from the VCR in the same way as it normally does, that is, using either CoCreateInstance or QueryInterface, as shown here:

 int main(int argc, char* argv[])  {   ::CoInitialize(NULL);    IComponentVideo* pCVideo = NULL;    HRESULT hr = ::CoCreateInstance(CLSID_VCR, NULL, CLSCTX_ALL,      IID_IComponentVideo, reinterpret_cast<void**>(&pCVideo));    if (FAILED(hr)) {     DumpError(hr);      ::CoUninitialize();      return 1;    }    UseCVideo(pCVideo);    pCVideo->Release();    ::CoUninitialize();    return 0;  }  void UseCVideo(IComponentVideo* pCVideo)  {   long val;    HRESULT hr;    for(int i=0; i<10; i++) {     hr = pCVideo->GetCVideoSignalValue(&val);      if (FAILED(hr)) {       DumpError(hr);        continue;      }      cout << "Round: " << i << " - Value: " << val << endl;    }  } 

There are two issues with containment:

  • For each method in the inner component, the outer component has to implement a wrapper. For a component with potentially thousands of methods, this may become a nuisance.

  • If the inner component adds a new interface that could be useful to the client, the client never gets to see it, unless the code of the outer component is modified to incorporate the new interface methods.

These issues may or may not be a problem for the developer of the outer component. If all that is needed is isolation from the inner component, with exposure to specific methods, containment is a good technique for component reusability.

Aggregation

Looking at our implementation for containment, an optimization pops into your mind right away why bother writing wrappers for each interface method of the inner component? Why not just return the interface pointer to the inner component if a request for the interface is made, as shown in the following code:

 STDMETHODIMP CVcr::QueryInterface(REFIID iid, void** ppRetVal)  {   ...    if (IsEqualIID(iid, IID_IComponentVideo)) {     return m_pComponentVideo->QueryInterface(       IID_IComponentVideo, ppRetVal);    }    ...  } 

A simple trick, and we save lots of wrapper code. We deserve a pat on the back.

Such a mechanism of component reusage, where the outer component passes the inner component s interface pointer directly to the client, is called aggregation. Figure 3.3 illustrates this concept.

Figure 3.3. COM aggregation.
graphics/03fig03.gif

Don t gloat too much, however. The code fragment shown earlier violates a number of QueryInterface rules.

Consider the scenario where the client obtains two interface pointers from the VCR object ISVideo and IComponentVideo. Note that, in our server implementation, QI for ISVideo will go through the outer component and IComponentVideo will go through the inner component.

  • The object identity rule says that a QI for IUnknown on both the interface pointers should return the same pointer. However, as the QI on ISVideo goes through the outer component and the QI on IComponentVideo goes through the inner component, the returned IUnknown pointers are different. This violates the object identity rule.

  • If a QI for IVideo is made on the ISVideo interface pointer, it succeeds. However, if a QI for IVideo is made on the IComponentVideo pointer, it fails. This violates the symmetry rule.

The crux of the problem is that the client sees two different unknowns the inner IUnknown and the outer IUnknown. Each IUnknown implements its own QI and reference counting logic.

The client should see only one object identity, the identity of the outer component, and should never see the inner component s unknown. The inner and outer component should cooperate with each other in convincing the client that there is only one unknown and that an interface pointer on the inner component belongs to the outer component.

As the client may get a direct interface pointer to the inner component, the inner component should just delegate all the IUnknown calls to the outer component.

 class CInnerComponent : public IUnknown  { private:    IUnknown* m_pOuter;  public:    STDMETHOD(QueryInterface)(REFIID iid, void** pp)    {     return m_pOuter->QueryInterface(iid, pp);    }    STDMETHOD_(ULONG, AddRef)()    {     return m_pOuter->AddRef();    }    STDMETHOD_(ULONG, Release)()    {     return m_pOuter->Release();    }    ...  }; 

Such an implementation of IUnknown, which delegates all three method calls to some other implementation of IUnknown, is referred to as the delegating unknown.

There is still a catch in our aggregation logic. Consider the case when the client queries the outer object for an interface that belongs to the inner object. In this case, the outer object will forward the request to the inner object, which in turn delegates it back to the outer object. The process will get into a cycle that cannot be broken.

What this means is, when it comes to QueryInterface, the outer component cannot use the delegating unknown from the inner component. The inner component needs to provide a second implementation of the unknown an implementation that does not delegate calls back to the outer component.

Implementing the Inner Component

There are several ways to provide two distinct implementations of IUnknown from a single object. The most commonly used technique is for the inner component to implement two IUnknown interfaces. The non-delegating unknown implements IUnknown for the inner component in the usual way. The delegating unknown forwards IUnknown method calls to either the outer unknown (if aggregated) or the non-delegating unknown (if not aggregated).

The non-delegating IUnknown methods can be declared as three internal methods, as shown here:

 class CComponentVcr : public IComponentVideo  { public:        ...    // Non-delegating IUnknown calls    STDMETHOD(InternalQueryInterface)(REFIID iid, void** ppv);    STDMETHOD_(ULONG, InternalAddRef)();    STDMETHOD_(ULONG, InternalRelease)();  }; 

However, a non-delegating unknown is still an IUnknown type, and therefore has to satisfy the vtbl requirements of IUnknown.

One way to achieve the vtbl requirements is to have a member variable that maintains the binary layout of IUnknown, as shown in the following code fragment:

 class CComponentVcr : public IComponentVideo  { public:    ...    class CNDUnknown : public IUnknown    {   public:      ...      CComponentVcr* Object(); // helper function      STDMETHOD(QueryInterface)(REFIID iid, void** ppv)      {       return Object()->InternalQueryInterface(iid, ppv);      }      STDMETHOD_(ULONG, AddRef)()      {       return Object()->InternalAddRef();      }      STDMETHOD_(ULONG, Release)()      {       return Object()->InternalRelease();      }    };    CNDUnknown m_NDUnknown;    ...  }; 

In the above code, the helper method CNDUnknown::Object needs to return the pointer to the CComponentVcr instance. A simple C trick that can be used is to reconstruct the pointer to the CComponentVcr instance based on the knowledge that the member variable m_NDUnknown has a fixed memory that is offset from the beginning of its parent class, as shown here:

 class CNDUnknown : public IUnknown  { public:    ...    CComponentVcr* Object()    {     return reinterpret_cast<CComponentVcr*>(       reinterpret_cast<BYTE*>(this) -         offsetof(CComponentVcr, m_NDUnknown));    }    ... 

The non-delegating implementation of IUnknown, that is, methods InternalQueryInterface, InternalAddRef, and InternalRelease, are similar to the ones we have seen many times in earlier examples. The only difference is that, when requested for IUnknown, QueryInterface returns a pointer to the non-delegating IUnknown, and not this. The code fragment is shown below:

 STDMETHODIMP CComponentVcr::InternalQueryInterface(REFIID iid,    void** ppRetVal)  {   *ppRetVal = NULL;    if (IsEqualIID(iid, IID_IUnknown)) {     *ppRetVal = static_cast<IUnknown*>(&m_NDUnknown);    }else    if (IsEqualIID(iid, IID_IComponentVideo)) {     *ppRetVal = static_cast<IComponentVideo*>(this);    }    if (NULL != (*ppRetVal)) {     reinterpret_cast<IUnknown*>(*ppRetVal)->AddRef();      return S_OK;    }    return E_NOINTERFACE;  }  STDMETHODIMP_(ULONG) CComponentVcr::InternalAddRef()  {   return (++m_lRefCount);  }  STDMETHODIMP_(ULONG) CComponentVcr::InternalRelease()  {   ULONG lRetVal = ( m_lRefCount);    if (0 == lRetVal) {     delete this;    }    return lRetVal;  } 

The logic for delegating unknown implementation is straightforward. A member variable can be used to obtain the desired indirection to the actual IUnknown implementation.

 class CComponentVcr : public IComponentVideo  { private:    IUnknown* m_pUnkActual; // actual unknown to be invoked  public:    // IUnknown interface (delegating IUnknown)    STDMETHOD(QueryInterface)(REFIID iid, void** ppv)    {     return m_pUnkActual->QueryInterface(iid, ppv);    }    STDMETHOD_(ULONG, AddRef)()    {     return m_pUnkActual->AddRef();    }    STDMETHOD_(ULONG, Release)()    {     return m_pUnkActual->Release();    }  } 

Variable m_pUnkActual needs to point to either the outer unknown (in case of aggregation) or to the non-delegating unknown (if used as a standalone object). The code below defines an initialization method on the object. The parameter passed is the pointer to the outer object, in the case of aggregation, or NULL, if used as a stand-alone object.

 class CComponentVcr : public IComponentVideo  { public:    HRESULT Init(IUnknown* pUnkOuter);    ...  };  HRESULT CComponentVcr::Init(IUnknown* pUnkOuter)  {   if (NULL == pUnkOuter) {     m_pUnkActual = &m_NDUnknown;    }else {     m_pUnkActual = pUnkOuter;    }    return S_OK;  } 

So how would a component know if it is being instantiated as a standalone object or as an inner object? Obviously, this information has to be available in the IClassFactory::CreateInstance method so that the implementation can construct the object appropriately.

COM designers had already considered the possibility of component reuse using aggregation. Recall that the IClassFactory::CreateInstance method had three parameters:

 HRESULT CreateInstance([in] IUnknown* pUnk, [in] REFIID riid,    [out, iid_is(riid)] void** ppv); 

So far, we took the first parameter for granted and just passed a NULL value. Now, it is time for us to pass a nonNULL value as the first parameter.

COM specifies that, if IClassFactory::CreateInstance is invoked with a desire to aggregate, the first parameter should be a pointer to the outer object.

Recall that in the case of aggregation, the inner component needs to return a pointer to the non-delegating unknown. Also recall that, in our implementation of InternalQueryInterface, we return a non-delegating unknown only if the requested interface is IID_IUnknown. Otherwise, we would need to create a non-delegating vtbl layout for every interface that is requested.

To ease the development pain, COM further mandates that, in the case of aggregation, the requested interface be restricted to IID_IUnknown. That is, if the first parameter to CreateInstance is nonNULL, the second parameter has to be IID_IUnknown.

These specifications apply not only to IClassFactory::CreateInstance, but also to its corresponding API function, CoCreateInstance.

With these specifications, our function CreateInstance can be implemented as follows:

 STDMETHODIMP CComponentVcrClassObject::CreateInstance(   IUnknown* pUnkOuter,    REFIID riid,    void** ppV)  {   *ppV = NULL; // always initialize the return value    if ((NULL != pUnkOuter) && (IID_IUnknown != riid) ) {     return E_INVALIDARG;    }    CComponentVcr* pVCR = new CComponentVcr;    if (NULL == pVCR) {     return E_OUTOFMEMORY;    }    HRESULT hr = pVCR->Init(pUnkOuter);    if (FAILED(hr)) {     delete pVCR;      return hr;    }    hr = pVCR->InternalQueryInterface(riid, ppV);    if (FAILED(hr)) {     delete pVCR;    }    return hr;  } 

Note that what we return is the non-delegating version of the unknown. If a stand-alone object is created, this is certainly appropriate. If an aggregate is being created, this is necessary to ensure that the inner object is AddRefed, not the outer object.

ATL s Support for Implementing the Inner Component. By default, ATL assumes that a component will support aggregation. If the support for aggregation is not desired, it can be turned off by using an ATL-sup-plied macro, DECLARE_NOT_AGGREGATABLE, as shown in the following code fragment:

 class CVcr : ...  {   ...  DECLARE_NOT_AGGREGATABLE(CVcr)    ...  }; 
Implementing the Outer Component

Let s examine the implementation details of the outer component.

As you would expect, the outer component has to maintain a pointer to the IUnknown interface of the inner component.

 class CVcr : public IVideo, public ISVideo  { public:    CVcr(void);    HRESULT Init();    ~CVcr();    ...  private:    ...    IUnknown* m_pUnkInner;  }; 

The inner component can be instantiated during some initialization call and can be released in the destructor, as shown here:

 CVcr:: CVcr()  {   ...    m_pUnkInner = NULL;  }  HRESULT CVcr::Init()  {   HRESULT hr = ::CoCreateInstance(     CLSID_ComponentVCR,      GetRawUnknown(),      CLSCTX_ALL,      IID_IUnknown,      reinterpret_cast<void**>(&m_pUnkInner));    return hr;  }  CVcr::~CVcr()  {   if (NULL != m_pUnkInner) {     m_pUnkInner->Release();    }    ...  } 

When the client queries the interface that is supported by the inner component, the outer component can just forward the request to the inner component:

 STDMETHODIMP CVcr::QueryInterface(REFIID iid, void** ppRetVal)  {   ...    if (IsEqualIID(iid, IID_IComponentVideo)) {     return m_pUnkInner->QueryInterface(iid, ppRetVal);    }    ...  } 

Now you can pat yourself on your back. We just finished writing a COM server that supports aggregation.

ATL Support for the Outer Component. ATL simplifies the grunge work of dealing with the inner components. Following are the steps:

Step 1: Add a member variable to hold the inner component.

 class CVcr : ...  {   ...  private:    CComPtr<IUnknown> m_spUnkInner;  }; 

Step 2: Add the COM_INTERFACE_ENTRY_AUTOAGGREGATE macro to the interface map.

 class CVcr : ...  {   ...  BEGIN_COM_MAP(CVcr)    COM_INTERFACE_ENTRY(IVideo)    COM_INTERFACE_ENTRY(ISVideo)    COM_INTERFACE_ENTRY_AUTOAGGREGATE(__uuidof(IComponentVideo),      m_spUnkInner.p, __uuidof(ComponentVCR))  END_COM_MAP()  }; 

Step 3: Add the DECLARE_GET_CONTROLLING_UNKNOWN macro to the class.

 class CVcr : ...  {   ...  DECLARE_GET_CONTROLLING_UNKNOWN ()  }; 

That s it. With just three simple steps, ATL can automatically manage the inner component.

Aggregation Gotchas

In the previous code fragment, note that the initialization method of the outer object AddRef s the inner object. According to the reference counting rules, this is the right thing to do. However, in our code, the initialization method of the inner object does not AddRef the outer object, even though it duplicates the pointer to the outer object. This violates our reference counting rule #4. However, there is a reason behind the logic. Had the inner object called AddRef on the outer object, the reference count of the outer object would never go to zero, even after the client has released all the references to the object. Thus, the outer object, and therefore the inner object, will always stay in memory.

To avoid such an unbroken cycle in the case of aggregation, COM explicitly mandates that the outer object hold a reference-counted pointer to the inner object s non-delegating unknown and the inner object hold a non-reference-counted pointer to the IUnknown of the outer object.

What about violating the reference counting rule? Well, the above relationship between the outer and inner object is covered under the special knowledge clause of reference counting: as the lifetime of the inner object is a proper subset of the lifetime of the outer object, it is safe for the inner object not to reference-count the outer object.

A similar reference counting problem occurs when the inner object needs to communicate with the outer object. In order to do so, the inner object needs to call QueryInterface through the controlling IUnknown. This results in AddRefing the outer object. If the inner object holds the returned interface pointer as a data member, we once again get into an unbroken cycle.

To avoid such a problem, the inner object should acquire and release the interface pointer on demand, holding on to the pointer only as long as it is needed, as illustrated in the following code snippet:

 STDMETHODIMP CInner::SomeMethod()  {   ISomeInterface* pY = NULL;    m_pUnkOuter->QueryInterface(IID_ISomeInterface, (void**) &pY);    Use(pY);    pY->Release();  } 

Knowing that the lifetime of the inner object is less than the lifetime of the outer object, you can play another trick acquire and release the pointer once at initialization time, but continue to hold the pointer as a data member.

 STDMETHODIMP CInner::Initialize()  {   m_pUnkOuter->QueryInterface(IID_ISomeInterface,      (void**) &m_pY);    m_pY->Release();    // m_pY still points to the valid object.    // Use it whenever needed.  } 

However, this is a dangerous trick to play. If the outer object implements the requested interface as a tear-off interface, the call to Release will destroy the tear-off object, leaving variable m_pY pointing to an invalid object.


< BACK  NEXT >


COM+ Programming. A Practical Guide Using Visual C++ and ATL
COM+ Programming. A Practical Guide Using Visual C++ and ATL
ISBN: 130886742
EAN: N/A
Year: 2000
Pages: 129

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net