ATL Tear-Off Interfaces

If an interface of an object is not used often, it might be more efficient to delay the creation of the interface until it is actually requested, especially if the initialization of the object is expensive. The interface will be created only when a client needs it, and it will be released after the client is finished using the interface. In other words, the code for the interface will be instantiated only when the client explicitly requests the interface by calling QueryInterface. Such an interface is known as a tear-off interface.

A tear-off interface maintains its own reference counting, through the CComTearOffObjectBase ATL class. Note that this is not entirely consistent with the usual COM model where the object maintains an object-wide reference count. In implementing the tear-off interface, you actually need to implement it in a separate C++ class, and instantiate this class separately from the main object class. Also, therefore, the tear-off interface doesn't feature in the multiple inheritance of the main implementation class.

If a tear-off interface takes a long time to get created, it could be inefficient to release it each time the reference count becomes zero. If a tear-off interface is likely to be called more than once, you can create it as a cached tear-off interface. The interface pointer of such an interface is cached and returned the next time a client asks for the interface.

In the following example, we will create two types of tear-off interface: a regular tear-off and a cached tear-off.
  1. Create a new ATL COM AppWizard project. Call it Ripper. Everything default. Insert a new ATL Object. Use the short name OuterObject. Everything default. Add a property to the IOuterObject interface, called Name, with the parameter list shown below - make it a 'get' only property. This is an arbitrary property - we'll use it for testing by outputing simple strings:
    [out, retval] BSTR *pVal
    
  2. Implement the get_Name method to create and allocate a string:
    *pVal = ::SysAllocString(L"IOuterObject");
    return S_OK;
    
  3. Now for the regular tear-off. We need to declare a separate class, but for convenience we'll put it in the OuterObject header file: at the top, declare a new class to manage the reference counting for the first tear-off interface. This class will support a dual interface, and have a normal COM map. However, it must derive from CComTearOffObjectBase and not CComObjectRootEx:
    class CTearOff1:
     public IDispatchImpl,
     public CComTearOffObjectBase
    {
    public:
     CTearOff1(){}
     ~CTearOff1(){}
    
    BEGIN_COM_MAP(CTearOff1)
      COM_INTERFACE_ENTRY(ITearOff1)
    END_COM_MAP()
    };
    
    Note: for this to compile, you'll have to forward-declare the COuterObject class just before the CTearOff1 class.
  4. Add the corresponding entries to the IDL file, as shown below (and don't forget to list the tear-off interface in the coclass):
     
    [
     object,
     uuid(9B8A71F7-DB54-11CF-9462-00AA00BBAD7F),
     dual,
     helpstring("ITearOff1 Interface"),
     pointer_default(unique)
    ]
    interface ITearOff1 : IDispatch
    {
    };
    
  5. Next, add a Name get property to this tear-off interface, with exactly the same signature and implementation as the Name property in the outer interface (but creating a different string):
    *pVal = ::SysAllocString(L"ITearOff1");
    
  6. Update the outer object's COM map with the special ATL macro shown below. Then build the server.
    COM_INTERFACE_ENTRY_TEAR_OFF(IID_ITearOff1, CTearOff1)
    

    Just step aside for a minute, and note the definition of this macro:

    #define COM_INTERFACE_ENTRY_TEAR_OFF(iid, x)\
     {&iid,\
      (DWORD)&_CComCreatorData<\
       CComInternalCreator< CComTearOffObject< x > >\
      >::data,\
      _Creator},
    

    As you can see, when QueryInterface is called, this entry in the COM map will call CComObjectBase::_Creator, passing _CComCreatorData>>::data as a parameter. This data is an _ATL_CREATORDATA, which is a struct that has only one member, an _ATL_CREATORFUNC*, which is just a typedefed function pointer, and data will be initialized to CreateInstance. The definitions are in atlcom.h and atlbase.h:

    typedef HRESULT (WINAPI _ATL_CREATORFUNC)(void* pv, 
    REFIID riid, LPVOID* ppv);
    
    struct _ATL_CREATORDATA
    {
     _ATL_CREATORFUNC* pFunc;
    };
    
    template <class Creator>
    class _CComCreatorData
    {
    public:
     static _ATL_CREATORDATA data;
    };
    
    template <class Creator>
    _ATL_CREATORDATA _CComCreatorData<Creator>::data = 
    {Creator::CreateInstance};
    
    struct _ATL_CACHEDATA
    {
     DWORD dwOffsetVar;
     _ATL_CREATORFUNC* pFunc;
    };
    
    // ...
    
    static HRESULT WINAPI _Creator(void* pv, REFIID iid, 
    void** ppvObject, DWORD dw)
    {
     _ATL_CREATORDATA* pcd = (_ATL_CREATORDATA*)dw;
     return pcd->pFunc(pv, iid, ppvObject);
    }
    

    So, QueryInterface calls _Creator, which calls CreateInstance, which calls new to instantiate a new CComTearOffObject:

    template <class T1>
    class CComInternalCreator
    {
    public:
     static HRESULT WINAPI CreateInstance(void* pv, 
                                          REFIID riid, 
                                          LPVOID* ppv)
     {
      ATLASSERT(*ppv == NULL);
      HRESULT hRes = E_OUTOFMEMORY;
      T1* p = NULL;
      ATLTRY(p = new T1(pv))
      if (p != NULL)
      {
       p->SetVoid(pv);
       p->InternalFinalConstructAddRef();
       hRes = p->FinalConstruct();
       p->InternalFinalConstructRelease();
       if (hRes == S_OK)
       hRes = p->_InternalQueryInterface(
       riid, ppv);
       if (hRes != S_OK)
       delete p;
      }
      return hRes;
     }
    };
    

    Bearing in mind ATL's upside-down inheritance, you won't be surprised to see that the object that actually gets instantiated at the bottom of the hierarchy is a CComTearOffObject instead of the more usual CComObject. Also, instead of the usual CComObjectRootEx, the base for this is CComTearOffObjectBase as shown below (although, of course, CComTearOffObjectBase itself is derived from CComObjectRootEx). From this you'll see that the tear-off interface object caches a pointer to its owner object. In your code, you might use this pointer to call into the members of the owning object for your own purposes.

    template <class Owner, class ThreadModel = CComObjectThreadModel>
    class CComTearOffObjectBase : public CComObjectRootEx<ThreadModel>
    {
    public:
     typedef Owner _OwnerClass;
     CComObject<Owner>* m_pOwner;
     CComTearOffObjectBase() {m_pOwner = NULL;}
    };
    

    The CComTearOffObject class uses this pointer to forward requests for the IUnknown methods (after having taken care of its own reference count):

    template <class Base>
    class CComTearOffObject : public Base
    {
    public:
     CComTearOffObject(void* pv)
     {
      ATLASSERT(m_pOwner == NULL);
      m_pOwner = reinterpret_cast< CComObject < 
      Base::_OwnerClass > *>(pv);
      m_pOwner->AddRef();
     }
    
     // Set refcount to 1 to protect destruction
     ~CComTearOffObject()
     {
      m_dwRef = 1L;
      FinalRelease();
      m_pOwner->Release();
     }
    
     STDMETHOD_(ULONG, AddRef)() {return InternalAddRef();}
     STDMETHOD_(ULONG, Release)()
     {
      ULONG l = InternalRelease();
      if (l == 0)
      delete this;
      return l;
     }
    
     STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
     {
      return m_pOwner->QueryInterface(iid, ppvObject);
     }
    };
    
  7. Now write a client: a console project, called TestRipper. Above main, #import the Ripper type library (with no namespace, and named guids). Inside main, use CoInitialize/CoUninitialize as normal, and set up a try..catch block. In the try block, declare and initialize an IOuterObject smart pointer object. Use this smart pointer to call the GetName wrapper function, and print out the results, eg:
    IOuterObjectPtr pOuter(CLSID_OuterObject);
    BSTR b1 = pOuter->GetName();
    _tprintf(_T("%S\n"), b1);
    SysFreeString(b1);
    
  8. Build and test. Then, write additional client code to test the tear-off, eg:
    ITearOff1Ptr pT1 = pOuter;
    b1 = pT1->GetName();
    _tprintf(_T("%S\n"), b1);
    SysFreeString(b1);
    
  9. Finally, write a second tear-off - a cached tear-off called CTearOff2. Set this up in exactly the same way as the first tear-off - you can copy+paste the IDL code and the C++ code, replacing TearOff1 with TearOff2. Implement the Name get property as before, but with an appropriate different string. The first difference for a cached tear-off is in the outer object's COM map. You need this macro instead of the regular one:
    COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(IID_ITearOff2, 
                                        CTearOff2, 
                                        m_pUnkTearOff2.p)
    

    Where m_pUnkTearOff2 is a public data member in the outer object class of the CComPtr ATL interface pointer wrapper class, eg:

    CComPtr<IUnknown> m_pUnkTearOff2;
    
  10. The cached tear-off macro uses the CComCachedTearOffObject ATL class, and this makes use of the GetControllingUnknown function, so you also need to add this macro to the outer object class:
    DECLARE_GET_CONTROLLING_UNKNOWN()
    

    Again, take a moment to reflect on the ATL macros for cached tear-offs:

    #define COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(iid, \
                                                x, \
                                                punk)\
     {&iid,\
     (DWORD)&_CComCacheData<\
     CComCreator< CComCachedTearOffObject< x > >,\
     (DWORD)offsetof(_ComMapClass, punk)\
     >::data,\
     _Cache},
    

    An important part of this macro is the CComCacheData, which is defined as follows:

    struct _ATL_CACHEDATA
    {
     DWORD dwOffsetVar;
     _ATL_CREATORFUNC* pFunc;
    };
    
    template <class Creator, DWORD dwVar>
    class _CComCacheData
    {
    public:
     static _ATL_CACHEDATA data;
    };
    
    template <class Creator, DWORD dwVar>
    _ATL_CACHEDATA _CComCacheData<Creator, dwVar>::data = 
    {dwVar, Creator::CreateInstance};
    

    So, what this does is make the data an _ATL_CACHEDATA, which contains 2 members - a creator function pointer and also an offset. This offset is from the base of the owner class to the member data that will be used to cache the pointer to the tear-off. The _Cache function uses the offset to work out the address of the pointer, and to check to see if it needs to create a new tear-off object:

    static HRESULT WINAPI CComObjectRootBase::_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;
    }
    
  11. Build the revised server. Then enhance the client to use the second (cached) tear-off, as shown below.
    ITearOff2Ptr pT2 = pOuter;
    b1 = pT2->GetName();
    _tprintf(_T("%S\n"), b1);
    SysFreeString(b1);
    
  12. Build and test. The client, as you would expect, is blissfully unaware of the devilish machinations going on behind the scenes.

OK, so now you know how to set up regular and cached tear-offs with the ATL. But what about the pros and cons?

PRO #1: The more interfaces you implement with one class, the bigger the C++ class object, since it will have a vtbl pointer for each interface. Using tear-offs reduces this vptr bloat. If clients use an interface only rarely, this overhead is unwarranted.
CON #1: Since the tear-off is implemented in a separate class with its own reference counting, we trade the 4-byte vptr reduction against 4 bytes for the vptr in the separate C++ object, plus 4 bytes for the separate reference count, plus 4 bytes for a back-pointer to the main object class.
JUDGEMENT #1: If you only use the tear-off pattern for rarely-used interfaces (and can back up the surmise with profiling data), the pros outweigh the cons.

CON #2: If the object is being used across an apartment boundary, the stub will cache the object-wide reference count with no accommodation for the tear-off, so any benefit is lost.
CON #2a: If you are doing anything in the construction or destruction of the tear-off object that you assume is transient because the tear-off is transient (like, say, holding resources, file handles, database connections), and you're going across apartments, you lose the transience.
CON #3: In a regular tear-off, the tear-off is created when QueryInterface is called on the main object. This means that your tear-off should be stateless, since multiple QI calls will result in multiple tear-off instances, each with their own state.
PRO #3: One valid reason for using a cached tear-off is exactly the opposite: to maintain state information, since once created the tear-off is thereafter cached and multiple instances do not occur.

As you can see, there are more CONs than PROs listed here. The ATL does supply some very convenient macros for implementing regular and cached tear-offs, but the strategy is essentially an implementation trick for performance optimization. The bottom line is that tear-offs will sometimes be only marginally useful and therefore, you need to base your decision on the specifics of the system involved. Hopefully, what you've learned today will make that decision making process a bit easier.



Comments

  • Great, exactly what we need

    Posted by Legacy on 10/20/2001 12:00am

    Originally posted by: T.Liu

    Great, exactly what we need

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • You probably have several goals for your patient portal of choice. Is "community" one of them? With a bevy of vendors offering portal solutions, it can be challenging for a hospital to know where to start. Fortunately, YourCareCommunity helps ease the decision-making process. Read this white paper to learn more. "3 Ways Clinicians can Leverage a Patient Portal to Craft a Healthcare Community" is a published document owned by www.medhost.com

  • The impact of a data loss event can be significant. Real-time data is essential to remaining competitive. Many companies can no longer afford to rely on a truck arriving each day to take backup tapes offsite. For most companies, a cloud backup and recovery solution will eliminate, or significantly reduce, IT resources related to the mundane task of backup and allow your resources to be redeployed to more strategic projects. The cloud - can now be comfortable for you – with 100% recovery from anywhere all …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds