Understanding Tear-Off Interfaces

 < Free Open Study > 



Understanding "Tear-Off" Interfaces

Most often, when a coclass supports a given set of interfaces, the assumption is made that a client will ask, and ask often, for access to these interfaces. In other words, if we create a coclass supporting a set of interfaces such as {IDraw, IShapeEdit, IDoubleBuffer} we naturally assume clients will ask for them as they manipulate the object. However, some coclasses may support a subset of interfaces that are rarely accessed by clients. This behavior often happens as a coclass matures, and acquires the "latest and greatest" interfaces which may overshadow existing functionality. Of course, the ability to know which interfaces are accessed less frequently than others is more an art than a science (furthermore, this art may be nothing more than a developer's educated guess).

Imagine you have a coclass named CoMyATLExperience supporting the IAmDrowningInMacros interface. You created this interface when you first began to examine ATL, and created an interface to express your pain in a well-defined set of methods. Time has progressed, and you now love ATL macros. Given the laws of COM identity, you know that you are bound by the interface contract. Once a client is able to acquire an interface from your object via QueryInterface(), the client must always be able to acquire that same interface. This is often termed the "law of predictability."

Therefore, as existing clients expect to obtain an IAmDrowningInMacros pointer, you cannot remove this outdated interface from your ATL COM map. For the sake of this discussion, here is the ATL coclass in question:

// Here is a coclass that you suspect contains interfaces of little use to most clients. class ATL_NO_VTABLE CoMyATLExperience :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass< CoMyATLExperience,                      &CLSID_CoMyATLExperience >,      public IAmDrowningInMacros,          // Never used.      public IDontTrustFrameworks,         // Never used.      public IGuessThisIsOK,               // Used every now and then.      public IThinkILikeThis,              // Used every now and then.      public IAmAnATLJunkie,               // Often used.      public IOnlyOpenATLWorkspaces        // Often used. {      ... };

As you suspect most clients will only ask for the final two interfaces, you may wish to set up CoMyATLExperience in such a way that when an instance of this class is created, only the IAmAnATLJunkie and IOnlyCreateATLWorkspaces vPtrs are initialized. The remaining interfaces (which you assume to be of little use to most clients) will not have their vPtrs initialized until a client explicitly asks for access to these interfaces. The ability for a COM class to create vPtrs on demand can be visualized as "tearing off an interface," and hence the term tear-off interfaces.

Tear-off interfaces allow the coclass to be created more quickly (as fewer vPtrs need to spring to life) and leave a smaller code footprint if no clients are using the tear-offs (as vPtrs take up space, 4 bytes per interface).

A tear-off interface is implemented as a separate C++ class, and thus, the vPtr for less frequently used interfaces is not initialized until the object is created. The end result is avoiding overhead that may not actually be needed.

A Tear-Off Example in C++

Before we examine ATL's support for tear-off interfaces, it will be helpful to see how this might be set up in straight C++. To facilitate a simpler discussion than we would have with CoMyATLExperience, assume we have a C++ coclass named CoOldObject, which currently supports IOldInterface and INewFancyInterface.

The goal here is to implement IOldInterface as a tear-off using C++, as we suspect this interface is of little use to the outside world. Here is the IDL code describing each interface supported by CoOldObject:

// This interface has served its purpose. [ object, uuid(1A928153-238F-11D3-B8F8-0020781238D4)] interface IOldInterface : IUnknown {      HRESULT NoOneCallsAnymore(); }; // All the cool coclasses support this interface! [ object, uuid(362BD920-2397-11d3-B8F8-0020781238D4)] interface INewFancyInterface : IUnknown {      HRESULT IAmLovedByClients(); };

Here is the pre-tear-off implementation of CoOldObject. This is straight C++ COM, and should raise no eyebrows:

// This coclass supports two non tear-off interfaces. class CoOldObject : public IOldInterface, public INewFancyInterface { public: ...      STDMETHODIMP(NoOneCallsAnymore)()      {           MessageBox(NULL, "Someone DOES love me!",                     "I am so lonely", MB_OK | MB_SETFOREGROUND);      }      STDMETHODIMP(IAmLovedByClients)()      {           MessageBox(NULL, "EVERYONE loves me!",                     "I am so happy", MB_OK | MB_SETFOREGROUND);      }      STDMETHODIMP_(ULONG )AddRef()     {...}      STDMETHODIMP_(ULONG )Release()      {...}      STDMETHODIMP QueryInterface(REFIID riid, void** ppv)      {           if(riid == IID_INewFancyInterface)                *ppv = (INewFancyInterface *)this;           else if(riid == IID_IUnknown)                *ppv = (IUnknown*)( INewFancyInterface*)this;           else if(riid == IID_IOldInterface)                *ppv = (IOldInterface*)this;           else           {                *ppv = NULL;                return E_NOINTERFACE;           }           ((IUnknown*)(*ppv))->AddRef();           return S_OK;      } ... }; 

Implementing the Tear-Off Interface

A class exposing a tear-off interface is termed the owner class. The class implementing the outdated interface is termed the tear-off class. A separate C++ class will represent each interface you wish to expose as a tear-off. This should make sense, as we are trying to delay the creation of a vPtr until explicitly asked to do so. This tear-off class will derive from, and implement the methods of, the interface in question. The first step, then, is to create a new class implementing IOldInterface:

// This tear-off class implements the IOldInterface interface. class COldInterfaceTearOff : public IOldInterface { public:      // IOldInterface implementation.      STDMETHODIMP(NoOneCallsAnymore)()      {           MessageBox(NULL, "Someone DOES love me!",                     "I am so lonely", MB_OK | MB_SETFOREGROUND);           return S_OK;      } ... };

The way in which the tear-off class implements IUnknown is unique. Because a client may acquire a tear-off interface (such as IOldInterface), the client must be able to query for additional interfaces found on the owner (INewFancyInterface). In short, the tear-off must be able to query for interfaces implemented by the owner. To permit the tear-off class to call methods implemented by the owner (such as QueryInterface()), the tear-off class maintains a pointer back to the owning class (much like the nested class technique):

 // Forward declare owner class to quiet down the compiler. class CCoOldObject; // When the owner class creates a tear-off, it will pass in its own address which // we store in a private member variable. class COldInterfaceTearOff : public IOldInterface { private:      // A tear-off interface maintains a pointer back to the owner.      CoOldObject *m_pRealObject; public:      // The constructor of the tear-off sets the back pointer.      COldInterfaceTearOff(CoOldObject *pThis)           : m_pRealObject(pThis)      {           m_ref = 0;           // The constructor should call AddRef() on the 'real' object.           m_pRealObject -> AddRef();      }      ~ COldInterfaceTearOff()      {           // The destructor should call Release() on the 'real' object.           m_pRealObject -> Release();      }      STDMETHODIMP(NoOneCallsAnymore)()      {           MessageBox(NULL, "Someone DOES love me!",                     "I am so lonely", MB_OK | MB_SETFOREGROUND);           return S_OK;      } private:      ULONG m_ref;     // Tear-offs still maintain a reference count! }; 

As mentioned, it is important to ensure that the tear-off class is careful about its implementation of QueryInterface(). For example, if a client has a pointer to IOldInterface, they will expect to obtain any other interface the coclass supports:

// COldInterfaceTearOff does not implement INewFancyInterface! pOld -> QueryInterface(IID_INewFancyInterface, (void**)&pFancy);

We need to set up the QueryInterface() logic for the tear-off to give back a copy of an existing IOldInterface pointer and forward all other interface requests to the owner:

// Be sure your tear-off intercepts requests for the interface it implements! // Use the cached back pointer for all other queries. STDMETHODIMP COldInterfaceTearOff::QueryInterface(REFIID riid, void** ppv) {      // If they ask for any other interface than IID_IOldInterface, send the      // request back to the real object, because that is where that interface may      // be obtained.      if(riid != IID_IOldInterface)           return m_pRealObject->QueryInterface(riid, ppv);      // They want IID_IOldInterface, so just give back a reference to self.      if(riid == IID_IOldInterface)           *ppv = (IOldInterface*)this;      ... }

To finish up the tear-off class implementation, we would create a standard implementation for AddRef() and Release(). Tear-off classes are still bound to maintain a reference count and remove themselves when their reference count is at zero.

That completes the implementation of the tear-off object. To sum things up, a tear-off class will derive from the outdated interface in question, and provide a standard reference count. The constructor of the tear-off class will allow the class to maintain a reference to the owner, and use this interface pointer to redirect QueryInterface() calls.

Changing the Owner Coclass

As for the owner class, we will need to retrofit CoOldObject as follows:

  1. Remove IOldInterface from CoOldObject's inheritance chain. The owner class never directly derives from the tear-off interface:

    // The creatable class should not derive from the tear-off interface. class CoOldObject : public /* IOldInterface, */ INewFancyInterface {      ... }; 

  2. Declare COldInterfaceTearOff to be a friend class of CoOldObject. We need to ensure the tear-off class is able to access methods of the owner class:

    // The tear-off interface should be declared as a friend of the 'real' class. class CoOldObject : public /* IOldInterface, */ INewFancyInterface { ... private:      friend class COldInterfaceTearOff; };

  3. Reengineer CoOldObject's implementation of QueryInterface() to create a new instance of the tear-off class if (and only if) the requested IID is indeed IID_IOldInterface:

    // The owner class should dynamically allocate the tear-off class. STDMETHODIMP CoOldObject::QueryInterface(REFIID riid, void** ppv) {      // Give out non tear-off references as usual.      if(riid == IID_IUnknown)           *ppv = (IUnknown*)( INewFancyInterface*)this;      else if(riid == IID_INewFancyInterface)           *ppv = ( INewFancyInterface *)this;      // If the user asks for IOldInterface, create a new instance of the tear-off.      else if(riid == IID_IOldInterface)           *ppv = (IOldInterface*)new COldInterfaceTearOff(this);      else      {           *ppv = NULL;           return E_NOINTERFACE;      }      ((IUnknown*)(*ppv))->AddRef();      return S_OK; }

One problem with this implementation of QueryInterface() is that it will create a new instance of the tear-off object whenever IID_IOldInterface is queried for. A better solution would be to cache the initial COldInterfaceTearOff class, and hand out additional references accordingly. ATL provides a macro for this very behavior, as we will see shortly.

Using the Tear-Off

So what is the end result of this extra effort? As mentioned, our coclass now is able to save 4 bytes for each tear-off interface it supports. In this very simple example, the extra work to implement a tear-off may not be worth it. Nevertheless, here is some client code accessing this revised CoOldObject coclass. Notice how the client code is completely oblivious to the fact that the coclass is dynamically creating an IOldInterface reference:

// Tear-off clients have no clue they are using a tear-off interface. // (assume proper #includes for GUIDs and interface definitions). int main(int argc, char* argv[]) {      CoInitialize(NULL);      IOldInterface* pOld = NULL;      INewFancyInterface * pNew = NULL;      HRESULT hr;      hr = CoCreateInstance(CLSID_CoOldObject, NULL, CLSCTX_SERVER,                          IID_ INewFancyInterface, (void**)&pNew);      if(SUCCEEDED(hr))      {           pNew->IAmLovedByClients();           // Coclass will dynamically create the tear off.           hr = pNew->QueryInterface(IID_IOldInterface, (void**)&pOld);           pNew->Release();           if(SUCCEEDED(hr))           {                pOld->NoOneCallsAnymore();                pOld->Release();           }      }      CoUninitialize();      return 0; }

Creating a Tear-Off Interface Using ATL

ATL does offer framework support to implement tear-off interfaces. This support comes by way of two COM map macro entries and a few new ATL templates. To see how to use these new items, we will reengineer the previous raw C++ example using ATL.

When you wish to create a tear-off class with ATL, you will begin by creating a new class deriving from CComTearOffObjectBase<>. The parameters given to this template are the name of the owner class and the tear-off's threading model. Here is the full definition defined in <atlcom.h>:

// Your tear-off interface class will derive from CComTearOffObjectBase<>. template <class Owner, class ThreadModel = CComObjectThreadModel> class CComTearOffObjectBase : public CComObjectRootEx<ThreadModel> { public:      typedef Owner _OwnerClass;      CComObject<Owner>* m_pOwner;      CComTearOffObjectBase() {m_pOwner = NULL;} };

Notice how the CComTearOffObjectBase<> template already holds onto a pointer to the owner class. The tear-off class makes use of m_pOwner to access data members and methods of the owner class. This is very helpful when your tear-off needs data maintained in the owner to help implement methods of its own (we will see this in action in the next lab).

A tear-off class built using ATL will also derive from the COM interface that it is representing (in this case IOldInterface) and implement the interface methods. Finally, you will need to provide a standard COM map, with an entry for the tear-off interface. Here is the ATL version of the tear-off class:

 // Still need to forward declare owner to shut up compiler. class CCoOldObject; // The ATL version of a tear-off class. class ATL_NO_VTABLE CoOldInterfaceTearOff : public CComTearOffObjectBase<CCoOldObject, CComSingleThreadModel>, public IOldInterface     { public:      STDMETHOD(NoOneCallsAnymore)()           MessageBox(NULL, "Someone DOES love me!",                     "I am so lonely", MB_OK | MB_SETFOREGROUND);           return S_OK;      } BEGIN_COM_MAP(CoOldInterfaceTearOff)      COM_INTERFACE_ENTRY(IOldInterface) END_COM_MAP() };

Implementing the Owner Class in ATL

As mentioned, ATL provides two COM map macros to support tear-off interfaces. Let's start with COM_INTERFACE_ENTRY_TEAR_OFF. This macro requires two parameters: the GUID of the interface and the name of the CComTearOffObjectBase<> derived class that supports it (note the call to CComObjectRootBase::_Creator() ):

// Used to create a non-cached tear-off interface. #define COM_INTERFACE_ENTRY_TEAR_OFF(iid, x)\      {&iid,\      (DWORD)&_CComCreatorData<\           CComInternalCreator< CComTearOffObject< x > >\           >::data,\      _Creator},

To create a tear-off owner class in ATL, you will still take care not to derive from the outdated interface (such as IOldInterface). You will still need to declare the tear-off class as a friend of the owning class. The good news is that the new COM map macros will wrap up all the inner logic necessary to keep QueryInterface() functioning correctly. Here is the ATL version of the CoOldObject coclass:

// ATL's version of CoOldObject class ATL_NO_VTABLE CCoOldObject :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoOldObject, &CLSID_CoOldObject>,      // public IOldObject, not anymore!      public INewFancyInterface { public: ... // Your COM map will now make use of the tear-off macro. BEGIN_COM_MAP(CCoOldObject)      COM_INTERFACE_ENTRY(INewFancyInterface)      COM_INTERFACE_ENTRY_TEAR_OFF(IID_IOldInterface, CoOldInterfaceTearOff) END_COM_MAP() public:      STDMETHOD(IAmLovedByClients)();      friend class CoOldInterfaceTearOff; };

The COM_INTERFACE_ENTRY_CACHED_TEAR_OFF Macro

Like the previous C++ tear-off example, there is one slight problem with the current implementation of the owner class. COM_INTERFACE_ENTRY_TEAR_OFF is designed to create a new instance of the tear-off class each time a client queries for the interface in question. This behavior happens regardless of whether or not the client currently holds a valid IOldInterface pointer. For example, if a Visual Basic client were to acquire two references to IOldInterface, the coclass creates two instances of the tear-off class:

' Grab two references to IOldInterface. ' Private Sub btnDoIt_Click()      Dim o As New CoOldObject      o.IAmLovedByClients      Dim i As IOldInterface      Set i = o                ' Creates a tear-off.      i.NoOneCallsAnymore      Dim i2 As IOldInterface      Set i2 = o               ' Creates another tear-off.      i.NoOneCallsAnymore End Sub

To prevent this behavior, ATL provides an alternative tear-off macro named COM_INTERFACE_ENTRY_CACHED_TEAR_OFF. Using this interface, COM_INTERFACE_ENTRY_CACHED_TEAR_OFF can check if a previous version of the tear-off exists. If so, a new instance of the tear-off will not be created, but the existing instance will be referenced again.

This macro requires an extra parameter beyond the IID of the outdated interface and name of the tear-off class. You must also specify the IUnknown interface of the owner. Notice how this alternative macro makes use of CComCachedTearOffObject<>:

// This time create the tear-off class using CComCachedTearOffObject. #define COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(iid, x, punk)\      {&iid,\      (DWORD)&_CComCacheData<\           CComCreator< CComCachedTearOffObject< x > >,\           (DWORD)offsetof(_ComMapClass, punk)\           >::data,\      _Cache},

The call to CComObjectRootBase::_Cache() ensures only a single instance of the tear-off class is created:

// CComObjectRootBase helper function. static HRESULT WINAPI _Cache(void* pv, REFIID iid,                           void** ppvObject, DWORD dw) {      HRESULT hRes = E_NOINTERFACE;      _ATL_CACHEDATA* pcd = (_ATL_CACHEDATA*)dw;      IUnknown** pp = (IUnknown**)((DWORD)pv + pcd->dwOffsetVar);      if (*pp == NULL)           hRes = pcd->pFunc(pv, IID_IUnknown, (void**)pp);      if (*pp != NULL)           hRes = (*pp)->QueryInterface(iid, ppvObject);      return hRes; } 

Specifying the Owner's IUnknown

For a cached tear-off to work correctly, the tear-off object needs to communicate with the owner using an IUnknown pointer. DECLARE_GET_CONTROLLING_UNKNOWN is an ATL macro that expands to define a method named GetControllingUnknown(). This method is used to calculate the offset to the IUnknown pointer for an ATL coclass:

// ATL helper macro which returns your coclass's IUnknown* #define DECLARE_GET_CONTROLLING_UNKNOWN() public:\ virtual IUnknown* GetControllingUnknown() {return GetUnknown();}

CComCachedTearOffObject calls GetControllingUnknown(), and stores the owner's IUnknown pointer in the m_pOwner member variable defined by CComTearOff- ObjectBase<>:

// To create a cached tear-off, we need to supply a reference to the outer IUnknown. template <class contained> class CComCachedTearOffObject :      public IUnknown,      public CComObjectRootEx<contained::_ThreadModel::ThreadModelNoCS> { public:      typedef contained _BaseClass;      CComCachedTearOffObject(void* pv) :           m_contained(((contained::_OwnerClass*)pv)->                        GetControllingUnknown())      {           ATLASSERT(m_contained.m_pOwner == NULL);           m_contained.m_pOwner =                reinterpret_cast<CComObject<contained::_OwnerClass>*>(pv);      } ... }

So then, by adding the DECLARE_GET_CONTROLLING_UNKNOWN macro to your owner's class definition, CComCachedTearOffObject can communicate with the owner class to see if a previous instance of the tear-off exists.

Here is the updated ATL owner class making use of a cached tear-off interface. The implementation of the tear-off class itself is unchanged. However, the owner must directly pass in its own IUnknown to the tear-off using the GetControllingUnknown() method, and Release() it accordingly during the owner's FinalRelease() call:

// A cached tear-off à la ATL. class ATL_NO_VTABLE CCoOldObject :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CCoOldObject, &CLSID_CoOldObject>,      public INewFancyInterface { public:      CCoOldObject() : m_pUnk(NULL)      { } ... // This will insert the GetControllingUnknown() method into our class. DECLARE_GET_CONTROLLING_UNKNOWN() // Make use of a cached tear-off. BEGIN_COM_MAP(CCoOldObject)      COM_INTERFACE_ENTRY(INewFancyInterface)      COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(IID_IOldInterface,                CoOldInterfaceTearOff, m_pUnk) END_COM_MAP() void FinalRelease() {      if(m_pUnk)           m_pUnk->Release(); } public:      STDMETHOD(IAmLovedByClients)();      friend class CoOldInterfaceTearOff;      // Used to hold the IUnknown pointer.      IUnknown* m_pUnk; }; 

So with this, a single instance of the tear-off class is used to represent any number of interface references.

Think You're Sold on the Tear-Off?

Before you retrofit all your coclasses to use tear-off interfaces, you must be absolutely sure the offending interfaces are truly rarely used. The reason is simple: When a client does use a tear-off, your server's total size will swell by 12 bytes! That's 4 for the tear-off interface vPtr plus 4 for the tear-off class plus 4 for the owner's IUnknown pointer (not to mention the reference counter used by the tear-off).

The reasoning behind using tear-offs is that if we are willing to gamble that an interface has served its usefulness, we could save 4 bytes. Understand that if a client really does use the interface you have paid a greater penalty than you would have by just supporting the out-of-fashion interface in the first place.

Always remember that this technique is nothing more than a way to attempt to create a smaller, faster COM object and is always optional. You need to decide which (if any) interfaces may need to be implemented as a tear-off, and if you choose to do so, write the CComTearOffObjectBase<> derived class, and use one of the two COM map macros in the owner class.

That wraps up our discussion of tear-off interfaces, and how ATL can make it easier. The next lab will allow you to try each approach to tear-offs (cached and non-cached) on your own.

Lab 8-3: Building a Tear-Off Interface Using ATL

Even though tear-offs do have a dark side there are times when they can help build more efficient coclasses. As mentioned, as a coclass matures, interfaces that seemed like a good idea at the time may be replaced by the latest and greatest versions. In this lab you will be taking your current ATL CoCar developed in Chapter 6 and implement a tear-off, resulting in a more efficient manner to create your CoCar.

 On the CD   The solution for this lab can be found on your CD-ROM under:
Labs\Chapter 08\ATLTearOff
Labs\Chapter 08\ATLTearOff \VB Client

Step One: Add a New Interface to CoCar

As objects mature (and as COM developers learn better tricks) it is possible that interfaces which seemed like a good idea (or good design) at the time are now considered outdated. Open a copy of your ATLCoCar project (Lab 6-1) and examine the interface methods your coclass currently supports:

 // IOwnerInfo      STDMETHOD(get_Address)(/*[out, retval]*/ BSTR *pVal);      STDMETHOD(put_Address)(/*[in]*/ BSTR newVal);      STDMETHOD(get_Name)(/*[out, retval]*/ BSTR *pVal);      STDMETHOD(put_Name)(/*[in]*/ BSTR newVal); // IEngine      STDMETHOD(GetCurSpeed)(/*[out, retval]*/ int* curSpeed);      STDMETHOD(GetMaxSpeed)(/*[out, retval]*/ int* maxSpeed);      STDMETHOD(SpeedUp)(); // IStats      STDMETHOD(GetPetName)(/*[out, retval]*/ BSTR* petName);      STDMETHOD(DisplayStats)(); // ICreateCar      STDMETHOD(SetMaxSpeed)(/*[in]*/ int maxSp);      STDMETHOD(SetPetName)(/*[in]*/ BSTR petName);

Notice that the ICreateCar interface defines two methods to set the attributes of a CoCar, and IOwnerInfo defines two properties to set the information of the owner. Therefore, it takes four calls (on two interfaces) to set the state of an ATLCoCar object. This is not a very optimized approach. For example, what if your CoCar was part of a distributed system? That would mean four RPC calls would be required to set up a single object. ICreateCar (and IOwnerInfo, for that matter) might be a good candidate for a tear-off interface.

Let's modify the existing Chapter 6 lab to support a tear-off interface for ICreateCar, and introduce a new interface that allows a COM client to set the state of a CoCar in a single round trip. Begin by typing in the IDL code for a new COM interface named ICreate. Use the Implement Interface Wizard to support ICreate, and then add a single method named Create(). Here is the IDL:

// ICreate allows the user to set the state of the CoCar in a single round trip. [ object,uuid(1FB21BF0-3CCE-11d3-B910-0020781238D4), oleautomation, helpstring("ICreate Interface"), pointer_default(unique) ] interface ICreate : IUnknown {      [helpstring("This is the NEW Create")]      HRESULT Create([in] BSTR ownerName, [in] BSTR ownerAddress,                    [in] BSTR petName, [in] int maxSp); }; ... coclass ATLCoCar {      [default] interface ICreate;           // Let's make the new ICreate the [default]      interface ICreateCar;      interface IStats;      interface IEngine;      interface IOwnerInfo; }; 

Your COM map should now look something like this:

BEGIN_COM_MAP(CATLCoCar)      COM_INTERFACE_ENTRY(ICreateCar)      COM_INTERFACE_ENTRY(IStats)      COM_INTERFACE_ENTRY(IEngine)      COM_INTERFACE_ENTRY(IOwnerInfo)      COM_INTERFACE_ENTRY(ICreate) END_COM_MAP()

To implement your Create() method, you can just make use of your current private data members. Take the incoming parameters and assign them to your member variables:

// This one call does it all! STDMETHODIMP CATLCoCar::Create(BSTR ownerName, BSTR ownerAddress,                            BSTR petName, int maxSp) {      m_petName = petName;      m_ownerName = ownerName;      m_ownerAddress = ownerAddress;      if(maxSp < MAX_SPEED)           m_maxSpeed = maxSp;      return S_OK; }

Go ahead and compile to be sure you have not introduced any errors.

Now that we have a more efficient way to allow clients to establish the state of our ATL CoCar, we need to retrofit the outdated ICreateCar into a tear-off interface.

Step Two: Create the Tear-Off Class

Insert a new header file into your project named ICC_TearOff.h and an implementation file named ICC_TearOff.cpp. In your header file, create a new class named CCreateCar_TearOff, which derives from CComTearOffObjectBase<> as well as the outdated ICreateCar. To keep the compiler happy, you will need to forward declare your owner class before the tear-off definition:

// Need to forward declare to keep the compiler hushed up. class CATLCoCar; class ATL_NO_VTABLE CCreateCar_TearOff :      public CComTearOffObjectBase<CATLCoCar, CComSingleThreadModel>,      public ICreateCar { };

Next, create a COM map for the class, and support the ICreateCar interface using the COM_INTERFACE_ENTRY macro:

// Tear-off objects only return an interface pointer for the outdated interface. BEGIN_COM_MAP(CCreateCar_TearOff)      COM_INTERFACE_ENTRY(ICreateCar) END_COM_MAP()

Add prototypes for the methods of ICreateCar and override FinalConstruct() and FinalRelease(), placing a message in each method. This will be helpful when we examine the differences between a standard and cached tear-off:

// Just a simple test. HRESULT FinalConstruct() {      MessageBox(NULL, "Created tear-off", "Notice!",                 MB_OK | MB_SETFOREGROUND);      return S_OK; } void FinalRelease() {      MessageBox(NULL, "The tear-off is destroyed", "Notice!",                 MB_OK | MB_SETFOREGROUND); } // Methods of outdated ICreateCar. STDMETHOD(SetMaxSpeed)(int maxSp); STDMETHOD(SetPetName)(BSTR petName);

In your implementation file, we need to supply code for SetMaxSpeed() as well as SetPetName(). Recall that CComTearOffObjectBase<> maintains a pointer to the owner, which is stored in the m_pOwner member variable. Use this data member to call the correct method of the owner class:

#include "stdafx.h" #include "icc_tearoff.h" #include "atlcocar.h" // Use the pointer to your parent to set the state. STDMETHODIMP CCreateCar_TearOff::SetMaxSpeed(int maxSp) {      if(maxSp < MAX_SPEED)           m_pOwner->m_maxSpeed = maxSp;      return S_OK; } STDMETHODIMP CCreateCar_TearOff::SetPetName(BSTR petName) {      return m_pOwner->SetPetName(petName); }

This finishes up the tear-off class. If you wish, you may also create a tear-off for the IOwnerInfo interface as well, given that ICreate overshadows its functionality.

Step Three: Update the Owner Class

The coclass exposing the tear-off does not derive from the interface in question, but will dynamically create an instance of the tear-off class when necessary. Open your CATLCoCar.h file, remove ICreateCar from your inheritance list, and remove the COM map entry for this interface:

// Owner classes do not derive from the interface implemented by a tear-off. class ATL_NO_VTABLE CATLCoCar :      public CComObjectRootEx<CComSingleThreadModel>,      public CComCoClass<CATLCoCar, &CLSID_ATLCoCar>,      // public ICreateCar,      public IStats,      public IEngine,      public IOwnerInfo,      public ICreate { ... BEGIN_COM_MAP(CATLCoCar)      // COM_INTERFACE_ENTRY(ICreateCar)      COM_INTERFACE_ENTRY(IStats)      COM_INTERFACE_ENTRY(IEngine)      COM_INTERFACE_ENTRY(IOwnerInfo)      COM_INTERFACE_ENTRY(ICreate) END_COM_MAP() ... }; 

Next, to allow your tear-off to access the private sector of your class, declare a friendship with the tear-off (you may place the following anywhere within your class declaration):

// Allow CCreateCar_TearOff to access the private data of CATL_CoCar. friend class CCreateCar_TearOff;

Finally, we need to update CATLCoCar's COM map to support a tear-off version of ICreateCar. We will make use of the COM_INTERFACE_ENTRY_TEAR_OFF macro to create a non-cached solution. Edit the COM map by adding the following entry:

// Send in the GUID of the outdated interface and the name of the tear-off class. COM_INTERFACE_ENTRY_TEAR_OFF(IID_ICreateCar, CCreateCar_TearOff)

Go ahead and compile your new version of the ATL CoCar. If you are error free, you are ready to create a client and see the pitfalls of a non-cached tear-off.

Note 

Remember! The very first entry of your COM map must be a "simple" entry to allow ATL to calculate the offset to your IUnknown interface. Don't list the tear-off macro as the first entry!

Step Four: A Non-Cached Tear-Off Client

Assemble a simple Visual Basic client to test your new ICreate interface. Recall this new interface allows the user to set the state of the coclass in a single call. I am sure you can hack together a functional GUI by now, so here is the code in question:

' [General][Declarations] ' Private mCar As ATLCoCar Private mitfStats As ATLTEAROFFLib.IStats Private mitfOwner As IOwnerInfo Private Sub btnCreate_Click()      Set mCar = New ATLCoCar      mCar.Create txtOwnerName, txtOwnerAddress, txtPetName, txtMaxSpeed End Sub Private Sub btnStats_Click()      Set mitfStats = mCar      mitfStats.DisplayStats      Set mitfOwner = mCar      MsgBox mitfOwner.Name, , "Owner Name"      MsgBox mitfOwner.Address, , "Owner Address" End Sub

Now, exercise the tear-off. You should see the messages from the tear-offs FinalConstruct() and FinalRelease():

' Use the tear-off. ' Private Sub btnOldWay_Click()      Dim oldCar As New ATLCoCar      Dim itfCCar As ICreateCar      Set itfCCar = oldCar          ' Create tear-off interface.      itfCCar.SetMaxSpeed 40      itfCCar.SetPetName "JoJo" End Sub

Recall that non-cached tear-offs spawn a new instance of the class for each reference. To illustrate this behavior, set a second reference to the ICreateCar interface:

' Set another reference to the tear-off. ' Private Sub btnOldWay_Click()      Dim oldCar As New ATLCoCar      Dim itfCCar As ICreateCar      Set itfCCar = oldCar          ' Create tear-off interface.      itfCCar.SetMaxSpeed 40      itfCCar.SetPetName "JoJo"      Dim itfCCar2 As ICreateCar      Set itfCCar2 = oldCar          ' Create another tear-off interface.      itfCCar2.SetPetName "JoJo Jr." End Sub

When you run the program, you will see two message boxes pop up from FinalConstruct() and FinalRelease(), as you have indirectly created two tear-offs! To change this behavior, we need to set up CoCar to make use of a cached tear-off.

Step Five: A Cached Tear-Off

Recall that when we wish to work with a cached tear-off, we must be sure to use the DECLARE_GET_CONTROLLING_UNKNOWN macro in the owner class. This macro expands to define the GetControllingUnknown() method for your coclass, which is accessed during the creation of your cached tear-off. To update your owner coclass, following these steps:

  1. Add the DECLARE_GET_CONTROLLING_UNKNOWN macro in your header file.

  2. Declare a public IUnknown* member variable and initialize it to NULL in your constructor. In your coclass's FinalRelease() method, release this IUnknown pointer.

  3. Remove the COM_INTERFACE_ENTRY_TEAR_OFF entry from your COM map, and replace it with a COM_INTERFACE_ENTRY_CACHED_TEAR_OFF entry:

    // Use a cached tear-off! COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(IID_ICreateCar,                                CCreateCar_TearOff,                                m_pUnk)

Your tear-off class will require no changes. Go ahead and recompile and test with your VB client once again. This time you will see only a single tear-off instance has been created, even though you have two references to ICreateCar.



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

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