COMUSER - Nested Aggregation in EXE User |
Tutorial Home |
Previous Lesson |
Lesson List |
Next Lesson |
The COMUSER sample presents a series of COM objects, several of which were introduced in the COMOBJ code sample. These objects represent various types of cars, and they expose the following interfaces: ICar, IUtility, and ICruise. COMUSER works in conjunction with the separate COMOBJ.DLL, which creates COCar, COUtilityCar, and COCruiseCar COM objects.
COMUSER.EXE creates its own COUtilityCruiseCar COM object, which is constructed by reusing the COCruiseCar COM object by aggregation and augmenting it with the IUtility interface. Because the COCruiseCar COM object class is also an aggregate--that is, it reuses an inner COCar object by aggregation--COMUSER illustrates nested aggregation of COM objects.
For functional descriptions and a tutorial code tour of COMUSER, see the Code Tour section in COMUSER.HTM. For details on the external user operation of COMUSER, see both the Usage and Operation sections in COMUSER.HTM. To read COMUSER.HTM, run TUTORIAL.EXE in the main tutorial directory and click the COMUSER lesson in the table of lessons. You can also achieve the same thing by clicking the COMUSER.HTM file after locating the main tutorial directory in the Windows Explorer. See also COMOBJ.HTM in the main tutorial directory for more details on how COMOBJ works and exposes its services to COMUSER. You must build COMOBJ.DLL before building COMUSER. The makefile for COMOBJ copies the necessary COMOBJ.H, COMOBJ.LIB, and COMOBJ.DLL files to the appropriate sibling directories once the files are built.
In general, to set up your system to build and test the code samples in this COM Tutorial series, see the global TUTORIAL.HTM file for details. The supplied makefile is Microsoft NMAKE-compatible. To create a debug build, issue the NMAKE command in the Command Prompt window.
COMUSER is a simple application that you can execute directly from Windows in the normal manner or from the command prompt. No command line parameters are recognized by COMUSER.
The client sample and other related samples must be compiled before you can run the client. For more details on building the samples, see Building the Code Samples.
If you have already built the appropriate samples, COMUSER.EXE is the client executable to run for this sample.
The COMUSER.EXE application provides the main user interface for this lesson. It exercises the associated, but independent, COMOBJ.DLL. Here is a summary of operation from the standpoint of COMUSER.EXE as a controller of COMOBJ.DLL.
The COM objects that are used in the COMUSER and COMOBJ code samples represent sport utility vehicles. We invent some basic feature sets for modeling such car objects. These feature sets are implemented as interfaces to COM objects. The ICar interface provides some basic car behavior: Shift, Clutch, Speed, and Steer. The IUtility interface provides off-road utility systems: Offroad and Winch. The ICruise interface provides automatic cruise control facilities: Engage and Adjust.
COMUSER.EXE provides menus for creating, releasing, and invoking methods for four COM objects: COCar, COUtilityCar, COCruiseCar, and COUtilityCruiseCar. These objects have combinations of the ICar, IUtility, and ICruise interfaces. COCar objects expose the ICar interface. COUtilityCar objects expose the ICar and IUtility interfaces. COCruiseCar objects expose the ICar and ICruise interfaces. COUtilityCruiseCar objects expose the ICar, ICruise, and IUtility interfaces. As a result, COCar objects have only the basic car behavior (ICar). COUtilityCar objects have basic car behavior (ICar) with sport utility systems (IUtility). COCruiseCar objects have basic car behavior (ICar) with an automatic cruise control system (ICruise). COUtilityCruiseCar objects have basic car behavior (ICar), a cruise control system (ICruise), and a sport utility system (IUtility).
COCar is constructed as an aggregatable COM object with a native implementation of the ICar interface. COUtilityCar is constructed using containment and is implemented in COMOBJ.DLL. For details, see the COMOBJ lesson. COCruiseCar is constructed using aggregation and is also implemented in COMOBJ.DLL. COUtilityCruiseCar is constructed using aggregation and is implemented in COMUSER.EXE. COUtilityCruiseCar aggregates COCruiseCar, so nested aggregation is thereby illustrated in this lesson.
COMUSER.EXE presents a menu for each of these four main COM objects. Each menu has items that call the methods of the various available interfaces. The code samples (both COMUSER and COMOBJ) have trace message log statements throughout. When you exercise the objects from COMUSER.EXE, the main COMUSER window will display a log of internal activity in these COM objects.
Menu Selection: File/Exit
Exits COMUSER.
Menu Selection: Car/Create
Creates a COCar COM object. A checkmark beside the menu item indicates that there is already an instance of the object.
Menu Selection: Car/Release
Releases the COCar COM object.
Menu Selection: Car/ICar::Shift
Calls the ICar::Shift method on the COCar object.
Menu Selection: Car/ICar::Clutch
Calls the ICar::Clutch method on the COCar object.
Menu Selection: Car/ICar::Speed
Calls the ICar::Speed method on the COCar object.
Menu Selection: Car/ICar::Steer
Calls the ICar::Steer method on the COCar object.
Menu Selection: UtilityCar/Create
Creates the COUtilityCar COM object. A checkmark beside the menu item indicates that there is already an instance of the object.
Menu Selection: UtilityCar/Release
Releases the COUtilityCar COM object.
Menu Selection: UtilityCar/ICar::Shift
Calls the ICar::Shift method on the COUtilityCar object.
Menu Selection: UtilityCar/ICar::Clutch
Calls the ICar::Clutch method on the COUtilityCar object.
Menu Selection: UtilityCar/ICar::Speed
Calls the ICar::Speed method on the COUtilityCar object.
Menu Selection: UtilityCar/ICar::Steer
Calls the ICar::Steer method on the COUtilityCar object.
Menu Selection: UtilityCar/IUtility::Offroad
Calls the IUtility::Offroad method on the COUtilityCar object.
Menu Selection: UtilityCar/IUtility::Winch
Calls the IUtility::Winch method on the COUtilityCar object.
Menu Selection: CruiseCar/Create
Creates the COCruiseCar COM object. A checkmark beside the menu item indicates that there is already an instance of the object.
Menu Selection: CruiseCar/Release
Releases the COCruiseCar COM object.
Menu Selection: CruiseCar/ICar::Shift
Calls the ICar::Shift method on the COCruiseCar object.
Menu Selection: CruiseCar/ICar::Clutch
Calls the ICar::Clutch method on the COCruiseCar object.
Menu Selection: CruiseCar/ICar::Speed
Calls the ICar::Speed method on the COCruiseCar object.
Menu Selection: CruiseCar/ICar::Steer
Calls the ICar::Steer method on the COCruiseCar object.
Menu Selection: CruiseCar/ICruise::Engage
Calls the ICruise::Engage method on the COCruiseCar object.
Menu Selection: CruiseCar/ICruise::Adjust
Calls the ICruise::Adjust method on the COCruiseCar object.
Menu Selection: UtilityCruiseCar/Create
Creates the COUtilityCruiseCar COM object. A checkmark beside the menu item indicates that there is already an instance of the object.
Menu Selection: UtilityCruiseCar/Release
Releases the COUtilityCruiseCar COM object.
Menu Selection: UtilityCruiseCar/ICar::Shift
Calls the ICar::Shift method on the COUtilityCruiseCar object.
Menu Selection: UtilityCruiseCar/ICar::Clutch
Calls the ICar::Clutch method on the COUtilityCruiseCar object.
Menu Selection: UtilityCruiseCar/ICar::Speed
Calls the ICar::Speed method on the COUtilityCruiseCar object.
Menu Selection: UtilityCruiseCar/ICar::Steer
Calls the ICar::Steer method on the COUtilityCruiseCar object.
Menu Selection: UtilityCruiseCar/ICruise::Engage
Calls the ICruise::Engage method on the COUtilityCruiseCar object.
Menu Selection: UtilityCruiseCar/ICruise::Adjust
Calls the ICruise::Adjust method on the COUtilityCruiseCar object.
Menu Selection: UtilityCruiseCar/IUtility::Offroad
Calls the IUtility::Offroad method on the COUtilityCruiseCar object.
Menu Selection: UtilityCruiseCar/IUtility::Winch
Calls the IUtility::Winch method on the COUtilityCruiseCar object.
Menu Selection: Log/Clear
Clears the trace message log display.
Menu Selection: Log/Logging
Toggles the trace message logging facility on or off. A checkmark beside the menu item indicates that logging is on. Logging can be engaged but simply turned on or off. Unchecking this command turns the trace message logging facility off but does not disengage the logging mechanisms.
Menu Selection: Log/Copy
Copies the current contents of the trace message log to the Windows Clipboard.
Menu Selection: Help/COMUSER Tutorial
Opens the COMUSER.HTM tutorial file in the Web browser.
Menu Selection: Help/COMOBJ Tutorial
Opens the COMOBJ.HTM tutorial file in the Web browser.
Menu Selection: Help/Read Source File
Displays the Open common dialog box so you can open a source file from this lesson or another one in the Windows Notepad.
Menu Selection: Help/About COMUSER
Displays the About dialog box for this application, a standard part of this series of code samples. The code illustrates how to program the use of the CAboutBox class provided by APPUTIL.LIB.
Menu Selection: Help/About COMOBJ
Displays the About dialog box for COMOBJ.DLL, which is used by this application. In this series of code samples, partner DLLs like COMOBJ are given their own About dialog box in the native resources of the DLL. This menu item calls the DLL function that displays this dialog box.
Files Description COMUSER.TXT Short sample description. MAKEFILE The generic makefile for building the code sample application of this tutorial lesson. COMUSER.H The include file for the COMUSER application. Contains class declarations, function prototypes, and resource identifiers. COMUSER.CPP The main implementation file for COMUSER.EXE. Has WinMain and CMainWindow implementation. Also has the main menu dispatching. COMUSER.RC The application resource definition file. COMUSER.ICO The application icon resource. UTCRUCAR.H The class declaration for the COUtilityCruiseCar COM object. UTCRUCAR.CPP Implementation file for the COUtilityCruiseCar COM object. Also has the definition of the CreateUtilityCruiseCar function.
This code sample is based on the code in DLLUSER, which in turn is based on the code in EXESKEL. See the code tours in those samples for more details on using the application skeleton and the DLL.
COMUSER uses many of the utility classes and services provided by APPUTIL. For more details on APPUTIL, study the APPUTIL library source code in the sibling \APPUTIL directory and APPUTIL.HTM in the main tutorial directory.
COMUSER illustrates various kinds of COM objects in various settings. The object created and used in COMUSER is of class COUtilityCruiseCar. It is constructed by aggregation of the COCruiseCar COM object, which is implemented in COMOBJ.DLL. See the COMOBJ code tour for more information about the COCar, COUtilityCar, and COCruiseCar objects.
In addition to showing how COUtilityCruiseCar is constructed, this lesson demonstrates how the COMUSER.EXE, as a client of the COM objects implemented in both COMUSER and COMOBJ.DLL, creates, exercises, and releases those objects.
We start in COMUSER.CPP. The following include statements are used to bring in include files:
#include <windows.h> #include <ole2.h> #include <initguid.h> #include <commdlg.h> #include <apputil.h> #include <icars.h> #include <carguids.h> #include <comobj.h> #include "comuser.h" #include "utcrucar.h"
Of special note is INITGUID.H. This file must be included only once in each executable. It ensures that any GUIDs defined in this application will be instantiated as named data in the data segment of the EXE. If you forget to include this file, you will get unresolved external references to the IID_IMyInterface GUIDs during linking.
The ICARS.H and CARGUIDS.H files, located in the sibling \INC directory, are included to get the car-related interface abstract base class declarations and their associated GUID definitions.
The COMOBJ lesson introduced the MsgBox and MsgLog facilities, and their use is continued here. The LOG* macro calls in the source code are trace statements that log and display appropriate conditions at run time.
After the message log is created, a global MsgLog pointer is assigned. Such global variables are usually avoided in object-oriented programming, but they conveniently support the centrally defined logging macros used throughout this code sample series. A definition of this same-named global variable, g_pMsgLog, is also needed inside such subordinate DLLs as COMOBJ.DLL for the logging macros to work there. Because of this, the actual pMsgLog pointer for this current application's message log must be passed to the DLL. The ComObjInitMsgLog call below passes this needed pointer to the DLL so that its logged internal behavior is displayed in COMUSER.EXE's logging display.
// Create the Message Log ListBox as a child // window in the Main Window. if (m_pMsgLog->Create(m_hInst, m_hWnd, TRUE)) { // Assign the global MsgLog pointer. g_pMsgLog = m_pMsgLog; // Use macro to log messages. LOGID(IDS_START_MESSAGE_LOG); // Since we're exploiting the COMOBJ DLL and we want it to use // this same Message Log facility, we pass it a pointer to our // particular instance's CMsgLog object. ComObjInitMsgLog(g_pMsgLog); }
In the CMainWindow::DoMenu method, we see how the menu choices are dispatched. We'll take a typical example from the COM object implemented in this application. The following fragment shows the Create command from the UtilityCruiseCar menu.
case IDM_UCRU_CREATE: LOG("E: === UtilityCruiseCar Menu: Create."); if (NULL == m_pUtilityCruiseCar) { hr = CreateUtilityCruiseCar( NULL, IID_IUnknown, (PPVOID)&m_pUtilityCruiseCar); if (SUCCEEDED(hr)) { ::CheckMenuItem( hMenu, IDM_CAR_CREATE, MF_BYCOMMAND | MF_CHECKED); } else LOG("C: ???? Car creation failed."); } else LOG("E: ???? UtilityCruiseCar already exists."); break;
The CreateUtilityCruiseCar function is called. This function is defined in the UTCRUCAR.CPP file, part of this application. NULL is passed for the first argument because at this top level of usage we are not aggregating the new COUtilityCruiseCar object as part of some outer object. Once the object is created, the output argument, m_pUtilityCruiseCar, will write a pointer to the IUnknown interface of the new object to m_pUtilityCruiseCar. We cache this object pointer for subsequent use of the COM object. Following the rules of COM, the reference count on this pointer is incremented with a call to AddRef from the CreateUtilityCruiseCar function, and it will need to be decremented with a call to Release when it is no longer needed. A checkmark is displayed beside the UtilityCruiseCar/Create menu item to indicate that there is currently an instance of this object.
The following fragment handles the matching release of the COUtilityCruiseCar COM object.
case IDM_UCRU_RELEASE: LOG("E: === UtilityCruiseCar Menu: Release."); if (NULL != m_pUtilityCruiseCar) { RELEASE_INTERFACE(m_pUtilityCruiseCar); ::CheckMenuItem( hMenu, IDM_CAR_CREATE, MF_BYCOMMAND | MF_UNCHECKED); } else LOG("E: ???? No UtilityCruiseCar to Release."); break;
The UtilityCruiseCar/Create menu item is unchecked to show that the object no longer exists. The RELEASE_INTERFACE macro used above is defined in APPUTIL.H:
#define RELEASE_INTERFACE(p)\ {\ IUnknown* pTmp = (IUnknown*)p;\ p = NULL;\ if (NULL != pTmp)\ pTmp->Release();\ }
If the m_pUtilityCruiseCar object pointer is not NULL, a COUtilityCruiseCar object currently exists, and RELEASE_INTERFACE calls the interface's Release method. The pTmp pointer is used and the input interface pointer p is set to NULL prior to release to provide more robust behavior in multi-threaded programs.
A pointer like m_pUtilityCruiseCar is commonly referred to as a pointer to the object. Technically, it is a pointer to the IUnknown interface of that object. Once we have this pointer, we can access the object's other interfaces and make the object do things by calling their methods. Here is an example:
case IDM_UCRU_WINCH: LOG("E: === UtilityCruiseCar Menu: IUtility::Winch"); if (GetInterface(m_pUtilityCruiseCar, IID_IUtility, (PPVOID)&pIUtility)) { LOG("E: --Calling pIUtility->Winch"); pIUtility->Winch(30); LOG("E: --Releasing pIUtility"); pIUtility->Release(); } break;
This fragment calls the Winch method of the IUtility interface on the COUtilityCruiseCar object. The GetInterface call, defined in COMUSER.CPP, is a simple wrapper for IUnknown::QueryInterface. It is used to reduce code clutter by isolating some of the logging activity associated with the QueryInterface call in this tutorial. QueryInterface is called to obtain the COUtilityCruiseCar object's IUtility interface. The pointer to IUtility is used to call the Winch method. When the IUtility interface is no longer needed, Release is called on the pIUtility pointer.
We now look at the UtilityCruiseCar COM object itself (in UTCRUCAR.H and UTCRUCAR.CPP). The UtilityCruiseCar COM object class is declared in UTCRUCAR.H.
class COUtilityCruiseCar : public IUnknown { public: // Main Object Constructor & Destructor. COUtilityCruiseCar(IUnknown* pUnkOuter); ~COUtilityCruiseCar(void); // A general method for initializing a newly created // UtilityCruiseCar. HRESULT Init(void); // IUnknown methods. Main object, non-delegating. STDMETHODIMP QueryInterface(REFIID, PPVOID); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); private: // We declare nested class interface implementations here. // We implement the IUtility interface (ofcourse) in this // COUtilityCruiseCar COM object class. This is the native // interface that we are using as an augmentation to the // existing COCruiseCar COM object class. class CImpIUtility : public IUtility { public: // Interface Implementation Constructor & Destructor. CImpIUtility(COUtilityCruiseCar* pBackObj, IUnknown* pUnkOuter); ~CImpIUtility(void); // IUnknown methods. STDMETHODIMP QueryInterface(REFIID, PPVOID); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); // IUtility methods. STDMETHODIMP Offroad(short nGear); STDMETHODIMP Winch(short nRpm); private: // Data private to this interface implementation of // IUtility. ULONG m_cRefI; // Interface Ref Count // (debug). COUtilityCruiseCar* m_pBackObj; // Parent Object back // pointer. IUnknown* m_pUnkOuter; // Outer unknown for // Delegation. }; // Make the otherwise private and nested IUtility interface // implementation a friend to COM object instantiations of // this selfsame COUtilityCruiseCar COM object class. friend CImpIUtility; // Private data of COUtilityCruiseCar COM objects. // Nested IUtility implementation instantiation. This IUtility // interface is instantiated inside this COUtilityCruiseCar // object as a native interface. CImpIUtility m_ImpIUtility; // Main Object reference count. ULONG m_cRefs; // Outer unknown (aggregation delegation). Used when this // COUtilityCruiseCar object is itself being aggregated. IUnknown* m_pUnkOuter; // We need to save the IUnknown interface pointer on the // COCruiseCar object that we aggregate. We use this when // we need to delegate IUnknown calls to this aggregated // inner object. IUnknown* m_pUnkCruiseCar; };
The IUtility interface is implemented in this COM object as a nested class. The embedded CImpIUtility is declared in the private section of the COUtilityCruiseCar object class. An instance of CImpIUtility is also created:
// Nested IUtility implementation instantiation. This IUtility // interface is instantiated inside this COUtilityCruiseCar // object as a native interface. CImpIUtility m_ImpIUtility;
As we'll see, COUtilityCruiseCar is itself constructed by aggregating the COCruiseCar object with this IUtility implementation. That is what the m_pUnkCruiseCar pointer, also in the COUtilityCruiseCar private area, is for. It caches a pointer to the aggregated COCruiseCar object. The pointer is used to delegate certain IUnknown calls to the inner object's IUnknown interface.
For the case when a COUtilityCruiseCar object is itself aggregated by an outer COM object, it has an m_pUnkOuter delegation pointer in its private data area. The value of this pointer is set when the COUtilityCruiseCar object is created. Because COMUSER does not aggregate this object, it sets the pointer to NULL.
CImpIUtility itself also has a private data member, m_pUnkOuter, for supporting aggregation and delegation. This pointer is used to delegate calls to the IUtility interface's IUnknown to either the COUtilityCruiseCar object (when it is not aggregated) or to the controlling IUnknown (when the COUtilityCruiseCar object is aggregated). The m_pBackObj pointer is for convenient use inside any of the IUtility method functions to reference members of the COUtilityCruiseCar object.
As an embedded class, CImpIUtility needs to be made a friend class to COUtilityCruiseCar so that CImpIUtility's methods can access the private data in COUtilityCruiseCar.
The method definitions for the COUtilityCruiseCar class are in UTCRUCAR.CPP. As we observed in the COMOBJ code sample, the COUtilityCruiseCar constructor uses a member initializer, m_ImpIUtility(this, pUnkOuter), to pass the 'this' and pUnkOuter arguments to the CImpIUtility constructor, which is executed just prior to the main body of the COUtilityCruiseCar constructor. Both of the argument values are available when the member initializer is executed. In these code samples, the successful use of the nested classes technique for interface implementations depends on the use of such member initializers. Here is the constructor:
COUtilityCruiseCar::COUtilityCruiseCar(IUnknown* pUnkOuter) : m_ImpIUtility(this, pUnkOuter) { // Zero the COM object's reference count. m_cRefs = 0; // No AddRef necessary if non-NULL, as this COM object's // lifetime is totally coupled with the controlling Outer // object's lifetime. m_pUnkOuter = pUnkOuter; // Zero the pointer to the aggregated COCruiseCar object's // IUnknown interface (for delegation of IUnknown calls to it). m_pUnkCruiseCar = NULL; LOGF1("E: COUtilityCruiseCar Constructor. m_pUnkOuter=0x%X.", m_pUnkOuter); return; }
The COUtilityCruiseCar::Init method is almost identical to the COCruiseCar::Init that we studied in the COMOBJ code sample.
The CreateCruiseCar function is implemented in COMOBJ.DLL. CreateCruiseCar calls QueryInterface which assigns its output m_pUnkCruiseCar object pointer. This is a pointer to the aggregated COCruiseCar object's IUnknown. We cache this pointer in the COUtilityCruiseCar object for future use in delegating IUnknown calls to COCruiseCar. An example of such delegation is in COUtilityCruiseCar object's IUnknown definition of QueryInterface.
STDMETHODIMP COUtilityCruiseCar::QueryInterface( REFIID riid, PPVOID ppv) { HRESULT hr = E_NOINTERFACE; *ppv = NULL; if (IID_IUnknown == riid) { *ppv = this; LOG("E: COUtilityCruiseCar::QueryInterface. 'this' pIUnknown returned."); } else if (IID_IUtility == riid) { // This IUtility interface is implemented in this // COUtilityCruiseCar object and might be called // a native interface of COUtilityCruiseCar. *ppv = &m_ImpIUtility; LOG("E: COUtilityCruiseCar::QueryInterface. pIUtility returned."); } if (NULL != *ppv) { // We've handed out a pointer to an interface so obey the COM // rules and AddRef its reference count. ((LPUNKNOWN)*ppv)->AddRef(); hr = NOERROR; } else if (IID_ICar == riid) { LOG("E: COUtilityCruiseCar::QueryInterface. ICar delegating."); // We didn't implement the ICar interface in this // COUtilityCruiseCar object. The aggregated inner // object (CruiseCar) is contributing the ICar // interface to this present composite or aggregating // UtilityCruiseCar object. So, to satisfy a QI request // for the ICar interface, we delegate the QueryInterface // to the inner object's IUnknown. hr = m_pUnkCruiseCar->QueryInterface(riid, ppv); } else if (IID_ICruise == riid) { LOG("E: COUtilityCruiseCar::QueryInterface. ICruise delegating."); // We didn't implement the ICruise interface in this // COUtilityCruiseCar object. The aggregated inner object // (CruiseCar) is contributing the ICruise interface to this // present composite or aggregating UtilityCruiseCar object. // As above, we delegate this QI to the aggregated object's // IUnknown. hr = m_pUnkCruiseCar->QueryInterface(riid, ppv); } return (hr); }
The IID_IUnknown and IID_IUtility cases are familiar, and if we pass one of these interface pointers by request, we increment the interface's reference count with a call to its AddRef method. The IID_IUtility interface is exposed as a native interface on this COM object, so it's easy to pass a pointer to it. For the IID_ICar and IID_ICruise interfaces, however, we need to pass the request to the aggregated inner object where they are implemented. We use the cached m_pUnkCruiseCar object pointer to delegate the QueryInterface call to aggregated objects that are providing implementations of those interfaces to the COUtilityCruiseCar COM object.
Prior to deleting COUtilityCruiseCar, the COUtilityCruiseCar::Release method artificially increments the object's reference count, as was discussed for the COCruiseCar object in the COMOBJ code sample.
The COUtilityCruiseCar::CImpIUtility::CImpIUtility constructor uses the same logic to assign its own IUnknown delegation pointers as that discussed for the COCruiseCar object in the COMOBJ code sample.
The COUtilityCruiseCar::CImpIUtility::Offroad method has been set up to show something a little different. Here we show the aggregation rules when an outer object needs to call methods of interfaces to the inner aggregated objects. The Offroad method stops the car (sets its speed to 0) in order to shift the transfer case to offroad 4-wheel drive Low gear (nGear = 3). In the COUtilityCruiseCar object's implementation of IUtility::Offroad, we need to call the ICar interface's Speed method if the Offroad request is for 4-wheel drive low speed. But this ICar interface is implemented in the inner COCruiseCar object. We must call QueryInterface to get a pointer to it and then carefully manage the reference count on the outer object, for reasons we will discuss in a moment.
STDMETHODIMP COUtilityCruiseCar::CImpIUtility::Offroad( short nGear) { HRESULT hr; ICar* pICar = NULL; // In our fantasy Sport-Utility Car world we may need to stop // the car before switching to 4-Wheel drive low // (nGear == 3 for 4L). Let's assume so because it's a // convenient excuse to show this aggregating COUtilityCruiseCar // outer object using one of the interfaces (ICar) // of its aggregated COCruiseCar inner object. COMUSER // gracefully cooperates in this by invoking this Offroad // method from its UtilityCruiseCar menu with nGear == 3. if (3 == nGear) { hr = m_pBackObj->m_pUnkCruiseCar->QueryInterface( IID_ICar, (PPVOID)&pICar); if (SUCCEEDED(hr)) { m_pUnkOuter->Release(); pICar->Speed(0); m_pUnkOuter->AddRef(); pICar->Release(); } } LOGF1("E: COUtilityCruiseCar::CImpIUtility::Offroad. Called. nGear=%i.",nGear); return NOERROR; }
The interface's m_pBackObj pointer provides a way to access the cached pointer to the aggregated COCruiseCar object, m_pUnkCruiseCar. Because the QueryInterface method of the inner object is delegated as part of an aggregated object, it will increment the reference count of the outermost aggregating object by calling the AddRef method of the controlling IUnknown. This outermost aggregating object may not be the COUtilityCruiseCar object. Here's the problem. A aggregate object is using an interface on itself from inside itself, but the call to AddRef is delegated to the outermost aggregating object and thus falsely records an outstanding external reference to it. As a result, the lifetime of that outer aggregating object is no longer fully controlled by releases of actual external references held to it.
To put the reference count in a better state for the duration that the outer object holds a reference to an aggregated inner object's interface, Release is explicitly called on the outer object after the QueryInterface call (to offset the internal delegated AddRef call). The acquired inner interface is now used and could be held for a lengthy period. To properly release it, AddRef is called explicitly on the outer object, followed by a call to Release on the acquired inner object's interface pointer. This sequence is necessary to avoid recursive or reentrant destruction during the process. This practice is recommended under Win32, especially if the inner interface pointer is held for some time and if the inner object is in a different process (or even on a different machine) from that of the outer object.
At the end of UTCRUCAR.CPP is the CreateUtilityCruiseCar function, which creates an instance of the COUtilityCruiseCar COM object. We saw a call to this function above in the code for the UtilityCruiseCar menu's Create item.
HRESULT CreateUtilityCruiseCar( IUnknown* pUnkOuter, REFIID riid, PPVOID ppv) { HRESULT hr; COUtilityCruiseCar* pCob; LOGF1("E: CreateUtilityCruiseCar. pUnkOuter=0x%X.",pUnkOuter); // If the creation call is requesting aggregation (pUnkOuter != // NULL), the COM rules state the IUnknown interface MUST also be // concomitantly requested. If it is not so requested // (riid != IID_IUnknown), then an error must be returned // indicating that no aggregate creation of the COUtilityCruiseCar // COM Object can be performed using anything other than a // controlling IUnknown interface. if (NULL != pUnkOuter && riid != IID_IUnknown) hr = CLASS_E_NOAGGREGATION; else { // Instantiate a COUtilityCruiseCar COM Object. pCob = new COUtilityCruiseCar(pUnkOuter); if (NULL != pCob) { // If we have succeeded in instantiating the // COUtilityCruiseCar object, we initialize it // to offer its interfaces. hr = pCob->Init(); if (SUCCEEDED(hr)) { // We QueryInterface this new COM Object not only to // deposit the main interface pointer to the caller's // pointer variable, but to also automatically bump // the Reference Count on the new COM Object after // handing out this *ppv external reference to it. hr = pCob->QueryInterface(riid, (PPVOID)ppv); } } else hr = E_OUTOFMEMORY; } if (SUCCEEDED(hr)) LOGF1("E: CreateUtilityCruiseCar Succeeded. *ppv=0x%X.",*ppv); return hr; }
The arguments to this function permit the object to be created as part of an even grander aggregation. If the first pUnkOuter parameter is non-NULL, the COUtilityCruiseCar object is to be aggregated, and all the subsequent logic will support this aggregation. For another such example, see the code for the COCruiseCar object in the COMOBJ directory. An instance of the COM object class is first created (memory is allocated for it, and its constructor is executed). The object's Init() member function is then called to create any subordinate objects and interfaces. In this case, the COCruiseCar COM object is created and aggregated into the new COUtilityCruiseCar object. If this step succeeds, QueryInterface is called to obtain the IUnknown of the outer object. On the way, this writes that object pointer at the location specified by the ppv output argument. On the way, this also calls the AddRef method to increment the reference count on the new object, because a pointer to it was passed to an outside consumer--in this case, COMUSER.EXE.
You can experiment with the COMUSER menus to watch the internal activity of these COM objects. Here we will tour some representative trace logs. This one is for the menu selection UtilityCruiseCar\Create:
E: === UtilityCruiseCar Menu: Create. E: CreateUtilityCruiseCar. pUnkOuter=0x0. E: COUtilityCruiseCar::CImpIUtility Constructor. Non-Aggregating. E: COUtilityCruiseCar Constructor. m_pUnkOuter=0x0. E: COUtilityCruiseCar::Init. D: CreateCruiseCar. pUnkOuter=0x770694. D: COCruiseCar::CImpICruise Constructor. Aggregating. D: COCruiseCar Constructor. m_pUnkOuter=0x770694. D: COCruiseCar::Init. D: CreateCar. pUnkOuter=0x770694. D: COCar::CImpICar Constructor. Aggregating. D: COCar Constructor. m_pUnkOuter=0x770694. D: COCar::QueryInterface. 'this' pIUnknown returned. D: COCar::AddRef. New cRefs=1. D: CreateCar Succeeded. *ppv=0x660698. D: COCruiseCar::QueryInterface. 'this' pIUnknown returned. D: COCruiseCar::AddRef. New cRefs=1. D: CreateCruiseCar Succeeded. *ppv=0x660674. E: COUtilityCruiseCar::QueryInterface. 'this' pIUnknown returned. E: COUtilityCruiseCar::AddRef. New cRefs=1. E: CreateUtilityCruiseCar Succeeded. *ppv=0x770694.
The 'E:' or 'D:' at the beginning of each line indicates, respectively, that execution was in the .EXE or in the .DLL.
First, COMUSER calls CreateUtilityCruiseCar (passing a NULL pUnkOuter, because it is simply creating a COUtilityCruiseCar object without aggregating one). Then a COUtilityCruiseCar object is created, causing first the constructor for the IUtility implementation and then that for the COUtilityCruiseCar object to be executed. COUtilityCruiseCar::Init is then called, and it calls the CreateCruiseCar function in COMOBJ.DLL to create an aggregated COCruiseCar object. This object is to be aggregated, so a non-NULL pUnkOuter is passed. When the COCruiseCar COM object is created, the constructors for CImpICruise and then COCruiseCar are called. The CImpICruise interface constructor is passed the non-NULL pUnkOuter. This ICruise interface implementation will later use this pointer to delegate its IUnknown calls to the controlling IUnknown. Next, CreateCruiseCar calls the COCruiseCar object's Init method to create any subordinate COM objects.
Because a COCruiseCar object obtains its ICar features from an aggregated COCar COM object, CreateCar is called within COMOBJ.DLL. As before, we see the constructors for CImpICar and COCar and that they are passed a non-NULL pUnkOuter, indicating aggregation. The same pUnkOuter value is passed to these nested aggregated objects. This is how all of them can delegate directly to the controlling IUnknown. As we back out of the call tree, each create function uses a call to QueryInterface to pass a pointer to an appropriate IUnknown and to increment (via calls to AddRef) the appropriate reference count. Each object then ends up with a reference count of 1.
Now that we have created a UtilityCruiseCar, here's the trace log for menu selection UtilityCruiseCar\ICar::Shift:
E: === UtilityCruiseCar Menu: ICar::Shift E: --Obtaining Interface Pointer. E: COUtilityCruiseCar::QueryInterface. ICar delegating. D: COCruiseCar::QueryInterface. ICar delegating. D: COCar::QueryInterface. pICar returned. D: COCar::CImpICar::Addref. Delegating. New cI=1. E: COUtilityCruiseCar::AddRef. New cRefs=2. E: Interface obtained. *ppv=0x6606A4 E: --Calling pICar->Shift D: COCar::CImpICar::Shift. Called. nGear=1. E: --Releasing pICar D: COCar::CImpICar::Release. Delegating. New cI=0. E: COUtilityCruiseCar::Release. New cRefs=1.
First, an ICar interface pointer on the COUtilityCruiseCar object must be obtained by calling QueryInterface. This call delegates to the next inner object that has the interface being asked for--in this case, the COCruiseCar object. This COM object's QueryInterface in turn does the same thing: it delegates the request to the ICar interface that is implemented in the COCar COM object. The pICar pointer is finally obtained from the COCar object's QueryInterface. The AddRef call from QueryInterface is delegated directly to the controlling IUnknown (in this case, COUtilityCruiseCar::AddRef). This call increments the COUtilityCruiseCar object's reference count to 2. An outside agent is now holding a reference to the ICar interface on this aggregate object. That same outside agent (COMUSER.EXE) has also been holding a reference to the object's IUnknown, which explains the reference count of 2. The ICar interface pointer is used to directly call the implementation of the Shift method in the COCar object. The pointer to the ICar interface is then released, which causes a delegation to the controlling IUnknown's Release method, upon which the object's reference count is decremented to a 1.
Here's the trace log for menu item UtilityCruiseCar\Release:
E: === UtilityCruiseCar Menu: Release. E: COUtilityCruiseCar::Release. New cRefs=0. E: COUtilityCruiseCar::Destructor. D: COCruiseCar::Release. New cRefs=0. D: COCruiseCar::Destructor. D: COCar::Release. New cRefs=0. D: COCar::Destructor. D: COCar::CImpICar Destructor. D: COCruiseCar::CImpICruise Destructor. E: COUtilityCruiseCar::CImpIUtility Destructor.
This menu item causes a cascade of releases and object destruction. The initial call to Release decrements the outstanding reference count of 1 to 0, causing the entire COUtilityCruiseCar COM object to be deleted. The object's destructor is called, which has a call to Release the aggregated COCruiseCar COM object. That object's reference count is decremented from 1 to 0, causing the COCruiseCar object to be deleted. As above, its destructor releases the aggregated COCar object, decrementing its reference count to 0 and causing its destructor to run. As we wind back out of all these destructors, we see the automatic destructors of the ICar, ICruise,and IUtility 'ImpI' interface implementations. These required no explicit deletions, because the interfaces are implemented as nested classes. This is one of the benefits of the nested classes technique.