Developers Workshop to COM and ATL 3.0
| < Free Open Study > |
|
Aggregation is the other binary reuse technique used in COM. Here, the outer object exposes the inner object's interfaces directly. Unlike containment, there is no need for the outer object to implement the interface(s) of the inner object. There is no need to delegate interface calls from the outer object to the inner object, as the inner object's interfaces are directly available to external clients. Conceptually, aggregation looks like the following:
As far as the client is concerned, the inner and outer objects appear to be one seamless coclass. Thus, to a client, CoHexagon supports IDraw, IColor, and IUnknown. The client has no idea that the set of interfaces exposed are in fact provided, in part, by an inner object.
COM objects (such as CoColor) must be preprogrammed to support the ability to be aggregated by another object. Therefore, unless a COM object has been equipped with the correct infrastructure, an outer object cannot aggregate it. Contrast this to a contained inner object, which requires no extra code to behave correctly.
Also understand that not all COM objects are able to participate in COM aggregation even with the correct internal riggings. Specifically, only COM objects that are contained within in-process servers can be aggregated and even then, the inner and outer objects must be in the same apartment (i.e., execution context).
Note | Again, only objects housed within in-proc servers may be aggregated. You cannot aggregate objects living in EXE servers (local or remote). |
The outer object and any aggregated inner objects must work together to uphold the rules of COM identity. As both the inner and outer objects maintain a unique implementation of QueryInterface(), a good deal of work needs to be done to allow a client to traverse among the interfaces of the inner and outer objects. Thankfully, ATL provides full and complete support for COM aggregation, which takes care of all the details for both the inner and outer object's implementation.
ATL's Support for COM Aggregation
When you insert a COM object using the ATL Object Wizard, one of the choices you need to specify is the object's level of "aggregation awareness." Assume we are creating an ATL-centric CoColor object, which is residing in ATLAggServer.dll. Based upon your selection from the Attributes tab (Figure 8-8), the ATL Object Wizard will configure your object with the proper level of aggregation support using ATL macros. Here again is a rundown of each choice:
-
Yes: Your object will be able to work either as a stand-alone (outer) object or an aggregated (inner) object. If you select this option, you will inherit the default functionality specified by the DECLARE_AGGREGATABLE macro defined in CComCoClass<>.
-
No: Your object will only be able to function as a stand-alone (outer) object. If you choose this option, the Object Wizard will include the DECLARE_NOT_AGGREGATABLE macro in your coclass's header file.
-
Only: This option ensures your object will never function as a stand-alone object, but must be part of an aggregate to function. The Object Wizard will include the DECLARE_ONLY_AGGREGATABLE macro in your coclass's header file.
Most of the time, you will want your object to behave as either an outer or inner object, and will therefore leave the default Yes selection. As we have seen earlier in Chapter 7, CComCoClass<> defines this macro by default:
// By default, all ATL objects are equipped to be aggregated. template <class T, const CLSID* pclsid = &CLSID_NULL> class CComCoClass { public: ... DECLARE_AGGREGATABLE(T) ... };
Configuring the Outer Object
COM objects that aggregate other objects are responsible for creating them. Much like COM containment, FinalConstruct() is the safest place to create your inner objects. The whole idea behind aggregation is to provide the illusion that the outer coclass is composed of more interfaces than it really is. The interfaces of the inner object are obtainable directly from the QueryInterface() logic of the outer class. As well, the inner object must be able to navigate to the outer object to forward QueryInterface() requests.
In order for the inner object to communicate with the outer object, we need to send in an IUnknown to the aggregated object. This is accomplished again using the GetControl- lingUnknown() method provided by the DECLARE_GET_CONTROLLING_UNKNOWN macro. When we are creating the inner object, we will send in the outer object's IUnknown as the second parameter to CoCreateInstance(). This will allow the inner object to forward QueryInterface() calls to the outer class.
Likewise, the outer class needs to have an IUnknown pointer to the aggregated object to forward QueryInterface() calls to the inner object. We can store this fetched interface pointer in a private member variable (m_pInnerUnk). Here is the initial bit of code which illustrates how inner and outer objects exchange IUnknown pointers:
// An ATL-centric CoHexagon aggregating CoColor. class ATL_NO_VTABLE CCoHexagon : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoHexagon, &CLSID_CoHexagon>, public IDraw { public: CCoHexagon() : m_pInnerUnk(NULL) { } // Need this to gain IUnknown*. DECLARE_GET_CONTROLLING_UNKNOWN() HRESULT FinalConstruct() { // We want to create the CoColor aggregate. HRESULT hr; hr = CoCreateInstance(CLSID_CoColor, GetControllingUnknown(), CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&m_pInnerUnk); return hr; } void FinalRelease() { if(pColor) { m_pInnerUnk ->Release(); } } BEGIN_COM_MAP(CCoHexagon) COM_INTERFACE_ENTRY(IDraw) END_COM_MAP() // IDraw public: STDMETHOD(Draw)(); private: // Interface pointer of inner object. IUnknown* m_pInnerUnk; };
With the above configuration, CoHexagon will create the CoColor object upon startup and destroy it upon shutdown. As well, CoColor now has a pointer back to CoHexagon (via the IUnknown pointer sent in using GetControllingUnknown() ), while we have a pointer to the inner object.
Our next task is to decide which of the interfaces supported by CoColor we wish to expose as interfaces of CoHexagon. This is an easy choice, as CoColor only supports IColor. In general, for each interface of the inner object you wish to expose from the outer object, add a COM_INTERFACE_ENTRY_AGGREGATE entry to the outer class's COM map. For example:
// CoHexagon's updated COM map. BEGIN_COM_MAP(CCoHexagon) COM_INTERFACE_ENTRY(IDraw) COM_INTERFACE_ENTRY_AGGREGATE (IID_IColor, m_pInnerUnk) END_COM_MAP()
For the sake of discussion, assume CoColor supported a total of three color-related interfaces {IColor, IGradient, IBlend}. As the outer object, we may select which interfaces of the inner object(s) we wish to expose. If we wish to expose IColor as well as IBlend, CoHexagon's COM map would be updated as:
// CoHexagon's updated COM map. BEGIN_COM_MAP(CCoHexagon) COM_INTERFACE_ENTRY(IDraw) COM_INTERFACE_ENTRY_AGGREGATE (IID_IColor, m_pInnerUnk) COM_INTERFACE_ENTRY_AGGREGATE (IID_IBlend, m_pInnerUnk) END_COM_MAP()
With the above additions, we have fully implemented an outer and inner object using the ATL framework. To summarize how to aggregate with ATL:
-
Inner Objects: Inner objects are configured automatically when using the ATL Object Wizard. From the Attributes tab, you may specify the level of aggregation support. By default, ATL objects leverage the DECLARE_AGGREGATABLE macro provided by CComCoClass<>.
-
Outer Objects: ATL objects that aggregate other objects begin by constructing the inner object from within FinalConstruct(), and cache the fetched interface. To send in the IUnknown of the outer object, add the DECLARE_GET_CONTROL- LING_UNKNOWN macro to your coclass, and use the GetControllingUnknown() helper function as the second parameter to CoCreateInstance(). Finally, update your COM map using COM_INTERFACE_ENTRY_AGGREGATE for each interface you wish to expose as your own.
Alternative COM Map Aggregation Entries
While COM_INTERFACE_ENTRY_AGGREGATE is the most common COM map entry when working with aggregates, ATL does provide three others, which we will now discuss before wrapping up our discussion of containment and aggregation.
When your outer class makes use of the COM_INTERFACE_ENTRY_AGGREGATE COM map macro, you get to select exactly which interfaces of the inner object are available to external clients. This approach is called selective aggregation, and gives the outer object the final say on how it appears to the outside world. Another possibility is blind aggregation. This approach is more of a commando tactic, as the outer object calls the inner object's InternalQueryInterface() for any interface the outer object does not support. In raw C++, it would look something like the following:
// Blind aggregation. STDMETHODIMP CoHexagon::QueryInterface(REFIID, riid, void** ppv) { ... if( riid == IID_IUnknown) *ppv = (IUnknown*)(IDraw*)this; else if(riid == IID_IDraw) *ppv = (IDraw*)this; // Forward all other requests to the inner object! else hr = m_pInnerUnk -> QueryInterface(riid, ppv); ... }
ATL provides the COM_INTERFACE_ENTRY_AGGREGATE_BLIND macro to support blind aggregation. In this case, you will not use COM_INTERFACE_ENTRY_AGGRE- GATE on an interface-by-interface basis, but rather send in the inner object's IUnknown as the sole parameter:
// CoHexagon is now blindly forwarding all calls it does not understand // into CoColor. BEGIN_COM_MAP(CCoHexagon) COM_INTERFACE_ENTRY(IDraw) COM_INTERFACE_ENTRY_AGGREGATE_BLIND(m_pInnerUnk) END_COM_MAP()
As you can imagine, selective aggregation is preferred to blind, in that the outer object has more control over the process. Using blind aggregation, CoHexagon might very well ask CoColor for ICreateCar, IStats, or IEngine! The common approach is to use selective aggregation and the COM_INTERFACE_ENTRY_AGGREGATE macro for each interface you wish to support on the outer object.
ATL's Auto Aggregation COM Map Entries
The final aggregation macros allow you to both create and expose the internal interfaces without explicitly creating the inner objects. COM_INTERFACE_ENTRY_AUTOAGGRE- GATE and COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND are interesting in that the aggregated object is only created when the client explicitly asks for an interface of the aggregated object.
In our previous design, CoHexagon created CoColor in its FinalConstruct() method using standard COM library calls. As the ATL framework calls FinalConstruct() as part of the object's creation, CoHexagon will load the server holding CoColor, regardless of whether the external client ever asks for IColor or not. If we configured CoHexagon to use the auto aggregation macros, we could clean up CoHexagon just a bit, and ensure the server housing the aggregated object is not loaded until absolutely required. Here is CoHexagon using auto aggregation:
// Auto aggregation delays the creation of the aggregated object until // absolutely necessary. class ATL_NO_VTABLE CCoHexagon : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoHexagon, &CLSID_CoHexagon>, public IDraw { public: CCoHexagon() : m_pInnerUnk(NULL) { } ... DECLARE_GET_CONTROLLING_UNKNOWN() void FinalRelease() { if(m_pColor) m_pInnerUnk ->Release(); } BEGIN_COM_MAP(CCoHexagon) COM_INTERFACE_ENTRY(IDraw) COM_INTERFACE_ENTRY_AUTOAGGREGATE (IID_IColor, m_pInnerUnk, CLSID_CoColor) END_COM_MAP() public: STDMETHOD(Draw)(); IUnknown* m_pInnerUnk; };
Notice how our call to FinalConstruct() has been removed, as the CoColor instance will be created automatically whenever the first request for IID_IColor is issued.
Lab 8-5: COM Aggregation with ATL
In the previous lab you were introduced to reuse of COM objects using containment. This final lab of the chapter will give you a chance to work with an alternative form of binary reuse, COM aggregation. Here, you will add to the objects housed within the ATLVehicles server to include a Jeep and hot rod, each aggregating your original ATLCoCar using the ATL framework.
On the CD The solution for this lab can be found on your CD-ROM under:Labs\Chapter 08\ATLVehiclesLabs\Chapter 08\ATLVehicles\VB ClientLabs\Chapter 08\ATLVehicles\CPP Client
Step One: Create the Aggregated Object
Open the ATLVehicles project developed in the previous lab. Using the ATL Object Wizard, insert another Simple Object named CoHotRod. Change the name of the initial interface to IDragRace, and be sure to select a Custom interface. All other options may be left at their defaults.
Your new CoHotRod will make use of COM aggregation in order to reuse the ATLCoCar coclass. Begin by adding the DECLARE_GET_CONTROLLING_UNKNOWN() macro to your cohotrod.h file. Recall this macro expands to define a method named GetControllingUnknown(), which returns the IUnknown pointer for your coclass.
// CoHotRod will reuse CoCar using aggregation. class ATL_NO_VTABLE CCoHotRod : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoHotRod, &CLSID_CoHotRod>, public IDragRace { public: CCoHotRod() { } DECLARE_REGISTRY_RESOURCEID(IDR_COHOTROD) DECLARE_PROTECT_FINAL_CONSTRUCT() DECLARE_GET_CONTROLLING_UNKNOWN() ... };
Next, override FinalConstruct(), and make a call to CoCreateInstance() to create the inner aggregated CoCar (don't forget to #include atltearoff.h). Make use of the GetControllingUnknown() method in your CoCreateInstance() call. Ask up front for the inner object's IUnknown pointer, and store this in a public IUnknown pointer maintained by CoHotRod. You may also wish to add a friendly message box, which will be helpful during the testing phase:
// Create the aggregate. HRESULT FinalConstruct() { MessageBox(NULL, "Created a ATLCoCar using aggregation!", "Message from hotrod", MB_OK | MB_SETFOREGROUND); HRESULT hr; hr = CoCreateInstance(CLSID_ATLCoCar, GetControllingUnknown(), CLSCTX_SERVER, IID_IUnknown, (void**)&pUnkInnerCar); return hr; }
In your class's FinalRelease(), dispose of the inner object's IUnknown pointer, and again, display a friendly message box:
// Kill the inner object within FinalRelease() void FinalRelease() { pUnkInnerCar->Release(); MessageBox(NULL, "The HOTROD is dead!", "Message from hotrod", MB_OK | MB_SETFOREGROUND); }
Step Two: Expose the Inner Object's Interfaces
Next, we will need to edit our COM map to expose interfaces of the inner object from the outer object. Let's equip CoHotRod to make use of the IEngine, IOwner, IOwnerInfo, and ICreateCar interfaces (recall that we revamped CoCar to support ICreateCar as a tear-off). Using the most standard ATL aggregation macro, COM_INTERFACE_ENTRY_AGGRE- GATE, update your COM map as follows:
// Don't forget that the first entry of your map should be a 'simple' entry such as // COM_INTERFACE_ENTRY, as ATL uses this to calculate the offset to your // IUnknown. BEGIN_COM_MAP(CCoHotRod) COM_INTERFACE_ENTRY(IDragRace) COM_INTERFACE_ENTRY_AGGREGATE(IID_ICreateCar, pUnkInnerCar) COM_INTERFACE_ENTRY_AGGREGATE(IID_IStats, pUnkInnerCar) COM_INTERFACE_ENTRY_AGGREGATE(IID_IEngine, pUnkInnerCar) COM_INTERFACE_ENTRY_AGGREGATE(IID_IOwnerInfo, pUnkInnerCar) END_COM_MAP()
Finish up the C++ implementation of CoHotRod by adding a single method to the [default] IDragRace interface named DragRace(). Give an implementation suited to a high-performance automobile:
// HotRods always win... STDMETHODIMP CCoHotRod::DragRace() { MessageBox(NULL, " Done! Dude, this is a fast car.", "Hot Rod message", MB_OK | MB_SETFOREGROUND); return S_OK; }
Go ahead and compile. Remember that coclasses that aggregate other COM objects do not derive from the interfaces of the inner object. Therefore, your inheritance chain for CoHotRod will only derive from IDragRace, beyond the ATL framework templates:
// Outer objects do not derive from the interfaces of the aggregated objects. class ATL_NO_VTABLE CCoHotRod : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoHotRod, &CLSID_CoHotRod>, public IDragRace { ... };
Step Three: Clean Up the IDL
As in the case with the contained ATLCoCar, we must update our coclass statement to ensure that type library-dependent COM language mappings can "see" our interfaces. Open the project's IDL file, and edit your coclass statement to support the following interfaces of the inner object. For ease of use by the object user, set ICreateCar to be the [default]:
// Expose interfaces. [ uuid(DDF18628-24F6-11D3-B8F9-0020781238D4), helpstring("CoHotRod Class")] coclass CoHotRod { [default] interface ICreateCar; // Inner object. interface IDragRace; // Outer object. interface IStats; // Inner object. interface IEngine; // Inner object. interface IOwnerInfo; // Inner object. };
With these edits, we are able to present CoHotRod as a COM object supporting five distinct interfaces, even though four of these come from an aggregated inner object.
Step Four: Blind Auto Aggregation
Recall that the COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND macro may be used to forward all client interface requests to the inner object, as well as automatically create the aggregated object when queried for. This ATL macro removes the need for your outer class to explicitly call CoCreateInstance(). Use the ATL Object Wizard to insert a new Simple Object named CoJeep, with support for a custom vTable interface. Once you bring in your new ATL vehicle, remove the generated ICoJeep from your COM map, IDL file, and inheritance chain (thus removing this interface from your project). Add support for IDragRace to CoJeep, and provide a prototype of the DragRace() method. Your CoJeep header file should appear as so:
// For all the off-roaders in the room. class ATL_NO_VTABLE CCoJeep : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoJeep, &CLSID_CoJeep>, public IDragRace { public: CCoJeep() { } BEGIN_COM_MAP(CCoJeep) COM_INTERFACE_ENTRY(IDragRace) END_COM_MAP() ... STDMETHOD(DragRace)(); };
Your IDL file should have no mention of ICoJeep, while the DragRace() method should provide an implementation appropriate to a fine Jeep:
// Jeeps don't drag race much... STDMETHODIMP CCoJeep::DragRace() { MessageBox(NULL, "Dude! I'm goin' offroadin'.", "Jeep message", MB_OK | MB_SETFOREGROUND); return S_OK; }
Now we need to aggregate the inner ATL CoCar. Include your atltearoff.h file and add a public IUnknown pointer to CoJeep. When using automatic blind aggregation, you will still need to add the DECLARE_GET_CONTROLLING_UNKNOWN() macro to your outer class, and you will still need to Release() the public IUnknown interface in your FinalRelease(). What you will not need to do is create the inner object yourself. Simply by using the COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND macro in your COM map, the framework creates the inner object for you using CoCreateInstance(). Update your CoJeep as so:
// Using automatic blind aggregation. class ATL_NO_VTABLE CCoJeep : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCoJeep, &CLSID_CoJeep>, public IDragRace { public: CCoJeep() : pUnkInnerCar(NULL) { } DECLARE_GET_CONTROLLING_UNKNOWN() ... // Kill the inner object within FinalRelease() void FinalRelease() { if(pUnkInnerCar) pUnkInnerCar->Release(); } BEGIN_COM_MAP(CCoJeep) COM_INTERFACE_ENTRY(IDragRace) COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND(pUnkInnerCar, CLSID_ATLCoCar) END_COM_MAP() public: STDMETHOD(DragRace)(); IUnknown *pUnkInnerCar; };
And finally, update the IDL code for your CoJeep by listing the following interfaces of the inner object:
// Expose interfaces to object browsers.
coclass CoJeep { [default] interface ICreate; interface IStats; interface IDragRace; interface IEngine; interface IOwnerInfo; };
Step Five: Update CoMiniVan
In this step, we will add support for the IDragRace interface to the CoMiniVan object. You may wish to use the Implement Interface Wizard for this purpose, or make the necessary edits by hand. Either way, provide an implementation of DragRace() for your minivan:
// No offense, but... STDMETHODIMP CCoMiniVan::DragRace() { MessageBox(NULL, "Dude, You have a minivan. You can't drag race.", "Minivan message", MB_OK | MB_SETFOREGROUND); return S_OK; }
Now then! Let's recap what we have been working through in this ATLVehicles project. First we made use of COM containment by building a CoMiniVan object which contained the ATLCoCar object. Next, we added two COM objects (CoJeep and CoHotRod), each using COM aggregation to reuse the inner ATLCoCar. All of the coclasses defined in the ATLVehicles project support the IDragRace interface and expose various interfaces of the inner contained/aggregated CoCar.
Step Six: Update Your VB Client
To finish up, we will illustrate interface-based polymorphism from within a VB client. Open your previous VB tester, and create a simple GUI to create a CoJeep (and exercise some interfaces), a CoHotRod (and exercise some interfaces), and a separate button named DragRace (see Figure 8-9).
The code of interest here is what is behind the Drag Race click event. As each coclass supports the IDragRace interface, we can treat them in the same way (that's polymorphism). So, add some VB code to create an array of IDragRace pointers, and set each to one of your ATL vehicles. You may then loop over this array, and ask each car to participate:
' Interface-based polymorphism in VB. ' Private Sub btnDragRace_Click() Dim ardrag(2) As IDragRace Set ardrag(0) = New CoHotRod Set ardrag(1) = New CoJeep Set ardrag(2) = New CoMiniVan Dim i As Integer For i = 0 To 2 ardrag(i).DragRace Next i End Sub
This same effect can be achieved in C++ as so:
// Interface-based polymorphism in C++. int main(int argc, char* argv[]) { CoInitialize(NULL); IDragRace* pDragRace[3] = {NULL}; // Make some cars. CoCreateInstance(CLSID_CoHotRod, NULL, CLSCTX_SERVER, IID_IDragRace, (void**)&(pDragRace[0])); CoCreateInstance(CLSID_CoMiniVan, NULL, CLSCTX_SERVER, IID_IDragRace, (void**)&(pDragRace[1])); CoCreateInstance(CLSID_CoJeep, NULL, CLSCTX_SERVER, IID_IDragRace, (void**)&(pDragRace[2])); // Drag race! for(int i = 0; i < 3; i++) pDragRace[i]->DragRace(); for(i = 0; i < 3; i++) pDragRace[i]->Release(); CoUninitialize(); return 0; }
| < Free Open Study > |
|