Examine reference counting issues under aggregation.
COM Reference Counting
Using the OuterCOMObj and InnerCOMObj objects from the last lesson, let's examine some issues related to reference counting in an aggregated object. We also need to extend our example to include COM servers. Assume that both OuterCOMObj and InnerCOMObj are implemented in in-process servers. OuterCOMObj is in OCOMSrv.dll, InnerCOMObj is in ICOMSrv.dll.
Reference Counting Mechanism:
Recall that COM objects use a reference counting mechanism to track interface and object usage to support
lifetime management of objects and servers.
COM Servers Track Reference counts
Recall from Basic COM course 1 that when a COM object gives away an interface pointer to a client, it increments a reference count on that pointer by calling AddRef. When the client is finished using a COM interface pointer, it calls Release on the pointer. Most implementations of Release decrement the reference counter, incremented in the AddRef call.
COM servers track reference counts on all COM objects implemented within the server. In an in-process COM server, COM calls DllCanUnloadNow to check all reference counts. If they are all zero, DllCanUnloadNow returns S_OK, and COM unloads the server.
The following code fragment depicts a client using the aggregation of OuterCOMObj and InnerCOMObj (remember, the client is not aware of the aggregation).
//Implementation of IPersistFile::Load
HRESULT CTextRender::Load(char *pszFile, DWORD grfMode) {
int hFile;
DWORD cch;
IMalloc * pIMalloc;
HRESULT hr;
/*
* Open the file and seek to the end to set the
* cch variable to the length of the file.
*/
hr=CoGetMalloc(MEMCTX_TASK, &pIMalloc);
if (FAILED(hr))
//Close file and return failure
psz=pIMalloc->Alloc(cch);
pIMalloc->Release();
if (NULL==psz)
//Close file and return failure
//Read text into psz buffer and close file
//Save memory pointer and return success
m_pszText=psz;
return NOERROR;
}
If an object will make many allocations throughout it's lifetime, it makes sense to call CoGetMalloc once when the object is created, store the IMalloc pointer in the object (m_pIMalloc or such), and call IMalloc::Release when the object is destroyed. Alternatively, the APIs CoTaskMemAlloc and its friends may be used.
Aggregate Reference Example
1 HRESULT hr;
2 IF2 *pIF2;
3 hr = CoCreateInstance(
4 (CLSID_OuterCOMObj), //object's CLSID
5 NULL,
6 CLSCTX_INPROC_SERVER, //in-process COM server
7 IID_IF2, //ask for interface IF2
8 &pIF2);
9 if (FAILED(hr)) ... ERROR Processing and return
10
11 ... Make Calls into IF2 ...
12
13 pIF2->Release();
Line 1
Declares an HRESULT to use for COM return values.
Line 2
Declares a pointer to interface IF2.
Line 3
The client creates an instance of OuterCOMObj asking for a pointer to IF2. The client thinks IF2 is implemented in OuterCOMObj, that is, the aggregated component is not visible to the client. As part of its start-up sequence, OuterCOMObj creates an instance of InnerCOMObj. A pointer to IF2 is returned to the client in pIF2.
Line 9
Checks to return status of CoCreateInstance.
Line 11
Assumes we make a series of successful calls into interface IF2.
Line 13
Releases interface IF2.
The QueryInterface Method
Every COM+ object is guaranteed to support the IUnknown interface, and a pointer to IUnknown can be obtained from CoCreateInstance. Aside from this rule, however, there are no guarantees. QueryInterface determines what other interfaces an object supports. We like to call this the "discovery phase" of the relationship between the client and the object, since the client calls QueryInterface to discover the capabilities of a particular object. In the following code, the QueryInterface method determines whether an object supports the ISum interface. If the object supports the desired interface, a pointer to that interface is returned in the second parameter of QueryInterface:
hr = pUnknown->QueryInterface(IID_ISum, (void**)&pSum);
if(FAILED(hr))
cout << "The IID_ISum interface is not supported. " << endl;
The first parameter to QueryInterface is the IID of the interface being queried for. The IID_ISum value is declared in the component.h file and defined in the component_i.c file. The MIDL compiler generates both of these files based on the ISum interface definition contained in the component.idl file described previously.
The component_i.c file contains the actual definitions for the GUIDs defined in the IDL file, as shown below:
The AddRef and Release methods perform reference counting, which is used to determine when an object can be freed from memory. For every interface pointer, you must call AddRef before calling any other methods, and you must call Release after you finish using the interface pointer. From the client's point of view, reference counting takes place on a per-interface basis. To make things more efficient, the objects themselves always call AddRef automatically before QueryInterface returns an interface pointer. For this reason, the client can skip the call to AddRef on interface pointers returned by the QueryInterface method
or from the CoCreateInstance function. Once we have a pointer to the desired interface, we no longer need the IUnknown pointer that was originally returned by CoCreateInstance. Accordingly, we can call the IUnknown::Release method to decrement the object's reference counter, as shown below.
The value returned by the Release method is the interface's reference counter.
Note that calling Release on an interface pointer does not necessarily destroy the object providing the implementation. Release simply decrements the object's reference counter. An object is destroyed when its reference count falls to 0. In the preceding code, we released the IUnknown interface pointer, but the object was not destroyed because the ISum interface pointer returned by QueryInterface caused the object's reference counter to be incremented via an implicit call to AddRef.
Since we don't have to worry about calling AddRef on the ISum pointer, we are ready to call the Sum method, as shown in the following code fragment. This, after all, is the goal of the entire sample project.
int sum;
hr = pSum->Sum(2, 3, &sum);
if(SUCCEEDED(hr))
cout << "Sum(2, 3) = " << sum << endl;
Optimizing reference counting In the example above, we don't need to call the AddRef method on thepSum interface pointer because AddRef is automatically called on interface pointers returned by the QueryInterface method.
In the code fragment above, the client correctly uses and releases a pointer to interface IF2. However, if we were to implement reference counting and server unloading as we did before (in COM Fundamentals I), we could have a problem. When the client calls IF2::Release,
the call goes directly into InnerCOMObj, which decrements IF2's reference counter. COM will call DllCanUnloadNow
in the inner COM server (ICOMSrv.dll) to see if the server can be unloaded. DllCanUnloadNow will see that all reference counters have a zero value and return S_OK, and COM will unload the server. Using these implementation techniques for reference counting means that none of this activity is visible to the outer COM object. When the inner COM server (ICOMSrv.dll) is unloaded, the inner COM object (InnerCOMObj) is removed from memory. We no longer have an aggregation of both objects, and OuterCOMObj thinks InnerCOMObj is still active.
This scenario illustrates another problem that must be solved when aggregating objects: How can aggregated COM objects work with the outer COM object to support lifetime management? Aggregation requires that all the COM objects involved must act like one object. All objects participating in the aggregation must support reference counting in such a way that all object lifetimes are synchronized. This means that all inner objects and the outer object are created and destroyed at the same time. We will answer this question and explain how interface navigation is supported in the next few lessons.
Memory Allocation Example
An object may need to pass memory between it and the client at some point in the object's lifetime-this applies to in-process as well as out-of-process servers. When such a situation arises the object must use the task allocator as described previously.
That is, the object must allocate memory whose ownership is transferred from one party to another through an interface function by using the local task allocator. CoGetMalloc provides a convenient way for objects to allocate working memory as well. For example, when the TextRender object
under consideration in this document loads text from a file in the function IPersistFile::Load (that is, CTextRender::Load) it will want to make a memory copy of that text.