Aggregation & containment with source code
Aggregation & containment with source code
For a long time, containment was considered unfashionable, and originally the ATL didn't support it, although
the ATL has always supported aggregation to some degree. The added opportunity with containment - that
you can enhance the inner object method behaviour in the outer object re-implementations - is also its added
risk.
Aggregation
Aggregation is a specialization of composition. When an outer object aggregates an interface of an inner
object, it doesn't reimplement the interface - it merely passes the inner object's interface pointer directly to
the client. So, it's less effort for the outer object. On the other hand, the outer object can't specialize the
behaviour of the inner object.
The goal of aggregation is to convince the client that an interface implemented by the inner object is actually
implemented by the outer object - the client should be unaware that there is more than one object in use.
However, the aggregation must be carefully managed so that the client can't use the IUnknown methods on
the inner object. For instance, since the client has a direct pointer to the inner object's IVehicle, the client
could use this pointer to call QueryInterface and ask for ICar, at which point the inner object would return
E_NOINTERFACE because it doesn't support ICar. This situation must be avoided in setting up aggregation.
Containment
The following notes describe a very simple COM containment project, not using the ATL, just the raw COM
API. The code in ..\Vehicle is for a simple COM object, which will be used as the inner contained object. The
outer COM object is in ..\Car. A simple console client is in ..\Client. The ..\Registry directory contains some
shared registry-update code.
First, the significant code from the inner object, Vehicle. From the Vehicle.idl, you will see that the Vehicle
server offers just one interface, IVehicle, with one method, Drive:
[uuid(CBB27840-836D-11d1-B990-0080C824B323)
]
interface IVehicle : IUnknown
{
HRESULT Drive([in] int i, [in, out] int* ip);
};
The CVehicle class implements this interface:
CVehicle();
~CVehicle();
private:
long m_cRef;
};
The IUnknown methods, the class factory and the registry code is entirely as you would expect. So this inner
object offers nothing at all out of the ordinary - no special provisions for containment at all.
Now examine the code for the outer object. First the idl. Note the outer object's idl imports the inner object's
idl, and that the outer object supports both inner object and outer object interfaces. Note the directories
search path includes the path for the vehicle.idl.
// Car.idl
import "ocidl.idl";
import "Vehicle.idl";
[uuid(A9032A50-F54C-11d1-BCB6-0080C824B323)
]
interface ICar : IUnknown
{
HRESULT Reverse([in] int i, [in, out] int* ip);
};
[ uuid(C724E4D0-F54C-11d1-BCB6-0080C824B323),
version(1.0)
]
library Car
{
importlib("stdole2.tlb");
[ uuid(EA969C30-F54C-11d1-BCB6-0080C824B323),
]
coclass Car
{
[default] interface IVehicle;
interface ICar;
};
};
The CCar class implements both these interfaces, but note the crucial code in the outer object
implementation of the inner interface's Drive method - its just a wrapper to the inner object's
implementation. Note also the arbitrary Init function which will be used internally to initialize the inner object
CCar();
~CCar();
private:
long m_cRef;
IVehicle* m_pV;
};
Given that we have a pointer to an inner object, the code in the constructor and destructor should be
common sense:
CCar::~CCar()
{
InterlockedDecrement(&g_cObjects);
if (m_pV != NULL)
m_pV->Release();
}
The IUnknown methods are implemented in the normal way. The class factory is substantially ordinary, but
note the extra call in the CreateInstance to the Init method:
HRESULT hr = pCar->Init();
if (FAILED(hr))
{
pCar->Release();
return hr;
}
HRESULT CCar::Init()
{
HRESULT hr = ::CoCreateInstance(CLSID_Vehicle,
NULL,
CLSCTX_INPROC_SERVER,
IID_IVehicle,
(void**)&m_pV);
if (FAILED(hr))
return E_FAIL;
else
return S_OK;
}
Finally, the client. This is the expected runtime output:
Walk position = 3
Swim position = 0
The client application code follows. The client wants to use both IVehicle which offers Drive, and ICar which
offers Reverse. The client assumes that the Car object offers both interfaces. In order for this to work, we will
create a new Car object which internally makes use of the existing Vehicle object.
void main()
{
int i;
static int j = 0;
CoInitialize(NULL);
IVehicle* pv = NULL;
HRESULT hr = ::CoCreateInstance(CLSID_Car,
NULL,
CLSCTX_INPROC_SERVER,
IID_IVehicle,
(void**)&pv);
if (SUCCEEDED(hr))
{
for (i = 1; i < 3; i++)
pv->Drive(i, &j);
ICar* pc = NULL;
hr = pv->QueryInterface(IID_ICar, (void**)&pc);
if (SUCCEEDED(hr))
{
for (i = 1; i < 3; i++)
pc->Reverse(i, &j);
cout << "position = " << j << endl;
pc->Release();
}
pv->Release();
}
CoUninitialize();
}
Aggregation
I'll now describe a very simple raw COM API aggregation project. The code in ..\ \Vehicle is for a simple COM
object, which will be used as the inner object in an aggregation. The outer COM object is in ..\ \Car. A simple
console client is in ..\Client. The ..\Registry directory contains some shared registry-update code. Examine Fig
3 below, which is a more detailed version of Fig 2.
[uuid(CBB27840-836D-11d1-B990-0080C824B323)
]
interface IVehicle : IUnknown
{
HRESULT Drive([in] int i, [in, out] int* ip);
};
In the Vehicle.cpp, you will see another interface - this is not in the IDL and doesn't have a GUID because we
won't be publishing it - it is purely for internal use in the Vehicle server. This is exactly the same as the
standard IUnknown. Our inner object will effectively have two sets of the standard IUnknown methods. One
will be used to delegate to the outer object's IUnknown. The other - the Non-Delegating Unknown - will mostly
do the normal work of an unaggregated IUnknown.
interface INDUnknown
{
virtual HRESULT __stdcall NDQueryInterface(
REFIID riid, void **ppv) = 0;
virtual ULONG __stdcall NDAddRef() = 0;
virtual ULONG __stdcall NDRelease() = 0;
};
The Vehicle COM object is implemented in the CVehicle class. This derives from IVehicle (which is a COM
interface, and therefore derives from IUnknown), and from the unpublished INDUnknown. Hence, the
implementation of the IUnknown methods, the IVehicle methods and the INDUnknown methods. All three of
the IUnknown methods are implemented to delegate to the corresponding IUnknown methods of the outer
object, using the pointer to the outer interface.
CVehicle(IUnknown* pUnknownOuter);
~CVehicle();
private:
long m_cRef;
IUnknown* m_pUnknownOuter;
};
The Non-Delegating AddRef and Release are implemented like normal IUnknown AddRef and Release, but the
INDUnknown::QueryInterface is different. The important difference is that requests for IUnknown are
unequivocally cast to the internal Non-Delegating INDUnknown - we don't want to return a pointer to our
inner normal IUnknown (because that delegates to the outer object).
reinterpret_cast<IUnknown*>(*ppv)->AddRef();
return S_OK;
}
Now the outer object. Again, in the IDL, there's only one interface: [uuid(A9032A50-F54C-11d1-BCB6-
0080C824B323) ] interface ICar : IUnknown { HRESULT Reverse([in] int i, [in, out] int* ip); };
The outer COM Object is implemented in Car.cpp. Note the pointer to the inner interface:
class CCar : public ICar
{
public:
virtual HRESULT __stdcall QueryInterface(
const IID& riid, void** ppv);
virtual ULONG __stdcall AddRef(void);
virtual ULONG __stdcall Release(void);
HRESULT Init();
CCar();
~CCar();
private:
long m_cRef;
IUnknown* m_pUnknownInner;
};
The outer object's QueryInterface. When queried for IVehicle, we delegate to the inner object's interface.
However, remember that m_pUnknownInner is a pointer to the inner object's Non-Delegating INDUnknown
(which doesn't have a QueryInterface method - so how does this work?)
static_cast<IUnknown*>(*ppv)->AddRef();
return S_OK;
}
Here's the client. As you can see, the client creates a Car COM object, and expects the object to implement
both the ICar interface and the IVehicle interface, completely unaware of the aggregation:
void main()
{
int i;
static int j = 0;
CoInitialize(NULL);
IVehicle* pv = NULL;
HRESULT hr = ::CoCreateInstance(
CLSID_Car, NULL, CLSCTX_INPROC_SERVER,
IID_IVehicle, (void**)&pv);
if (SUCCEEDED(hr))
{
for (i = 1; i < 3; i++)
pv->Drive(i, &j);
cout << "position = " << j << endl;
ICar* pc = NULL;
hr = pv->QueryInterface(IID_ICar, (void**)&pc);
if (SUCCEEDED(hr))
{
for (i = 1; i < 3; i++)
pc->Reverse(i, &j);
CoUninitialize();
}
ATL Aggregation
From Visual C++, version 6, Aggregation is supported by default. When you use ATL to implement
aggregation, you use a series of ATL macros to implement the inner and outer object.
Add the macro DECLARE_AGGREGATABLE to the class definition. If you use the v6 ATL Object Wizard
and check the aggregation box, this is already built-in, and in fact if you don't want aggregation you
must include the macro DECLARE_NOT_AGGREGATABLE instead.
Note: To ensure that the inner object does not do something while being created to release the outer object,
it is usually a good idea to declare the macro DECLARE_PROTECT_FINAL_CONSTRUCT. Again, v6 does this for
you. How could this be an issue? Well, consider this: the inner object might support only conditional
aggregation. In other words, it will allow itself to be aggregated only if the outer object attempting
aggregation supports some particular interface. So, the inner object would QI the outer object and only return
success on the CoCreateInstance if it likes the outer object's interfaces. You can see how easy it would be for
the inner object to accidentally release the outer object in such a scenario.
4. (*ip) += i;
5. return S_OK;
6. Now create another project - for the client. Make it a Win32 Console Application. Above main,
#import the Vehicle type library. Code main to test the Vehicle object, build and test.
7. try
8. {
9. IVehiclePtr pv(CLSID_Vehicle);
10.
11. for (i = 1; i < 3; i++)
12. pv->Drive(i, &j);
13.
14. cout << "position = " << j << endl;
15. }
16. catch (_com_error& e)
17. {
18. cout << e.ErrorMessage() << "\n";
19. }
4. (*ip) -= i;
5. return S_OK;
6. Copy and paste the definition of the IVehicle interface from the ATLVehicle IDL file into the ATLCar
IDL file - put it above or below the ICar interface. In the library section, list the IVehicle interface as a
second supported interface in the Car coclass.
7. Declare a new member variable in the CCar class: an IUnknown pointer called m_pInner - this will
eventually hold the IUnknown pointer on the aggregated inner object. Initialize this to NULL in the
constructor. In the CCar class COM map, add an entry for IVehicle - this will be a special aggregation
macro that evaluates out to a QueryInterface through the inner object's IUnknown:
8. COM_INTERFACE_ENTRY_AGGREGATE(IID_IVehicle, m_pInner)
9. Also in the CCar class declaration, add the DECLARE_GET_CONTROLLING_UNKNOWN() macro - this
evaluates out to declaring a function called GetControllingUnknown, which will get the outer object's
IUnknown pointer. Also declare these two functions:
Note: You can copy this from the ATLVehicle_i.c. Then implement the two new functions as shown
below. The FinalConstruct will instantiate the inner object, using CoCreateInstance like any regular
client; and the FinalRelease will Release the inner object:
HRESULT CCar::FinalConstruct()
{
return CoCreateInstance(CLSID_Vehicle,
GetControllingUnknown(),
CLSCTX_ALL, IID_IUnknown,
(void**) &m_pInner);
}
void CCar::FinalRelease()
{
if (NULL != m_pInner)
m_pInner->Release();
}
16. Now, update the client: first change the #import to import the ATLCar type library and not the
ATLVehicle type library (remember the ATLCar type library will also expose the IVehicle interface).
Also change the client code to use the outer (and aggregated inner) object - in this way, the client is
only directly aware of the outer object, and assumes the outer object implements both IVehicle and
ICar. Build and test.
17. try
18. {
19. // IVehiclePtr pv(CLSID_Vehicle);
20. IVehiclePtr pv(CLSID_Car);
21. for (i = 1; i < 3; i++)
22. pv->Drive(i, &j);
23.
24. cout << "position = " << j << endl;
25.
26. ICarPtr pc = pv;
27. for (i = 1; i < 3; i++)
28. pc->Reverse(i, &j);
29.
30. cout << "position = " << j << endl;
31. }
3. import "..\ATLVehicle\ATLVehicle.idl";
4. Then list the IVehicle as a supported interface in the coclass as with aggregation.
5. Make sure the client code still #imports the outer object's type library:
Note: By default, the high-level error-handling methods use the COM support classes _bstr_t and
variant_t in place of the BSTR and VARIANT data types and raw COM interface pointers. These
classes encapsulate the details of allocating and deallocating memory storage for these data types,
and greatly simplify type casting and conversion operations. The raw_native_types attribute is used
to disable the use of these COM support classes in the high-level wrapper functions, and force the
use of low-level data types instead.
public IDispatchImpl<IVehicle,
&IID_IVehicle,
&LIBID_ATLVEHICLELib>
...and this code in the CCar class body:
11. COM_INTERFACE_ENTRY(IVehicle)
12. In the CCar class, add a new member variable, an IVehicle pointer called m_pVehicle. Initialize this
in the constructor to NULL. Now change the implementation of the Drive method to use this pointer
to delegate the Drive behaviour (remember, the CCar::Drive is only a stub to the CVehicle::Drive):
Summary
So, we've peered under the hood to look at the underlying mechanics of containment and
aggregation - and did you get the quintessentially COM way the two inner object unknowns are
managed? We then examined the way the ATL supports the two strategies. Which strategy you use
depends on what you're trying to do. Although containment is still not exactly fashionable, it is easy
to do and doesn't fall foul of other constraints like MTS hosting. Aggregation will always be faster,
although the internal code is a little trickier. Horses for courses.