previous page
next page

Aggregation: The Controlling Outer

As with tear-offs, aggregation enables you to separate the code for a single identity into multiple objects. However, whereas using tear-offs require shared source between the owner and the tear-off class, aggregation does not. The controlling outer and the controlling inner do not have to share the same server or even the same implementation language (although they do have to share the same apartment). If you like, you can consider an aggregated object a kind of "binary cached tear-off." As with a cached tear-off, an aggregated instance's lifetime and identity are subsumed by that of the controlling outer. Just like a cached tear-off, an aggregated instance must have a way to obtain the interface pointer of the controlling outer. In a tear-off, we pass the owner as a constructor argument. In aggregation, we do the same thing, but using the COM constructor that accepts a single, optional constructor argumentthat is, the pUnkOuter parameter of IClassFactory::CreateInstance and its wrapper, CoCreateInstance:

interface IClassFactory : IUnknown {                      
  HRESULT CreateInstance([in, unique] IUnknown* pUnkOuter,
                                 [in] REFIID riid,        
                  [out, iid_is(riid)] void **ppvObject);  
  HRESULT LockServer([in] BOOL fLock);                    
};                                                        
                                                          
WINOLEAPI CoCreateInstance([in] REFCLSID rclsid,          
                   [in, unique] LPUNKNOWN pUnkOuter,      
                           [in] DWORD dwClsContext,       
                           [in] REFIID riid,              
            [out, iid_is(riid)] LPVOID FAR* ppv);         

In Chapter 4, "Objects in ATL," I discussed how ATL supports aggregation as a controlled inner using CComAggObject (or CComPolyObject). In this chapter, I show you the four macros that ATL provides for the controlling outer in the aggregation relationship.

Planned Versus Blind Aggregation

After an aggregate is created, the controlling outer has two choices of how to exposed the interface(s) of the aggregate as its own. The first choice is planned aggregation. In planned aggregation, the controlling outer wants the inner to expose one of a set of interfaces known by the outer. Figure 6.2 illustrates planned aggregation.

Figure 6.2. Planned aggregation


The downside to this technique is that if the inner's functionality grows, clients using the outer cannot gain access to the additional functionality. The upside is that this could be exactly what the outer had in mind. For example, consider the standard interface IPersist:

interface IPersist : IUnknown {             
  HRESULT GetClassID([out] CLSID *pClassID);
}                                           

If the outer were to blindly expose the inner's implementation of IPersist, when the client called GetClassID, it would get the CLSID of the inner, not the outer. Because the client wants the outer object's class identifier, we have again broken the spirit of the COM identity laws. Planned aggregation helps prevent this breach.

Blind aggregation, on the other hand, allows the outer's functionality to grow with the inner's, but it provides the potential for exposing identity information from the inner. For this reason, blind aggregation should be avoided. Figure 6.3 shows blind aggregation.

Figure 6.3. Blind aggregation


Manual Versus Automatic Creation

COM_INTERFACE_ENTRY_AGGREGATE and COM_INTERFACE_ENTRY_AGGREGATE_BLIND

ATL provides support for both planned and blind aggregation via the following two macros:

#define COM_INTERFACE_ENTRY_AGGREGATE(iid, punk) \             
  { &iid, (DWORD_PTR)offsetof(_ComMapClass, punk), _Delegate },
                                                                   
#define COM_INTERFACE_ENTRY_AGGREGATE_BLIND(punk) \            
  { NULL, (DWORD_PTR)offsetof(_ComMapClass, punk), _Delegate}, 

These macros assume that the aggregate has already been created manually and that the interface pointer is stored in the punk parameter to the macro. The _Delegate function forwards the QueryInterface request to that pointer:

static HRESULT WINAPI                                     
CComObjectRootBase::_Delegate(                            
  void* pv,                                               
  REFIID iid,                                             
  void** ppvObject,                                       
  DWORD dw)                                               
{                                                         
  HRESULT hRes = E_NOINTERFACE;                           
  IUnknown* p = *(IUnknown**)((DWORD_PTR)pv + dw);        
  if (p != NULL) hRes = p->QueryInterface(iid, ppvObject);
  return hRes;                                            
}                                                         

To use aggregation of an inner that has been manually created, the classes use COM_INTERFACE_ENTRY_AGGREGATE or COM_INTERFACE_ENTRY_AGGREGATE_BLIND in the interface map:

class CBeachBall :
  public CComObjectRootEx<CBeachBall>,
  public ISphere,
  public IRollableObject,
  public IPlaything {
public:
BEGIN_COM_MAP(CBeachBall)
  COM_INTERFACE_ENTRY(ISphere)
  COM_INTERFACE_ENTRY(IRollableObject)
  COM_INTERFACE_ENTRY(IPlaything)
  COM_INTERFACE_ENTRY_AGGREGATE(IID_ILethalObject,
    m_spunkLethalness)
  COM_INTERFACE_ENTRY_AGGREGATE_BLIND(m_spunkAttitude)
END_COM_MAP()

DECLARE_GET_CONTROLLING_UNKNOWN()
DECLARE_PROTECT_FINAL_CONSTRUCT()

  HRESULT FinalConstruct() {
    HESULT hr;
    hr = CoCreateInstance(CLSID_Lethalness,
                          GetControllingUnknown(),
                          CLSCTX_INPROC_SERVER,
                          IID_IUnknown,
                          (void**)&m_spunkLethalness);
    if( SUCCEEDED(hr) ) {
    hr = CoCreateInstance(CLSID_Attitude,
                           GetControllingUnknown(),
                           CLSCTX_INPROC_SERVER,
                           IID_IUnknown,
                           (void**)&m_spunkAttitude);
   }

   return hr;
 }

 void FinalRelease() {
   m_spunkLethalness.Release();
   m_spunkAttitude.Release();
 }
...
public:
  CComPtr<IUnknown> m_spunkLethalness;
  CComPtr<IUnknown> m_spunkAttitude;
};

Notice that I have used the FinalConstruct method to create the aggregates so that failure stops the creation process. Notice also that because I've got a FinalConstruct that hands out interface pointers to the object being created, I'm using DECLARE_PROTECT_FINAL_CONSTRUCT to protect against premature destruction. I've also got a FinalRelease method to manually release the aggregate interface pointers to protect against double destruction. Aggregation was one of the chief motivations behind the multiphase construction of ATL-based COM objects, so it's not surprising to see all the pieces used in this example.

However, one thing I've not yet mentioned is the DECLARE_GET_CONTROLLING_UNKNOWN macro. The controlling unknown is a pointer to the most controlling outer. Because aggregation can go arbitrarily deep, when aggregating, the outer needs to pass the pUnkOuter of the outermost object. To support this, ATL provides the GET_CONTROLLING_UNKNOWN macro to give an object the definition of a GetControllingUnknown function:

#define DECLARE_GET_CONTROLLING_UNKNOWN() public: \
  virtual IUnknown* GetControllingUnknown() { \    
    return GetUnknown(); }                         

You might question the value of this function because it simply forwards to GetUnknown, but notice that it's virtual. If the object is actually being created as an aggregate while it is aggregating, GetControllingUnknown is overridden in CComContainedObject:

template <class Base> class CComContainedObject : public Base {
...                                                            
  IUnknown* GetControllingUnknown()                            
  { return m_pOuterUnknown; }                                  
...                                                            
};                                                             

So, if the object is standalone, GetControllingUnknown returns the IUnknown* of the object, but if the object is itself being aggregated, GetControllingUnknown returns the IUnknown* of the outermost outer.

COM_INTERFACE_ENTRY_AUTOAGGREGATE and COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND

If you're not doing any initialization of the aggregates, sometimes it seems a waste to create them until (or unless) they're needed. For automatic creation of aggregates on demand, ATL provides the following two macros:

#define COM_INTERFACE_ENTRY_AUTOAGGREGATE(iid, punk, clsid) \ 
  { &iid, \                                                   
    (DWORD_PTR)&ATL::_CComCacheData< \                        
     ATL::CComAggregateCreator<_ComMapClass, &clsid>, \       
    (DWORD_PTR)offsetof(_ComMapClass, punk)>::data, \         
    _Cache },                                                 
                                                              
#define COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND(punk, clsid) \
  { NULL, \                                                   
    (DWORD_PTR)&ATL::_CComCacheData< \                       
     ATL::CComAggregateCreator<_ComMapClass, &clsid>, \      
    (DWORD_PTR)offsetof(_ComMapClass, punk)>::data, \        
    _Cache },                                                

The only new thing in these macros is the CComAggregateCreator, which simply performs the CoCreateInstance the first time the interface is requested:

template <class T, const CLSID* pclsid>                         
class CComAggregateCreator {                                    
public:                                                         
  static HRESULT WINAPI CreateInstance(void* pv, REFIID,        
    LPVOID* ppv) {                                              
    T* p = (T*)pv;                                              
    return CoCreateInstance(*pclsid, p->GetControllingUnknown(),
       CLSCTX_INPROC, __uuidof(IUnknown), ppv);                 
  }                                                             
};                                                              

Using automatic creation simplifies the outer's code somewhat:

class CBeachBall :
  public CComObjectRootEx<CBeachBall>,
  public ISphere,
  public IRollableObject,
  public IPlaything {
public:
BEGIN_COM_MAP(CBeachBall)
  COM_INTERFACE_ENTRY(ISphere)
  COM_INTERFACE_ENTRY(IRollableObject)
  COM_INTERFACE_ENTRY(IPlaything)
  COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_ILethalObject,
    m_spunkLethalness, CLSID_Lethalness)
  COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND(m_spunkAttitude,
    CLSID_Attitude)
END_COM_MAP()

DECLARE_GET_CONTROLLING_UNKNOWN()

  void FinalRelease() {
    m_spunkLethalness.Release();
    m_spunkAttitude.Release();
  }
...
public:
  CComPtr<IUnknown> m_spunkLethalness;
  CComPtr<IUnknown> m_spunkAttitude;
};

Although we no longer need to perform the creation in FinalConstruct, we're still required to use DECLARE_GET_CONTROLLING_UNKNOWN and to provide storage for the aggregated interfaces. We still release the interfaces manually in FinalRelease, as well, to avoid double destruction.

Aggregating the Free Threaded Marshaler

The ATL Object Wizard directly supports one particularly interesting use of aggregation: aggregating the implementation of IMarshal provided by the Free Threaded Marshaler (FTM). Any object that aggregates the FTM is said to be an apartment-neutral object. Normally, passing an interface pointer between apartments, even in the same process, results in a proxy-stub pair. The proxy-stub pair maintains the concurrency and synchronization requirements of both the object and the client, but it also adds overhead. In-process objects that provide their own synchronization and prefer to snuggle up to the client without the overhead of the proxy-stub can aggregate the FTM. By aggregating the FTM, an object short-circuits the creation of the proxy-stub for in-process objects. Therefore, the object can be passed between apartments in the same address space without the overhead of a proxy-stub.

The wizard generates the following code when the Free Threaded Marshaler option is checked in the ATL Object Wizard:

class ATL_NO_VTABLE CBowlingBall :                               
  public CComObjectRootEx<CComMultiThreadModel>,                 
  public CComCoClass<CBowlingBall, &CLSID_BowlingBall>,          
  public IBowlingBall                                            
{                                                                
public:                                                          
  CBowlingBall() { m_pUnkMarshaler = NULL; }                     
                                                                    
DECLARE_REGISTRY_RESOURCEID(IDR_BOWLINGBALL)                     
DECLARE_GET_CONTROLLING_UNKNOWN()                                
DECLARE_PROTECT_FINAL_CONSTRUCT()                                
                                                                 
BEGIN_COM_MAP(CBowlingBall)                                      
  COM_INTERFACE_ENTRY(IBowlingBall)                              
  COM_INTERFACE_ENTRY_AGGREGATE(IID_IMarshal, m_pUnkMarshaler.p) 
END_COM_MAP()                                                    
                                                                 
  HRESULT FinalConstruct() {                                     
    return CoCreateFreeThreadedMarshaler(GetControllingUnknown(),
                                         &m_pUnkMarshaler.p);    
  }                                                              
  void FinalRelease() {                                          
    m_pUnkMarshaler.Release();                                   
  }                                                              
                                                                 
  CComPtr<IUnknown> m_pUnkMarshaler;                             
...                                                              
};                                                               

Because the CLSID of the FTM is not available, instead of using auto-creation, ATL uses CoCreateFreeThreadMarshaler to create an instance of the FTM in the FinalConstruct method.

FTM Danger, Will Robinson! Danger! Danger!

Aggregating the FTM is easy, so I should mention a couple of big responsibilities that you, the developer, accept by aggregating the FTM:

  • Apartment-neutral objects must be thread safe You can mark your class ThreadingModel=Apartment all day long, but because your object can be passed freely between apartments in the same process and, therefore, can used simultaneously by multiple threads, you'd better use CComMultiThreadModel and at least object-level locking. Fortunately, the ATL Object Wizard makes the FTM option available for selection only if you choose a threading model of Both or Neutral.

  • Apartment-neutral objects are not aggregatable. Aggregating the FTM depends on being able to implement IMarshal. If the outer decides to implement IMarshal and doesn't ask the inner object, the inner can no longer be apartment neutral.

  • Apartment-neutral objects cannot cache interface pointers. An apartment-neutral object is said to be apartment neutral because it doesn't care which apartment it is accessed from. However, other objects that an apartment-neutral object uses might or might not also be apartment-neutral. Interface pointers to objects that aren't apartment neutral can be used only in the apartment to which they belong. If you're lucky, the apartment-neutral object attempting to cache and use an interface pointer from another apartment will have a pointer to a proxy. Proxies know when they are being accessed outside their apartments and return RPC_E_WRONG_THREAD for all such method calls. If you're not so lucky, the apartment-neutral object obtains a raw interface pointer. Imagine the poor single-threaded object accessed simultaneously from multiple apartments as part of its duty to the apartment-neutral object. It will most likely work just fine until you need to give a demo to your biggest client.

The only safe way to cache interface pointers in an apartment-neutral object is as cookies obtained from the Global Interface Table (GIT). The GIT is a process-global object provided to map apartment-specific interface pointers to apartment-neutral cookies and back. The GIT was invented after the FTM and is provided in the third service pack to NT 4.0, the DCOM patch for Windows 95, and out of the box with Windows 98/2000/XP. If you're aggregating the FTM and caching interface pointers, you must use the GIT. ATL provides the CComGITPtr class, discussed in Chapter 3, "ATL Smart Types," to make dealing with the GIT easier.

For an in-depth discussion of the FTM, the GIT, and their use, read Essential COM (Addison-Wesley, 1997), by Don Box.


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)