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.
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.
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.
|