[Previous] [Contents] [Next]

The Client

Now that we have defined the ISum interface in IDL, let's write the code to create a client application that uses this interface, assuming for the time being that we already have an implementation of the ISum interface. For starters, we'll create the client in C++, since this language forces us to understand and to work closely with the underlying COM+ mechanisms. In Chapter 3, we'll branch out into Visual Basic and Java, where the same principles apply but the details are often hidden from view.

The CoInitializeEx Function

All COM+ applications begin with a call to CoInitializeEx, which initializes the COM+ library. (CoInitialize is an obsolete function that calls CoInitializeEx with the COINIT_APARTMENTTHREADED flag.)You must call this function before you can use any other COM+ services except certain COM+ memory allocation calls.4 You could use OleInitialize, which calls CoInitializeEx internally, but this is done only when an application specifically intends to use the compound document features of OLE.

The first parameter of CoInitializeEx must be NULL.5 The second parameter indicates the desired threading model, as shown in the following code fragment. (For more information about threading models, see Chapter 4.)

hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if(FAILED(hr))
    cout << "CoInitializeEx failed." << endl;

Notice that all COM+ function names are prefixed with Co, indicating that they are part of the COM+ API.

The CoCreateInstance Function

After initializing the COM+ library using CoInitializeEx, you must give the client a way to instantiate the desired coclass containing an implementation of ISum. The CoCreateInstance call is the standard way to do this, as shown here. You can think of it as the COM+ equivalent to C++'s new operator:

// {10000002-0000-0000-0000-000000000001}
const CLSID CLSID_InsideCOM = {0x10000002, 0x0000, 0x0000, 
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}};

hr = CoCreateInstance(CLSID_InsideCOM, NULL, 
    CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pUnknown);
if(FAILED(hr))
    cout << "CoCreateInstance failed." << endl;

The first parameter of CoCreateInstance is a class identifier (CLSID). A CLSID is a GUID that is associated with the coclass we want to instantiate. CoCreateInstance uses this CLSID to search for a match in the HKEY_ CLASSES_ROOT\CLSID section of the registry6 in order to locate the desired component. HKEY_CLASSES_ROOT\CLSID is undoubtedly the most fundamental COM+-related key in the registry. If an entry for the CLSID is found, the various subkeys provide COM+ with information about the component; CoCreateInstance fails if no match is found.

As you might know, a component can contain multiple coclasses. A component that supports multiple coclasses must register a unique CLSID for each supported coclass. The client must call CoCreateInstance once for each COM+ object it wants to create. If some of the objects coexist in the same component, that's all right with COM+. In the registry, each coclass is listed by its CLSID, and the entry contains information specifying the component containing that coclass. Figure 2-1 shows two calls to CoCreateInstance for two different coclasses that turn out to be housed in the same component. Using the CLSID, CoCreateInstance does a lookup in the registry; in this case, both Object1 and Object2 originate from the same component.

Click to view at full size.

Figure 2-1. Two coclasses that inhabit the same component.

The second parameter of CoCreateInstance specifies whether this object will be part of an aggregate object. (Aggregation is covered later in this chapter.) Because this example does not use aggregation, we'll pass NULL for this parameter.

The third parameter specifies the context in which the component will run. COM+ provides support for calling in-process, local, and remote components. Sometimes a particular component is implemented in several flavors. (For example, it might be available in an in-process version and a local and remote version.) In such cases, you can select the version that best meets your needs. For each class context, you must use a different registry entry to specify that component type. The following table shows the available class contexts and the subkeys of HKEY_CLASSES_ROOT\CLSID\{YourCLSID} that identify the different component types available for a particular coclass. If you aren't particular about the component type, specify CLSCTX_SERVER to retrieve the first available in-process, local, or remote component, in that order. In our example, we'll access an in-process component, so we'll specify the CLSCTX_INPROC_SERVER class context.

Class Context Flags Subkey Description
CLSCTX_INPROC_SERVER InprocServer32 Full path to a 32-bit in-process component.
CLSCTX_INPROC_HANDLER InprocHandler32 Full path to a 32-bit DLL component handler.*
CLSCTX_LOCAL_SERVER LocalServer32 Full path to a 32-bit executable component.
CLSCTX_REMOTE_SERVER AppID A GUID that references additional information for remote components. It is stored in the HKEY_CLASSES_ROOT\AppID\{YourAppID} section of the registry. (For more information, see the section titled "The AppID RegistryKey" in Chapter 12.)
CLSCTX_INPROC Defined as a combination of the CLSCTX_INPROC_SERVER and CLSCTX_INPROC_HANDLER flags.
CLSCTX_SERVER Defined as a combination of the CLSCTX_INPROC_SERVER, CLSCTX_LOCAL_SERVER, and CLSCTX_REMOTE_SERVER flags.
CLSCTX_ALL Defined as a combination of the CLSCTX_INPROC_SERVER, CLSCTX_INPROC_HANDLER, CLSCTX_LOCAL_SERVER, and CLSCTX_REMOTE_SERVER flags.

* The CLSCTX_INPROC_HANDLER class context is used by lightweight client-side handlers, a special type of COM+ in-process component that runs in the client process and implements certain client-side parts of the class. Instances of the actual class are accessed remotely. For more information, see Chapter 15.

When you instantiate a component using CoCreateInstance, you can use the fourth parameter to specify the interface identifier (IID) of the desired interface. (The IID is a 128-bit value composed in the same way as a GUID.) A pointer to this interface is returned in the fifth parameter. In this case, we are looking for a pointer to the ISum interface, so we could pass the IID_ISum value to CoCreateInstance. However, convention dictates that an application should normally get a pointer to the IUnknown interface first and then use the QueryInterface call to locate the other interfaces. Never ones to disregard convention, we'll pass IID_IUnknown.

The Methods of IUnknown

If all goes well, a pointer to the IUnknown interface of the requested COM+ object is returned in the fifth parameter of CoCreateInstance. The name IUnknown indicates that at this stage the true capabilities of the object are unknown. With this pointer, we can call any of the three IUnknown methods: QueryInterface, AddRef, or Release.

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:

// {10000001-0000-0000-0000-000000000001}
const IID IID_ISum = {0x10000001, 0x0000, 0x0000, 
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}};

The AddRef and Release Methods

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.

m_cRef = pUnknown->Release();
cout << "pUnknown->Release() reference count = " << 
    m_cRef << endl;

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 the pSum interface pointer because AddRef is automatically called on interface pointers returned by the QueryInterface method. Nevertheless, you must explicitly call the AddRef method whenever an interface pointer is aliased, as shown here:

IUnknown* pUnknown;
ISum* pSumOne;
ISum* pSumTwo;

// QueryInterface calls AddRef on pSumOne.
pUnknown->QueryInterface(IID_ISum, (void**)&pSumOne);

// Release IUnknown; we don't need it anymore.
pUnknown->Release();

// Copy a pointer.
pSumTwo = pSumOne;

// This requires an explicit AddRef.
pSumTwo->AddRef();

pSumOne->Sum(5, 3);
pSumTwo->Sum(4, 7);

// Now we've finished; call Release.
pSumTwo->Release();
pSumOne->Release();

Here the interface pointer pSumOne was copied to pSumTwo. This aliasing results in two interface pointers; therefore, we must call AddRef to increment the reference counter. In some cases, if you are certain that the reference count will remain above 0, thus ensuring that the object is not freed, you can optimize away the extra AddRef and Release calls, as shown below.

// QueryInterface calls AddRef on pSumOne.
pUnknown->QueryInterface(IID_ISum, (void**)&pSumOne);

// Release IUnknown; we don't need it anymore.
pUnknown->Release();

// Copy a pointer.
pSumTwo = pSumOne;

// Don't need this. Reference count is sure to be at least 1.
// pSumTwo->AddRef();

pSumOne->Sum(5, 3);
pSumTwo->Sum(4, 7);

// Matching AddRef commented out.
// pSumTwo->Release()

// Now we've finished; call Release.
pSumOne->Release();

In more complex code, it is often difficult to correctly remove the unnecessary AddRef and Release calls. Such code also becomes very fragile if someone modifies it without being aware of the reference counting assumptions made by the original developer. And in most cases, the performance improvement that comes from optimizing AddRef and Release calls is not significant. As we'll see in Chapter 19, the proxy caches AddRef and Release calls when you access remote objects so that each client call does not necessarily result in a remote call. All in all, AddRef and Release optimizations have been oversold.

The CoUninitialize Function

With the main job of the component complete, we'll execute some cleanup code. We need to call IUnknown::Release because we are finished with the ISum pointer. Note that this is the last open pointer to the object; therefore, the reference counter will be decremented to 0, initiating the object's destruction. Then we'll call CoUninitialize to close the COM+ library, freeing any resources that it maintains and forcing all RPC connections to close, as shown here:

CoUninitialize();

Although our sample makes only one call to the CoInitializeEx function, more sophisticated applications might call it multiple times to support different threading requirements. (See Chapter 4 for more details about COM+ threading models.) In such cases, calls to the CoInitializeEx and CoUninitialize functions must be balanced—if there are multiple calls to CoInitializeEx, there must be the same number of calls to CoUninitialize. Only the CoUninitialize call corresponding to the CoInitializeEx call that initialized COM+ can close it.

The V-Table Situation

Figure 2-2 shows the mechanism through which a client program holding an interface pointer can call methods in a component.

Click to view at full size.

Figure 2-2. An interface pointer is really a pointer to a pointer to a v-table containing function pointers.

You can see that for every C++ class containing virtual functions, the compiler automatically creates a v-table structure containing pointers to all of the class's virtual functions, including those declared in a base class. Note that a vtable is built on a per-class basis, not for each instance of the class. A pointer to the v-table itself is stored as the first member in the object's memory structure and is followed by the other members of the class. What we refer to as an interface pointer is actually a pointer to a pointer to a table of function pointers. Needless to say, this is a rather inconvenient way to describe the mechanism through which a client calls an object. C++ helps us forget about the extra level of indirection by implicitly converting pUnknown->pVtbl->AddRef() to pUnknown>AddRef().

The indirection provided by a v-table can be compared with the software interrupts used by MS-DOS. An MS-DOS application requests service from the operating system by setting parameter values in various registers and then issuing software interrupt 21h. MS-DOS installs a pointer to an interrupt handler at vector 21h in the interrupt table and through it responds to the application's request. Although v-tables are based on direct function calls rather than on software interrupts, they provide a similar, well-known entry point for accessing an arbitrary service.

It is also important to realize that the function pointers in the v-table are stored in the order of declaration—that is, the first method declared in the IDL definition corresponds to the first function pointer in the v-table for that class. The v-table order of virtual functions that are declared in a base class and implemented in a derived class is determined by the order of the functions in the base class. Therefore, you cannot mess up a standard interface such as IUnknown simply by declaring its methods in the wrong order: the order of the IUnknown methods in your class's v-table is determined by their order in the system header file (unknwn.h), where IUnknown is declared.

When multiple inheritance is used to implement several interfaces in one class, the compiler concatenates the v-tables of the various interfaces. The order in which their entries appear in the class's memory layout is determined by the order in which the interfaces are inherited.

Problems with QueryInterface

In the system IDL file, unknwn.idl, the IUnknown::QueryInterface method is declared as follows:

HRESULT QueryInterface([in] REFIID riid, 
    [out, iid_is(riid)] void** ppvObject);

The iid_is IDL attribute expresses the relationship between the first parameter and the second: ppvObject returns an interface pointer whose type is specified by riid. (For a complete discussion of the iid_is IDL attribute, see Chapter 16.) However, by the time the C++ compiler gets its hands on this interface in the unknwn.h system header file, the MIDL compiler has transformed the QueryInterface method to the following:

virtual HRESULT __stdcall QueryInterface(REFIID riid, 
    void** ppvObject)=0;

This declaration is the source of much consternation and many bugs in COM+ clients and components. Figure 2-3 shows how the QueryInterface mechanism works when used correctly. Notice that COM+ objects are drawn with standard and custom interface "lollipops" extending from the left side of the object, while IUnknown is drawn separately on top of the object because all objects support this interface.

Click to view at full size.

Figure 2-3. Querying an object for its ISum interface pointer.

Because QueryInterface must be able to query for any interface (IUnknown, ISum, IYouNameIt), the second parameter is declared as a pointer to a pointer to pretty much anything (void**). This very flexible declaration allows a programmer to do pretty much anything, including making pretty much any kind of mistake. Examine the following code to see whether you can find the insidious bug that the C++ compiler cannot:

hr = pUnknown->QueryInterface(IID_ISum, (void**)pSum);

The problem here is that the address-of (&) operator is missing before pSum. The compiler, blinded by the explicit void** cast, assumes that we know what we are doing and stuffs the retrieved pointer into any invalid location pointed to by pSum. The result is an immediate crash.

Another problem with QueryInterface relates to assigning a type of interface pointer to a pointer that was declared to point to a different type of interface. See if you can figure out what's wrong with this code:

ISum* pSum;
hr = pUnknown->QueryInterface(IID_IUnknown, (void**)&pSum);
int sum;
pSum->Sum(2, 3, &sum); // Uh-oh!

The QueryInterface call requests the IUnknown interface (IID_IUnknown), but it stores the pointer in pSum. The pSum function is a pointer to an interface of type ISum, not IUnknown, but the compiler thinks this is all right because pSum is explicitly cast to void**. Because pSum is a pointer to an interface of type ISum, the compiler doesn't have any problem calling the Sum function. Of course, in this case pSum is only a pointer to IUnknown and might spontaneously combust when the Sum function is called.7

To make QueryInterface safer, Microsoft has overloaded the definition IUnknown::QueryInterface in the unknwn.h system header file. The new and improved version of QueryInterface that accepts only one argument is shown below:

template <class Q>
HRESULT STDMETHODCALLTYPE QueryInterface(Q** pp)
{
    return QueryInterface(__uuidof(Q), (void**)pp);
}

This alternative definition of the QueryInterface method is implemented as a function template and relies on the __uuidof operator, a Microsoft extension to C++ that aids in COM+ programming. (For more information about function templates, see the section titled "C++ Templates (A Quick Introduction)" in Chapter 3.) You can use the __uuidof operator to retrieve the GUID of an interface pointer as long as the GUID was supplied using another Microsoft extension, __declspec(__uuid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")) . Recall that the header files generated by the MIDL compiler use this extension, so this is the case for all interfaces defined in IDL. This technique makes calling QueryInterface even easier because you don't need to specify an IID—the correct one is obtained for you. Here is a sample that uses this modern form of QueryInterface:

ISum* pSum;
hr = pUnknown->QueryInterface(&pSum); // IID_ISum, (void**)&pSum

Building the Client Project

To build the client project, use the InProcess.dsw workspace file in the Samples\The IUnknown Interface folder on the companion CD. When you build the projects in this workspace, Visual C++ automatically compiles the IDL file using the MIDL compiler and then builds the client application. The complete client.cpp code is shown in Listing 2-2.

client.cpp

#define _WIN32_DCOM
#include <iostream.h>
#include "component.h" // Generated by MIDL

// {10000002-0000-0000-0000-000000000001}
const CLSID CLSID_InsideCOM = {0x10000002, 0x0000, 0x0000, 
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}};

void main()
{
    IUnknown* pUnknown;
    ISum* pSum;

    HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
    if(FAILED(hr))
        cout << "CoInitializeEx failed. " << endl;

    hr = CoCreateInstance(CLSID_InsideCOM, NULL, 
        CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pUnknown);
    if(FAILED(hr))
        cout << "CoCreateInstance failed. " << endl;

    hr = pUnknown->QueryInterface(IID_ISum, (void**)&pSum);
    if(FAILED(hr))
        cout << "IID_ISum not supported. " << endl;

    pUnknown->Release();

    int sum;
    hr = pSum->Sum(2, 3, &sum);

    if(SUCCEEDED(hr))
        cout << "Client: Calling Sum(2, 3) = " << sum << endl;
    pSum->Release();

    CoUninitialize();
}

Listing 2-2. The complete client code.