Now that you have a distributed component in place, you can make use of its exposed COM objects. For simplicity, the term client refers to any piece of code that makes use of a COM object’s interface pointer. This implies that a COM object may be a client to another COM object. In other words, any COM object can be both a client and a server at the same time. It is this ability that allows maximum distributed capabilities.
In this section, we’ll examine the basics for using services of a distributed COM object. You’ll learn how to instantiate a distributed object, use it, and release the acquired resources.
Recall that before a piece of code can use COM, it must first initialize the COM library. A client can call CoInitialize or CoInitializeEx to initialize the library. To be precise, any thread that uses COM must do this. This means that if your application spawns ten threads that use COM, all must call CoInitialize or CoInitializeEx. After you initialize COM, you may create, use, and release COM objects. When you’re all done, you should call CoUninitialize.
To use a distributed COM object, you must first instantiate it. There are several ways to instantiate COM objects in COM, and we will discuss four common ones. Each of these techniques has advantages and disadvantages.
The easiest way to create a COM object is by using the CoCreateInstance COM API function:
HRESULT CoCreateInstance( REFCLSID rclsid, // CLSID of the COM class LPUNKNOWN pUnkOuter, // Pointer to an outer unknown, used for aggregation DWORD dwClsContext, // Preferred server context(s) REFIID riid, // IID LPVOID * ppv // Resulting interface pointer requested );
This API function asks the class factory associated with the specified COM class, identified by the given CLSID (rclsid
, the first parameter), to create a new COM object.
We’ll ignore the second parameter because it is used for aggregation, a topic that is discussed in the Object Orientation section of this chapter.
The third parameter indicates the type of server that you prefer to use. This parameter allows you to indicate virtually how far the target COM class is from the client (the caller). Specifically, you can indicate to COM that the COM object you need lives in an in-process server, in-process handler, out-of-process server, remote server, or any combination of these. These different choices are defined in the CLSCTX
enumeration, shown earlier in the discussion of the CoRegisterClassObject, but redisplayed here for quick reference:
typedef enum tagCLSCTX { CLSCTX_INPROC_SERVER, CLSCTX_INPROC_HANDLER, CLSCTX_LOCAL_SERVER, CLSCTX_REMOTE_SERVER } CLSCTX; #define CLSCTX_SERVER (CLSCTX_INPROC_SERVER| \ CLSCTX_LOCAL_SERVER| \ CLSCTX_REMOTE_SERVER) #define CLSCTX_INPROC (CLSCTX_INPROC_SERVER| \ CLSCTX_INPROC_HANDLER) #define CLSCTX_ALL (CLSCTX_INPROC_SERVER| \ CLSCTX_INPROC_HANDLER|\ CLSCTX_LOCAL_SERVER| \ CLSCTX_REMOTE_SERVER)
The four flags have the following meanings:
- CLSCTX_INPROC_SERVER
The COM class, along with the code that manages the instances of this class, lives within an in-process server. This implies that the COM class and all the code that manages its instances will be loaded within the same process as the client code.
- CLSCTX_INPROC_HANDLER
The COM class, along with the code that manages the instances of this class, lives within an in-process handler. A handler is a special in-process server that virtually implements customized proxies for remote method invocations.
- CLSCTX_LOCAL_SERVER
The COM class lives within an out-of-process server. Furthermore, the out-of-process server is a local server, meaning it lives on the same machine as your client code.
- CLSCTX_REMOTE_SERVER
The COM class lives within a remote out-of-process server, meaning that the server executes on a totally different machine.
If you don’t really care which type of server creates instances of the requested COM class, you would just use the CLSCTX_SERVER
context. This context includes all server contexts except CLSCTX_INPROC_HANDLER
. When you use CLSCTX_SERVER
, you tell COM to choose the best (nearest) server to create an instance of the requested COM class. COM will first look for an in-process server that supports such COM class. If there’s none, COM will look for a local out-of-process server. If this fails, it will look for a remote out-of-process server that can serve up the requested instance. COM refers to the registry to get this information. This means that all COM configuration information for each server type must be correctly configured first. You may also specify CLSCTX_ALL
to encompass all server contexts, including CLSCTX_INPROC_HANDLER
.
The fourth parameter is an interface identifier (IID) of the interface that you are requesting. The fifth and last parameter is an output parameter that returns a pointer to the requested interface specified by the IID. Here’s how you can create and instance of a COM class:
IOcr *pOcr = NULL ; HRESULT hr = CoCreateInstance(CLSID_OcrEngine, NULL, CLSCTX_SERVER, IID_IOcr, reinterpret_cast<void**>(&pOcr));
The code asks the class factory associated with CLSID_OcrEngine
to create a COM object that is an instance of the COM class whose GUID is CLSID_OcrEngine
. When this happens, it returns a pointer to an IOcr interface of the instantiated COM object.
An advantage of this API function is simplicity. It won’t allow you to specify a target remote server. In order to create an instance on a remote machine using this API function, you must configure the RemoteServerName
named value in the registry (on the client machine) under the AppID
key that refers to the target server. This can be regarded as an advantage or a disadvantage. From the advantage viewpoint, this allows pre-DCOM servers to support location transparency, without changing a single line of code. In addition, this allows an administrator to configure the target server. The registry entry required for this configuration is:
[HKEY_CLASSES_ROOT\AppID\{EF20ACA0-C12A-11d1-ABF6-00207810D5FE}] @="OcrEngine" "RemoteServerName"="dog.dcom.com"
The easiest way to add this entry is to either use dcomcnfg.exe or oleview.exe. You can also manually add this entry into the registry yourself.
Configuring a remote server is an inappropriate choice if you want dynamic selection of target remote servers. In other words, there are situations in which you would want to allow a user to pick a target server at runtime. In these cases, static configuration of a remote server name is a disadvantage. There’s also another disadvantage with this API function, which deals with performance. You can request only one interface pointer per call (network round-trip).
In a distributed environment, the CoCreateInstance API function would be slow to the groaning point if you wanted to query for 100 interfaces on a remote server. Making a 100 network round-trips is more expensive than making a single one. CoCreateInstanceEx is an API function that’s optimized for this sort of thing. As an extension to CoCreateInstance, CoCreateInstanceEx solves the two problems of a configured-only server destination and poor performance.
HRESULT CoCreateInstanceEx( REFCLSID rclsid, // CLSID of the COM class IUnknown *pUnkOuter, // Pointer to an outer unknown, used for // aggregation DWORD dwClsCtx, // Preferred server context(s) COSERVERINFO *pServerInfo, // Target machine ULONG cmq, // Size of MULTI_QI array MULTI_QI *pResults // MULTI_QI array containing resulting QI // requests );
The first three parameters of this API function correspond exactly to the ones in CoCreateInstance. The fourth parameter, COSERVERINFO
, allows you to indicate information regarding a target server. You may specify the name of the target host via the pwszName
member. This can be a netbios name (e.g., “dog”), a DNS name (e.g., “dog.dcom.com”), or an actual IP address (“192.0.0.1”).[40]
typedef struct _COSERVERINFO { DWORD dwReserved1; // Must be 0 LPWSTR pwszName; // Server name COAUTHINFO *pAuthInfo; // Security information, discussed in Chapter 5 DWORD dwReserved2; // Must be 0 } COSERVERINFO;
With the COSERVERINFO
structure, you can dynamically select a server to create a COM object. This allows greater flexibility than the static RemoteServerName
named value registry configuration. For example, this allows you to dynamically ask the user for a target server name.
To avoid network traffic, the CoCreateInstanceEx also supports the request for multiple interface pointers in one network round-trip. Each interface request can be specified using a MULTI_QI
structure. The sixth parameter of CoCreateInstanceEx can be an array of these structures. The size of this array is indicated by the fifth parameter of this function.
typedef struct _MULTI_QI { const IID* pIID; IUnknown * pItf; HRESULT hr; } MULTI_QI;
You may create an array of these structures, each of which includes a requested IID, the resulting interface pointer, and the resulting status code. Simply set the first member to the requested IID, the second member to NULL
, and the third member to S_OK
. Given what you know so far, here’s how you can create a COM object on a selected server and retrieve back an array of interface pointers of interest:
// Request for IOcr and ISpell MULTI_QI mqi[] ={ {&IID_IOcr, NULL, S_OK}, {&IID_ISpell, NULL, S_OK} }; // Target hostname COSERVERINFO csi = { 0, TEXT("dog.dcom.com"), NULL, 0 } ; // Create a COM object and get back two interfaces HRESULT hr = CoCreateInstanceEx(CLSID_OcrEngine, NULL, CLSCTX_SERVER, &csi, sizeof(mqi)/sizeof(mqi[0]), mqi);
CoCreateInstance and CoCreateInstanceEx both use standard factories to create COM objects. Recall that standard factories implement the standard IClassFactory interface. This implies that you can create COM objects using these API functions only if the target class factory implements IClassFactory.
Briefly put on your object implementor’s hat. If you want the general public to instantiate your objects, the factories you create must implement the standard IClassFactory interface. However, if you want to prevent generic instantiation, perhaps because you need customized or private object creation that’s not supported by the standard IClassFactory interface, you would implement your own custom factory interface to support object creation. This implies that clients may not use CoCreateInstance or CoCreateInstanceEx to create your objects.
To create an object whose factory is a custom factory, you use the CoGetClassObject API function. This API function allows you to obtain an interface pointer to a custom interface for object instantiation. The drawback of this method is you must know the IID of the object creation interface. However, this drawback can be regarded as an advantage, since it offers more programming flexibility.
HRESULT CoGetClassObject( REFCLSID rclsid, // CLSID of the COM class DWORD dwClsContext, // Preferred server context(s) COSERVERINFO * pServerInfo, // Target machine REFIID riid, // Custom factory interface identifier LPVOID * ppv // Resulting pointer to custom factory // interface );
You are already familiar with the first three parameters of this function. However, the last two parameters deserve some discussions. The fourth parameter allows you to specify the custom factory interface that supports custom creation of COM objects. You must know the custom creation interface before you can call CoGetClassObject. The requested interface pointer is returned in the last parameter. Note that the last parameter returns a pointer to the custom factory interface that can create objects.
Here’s an example. Assume there’s a factory associated with the CLSID CLSID_MyCustomFactory
. In addition, assume that this factory implements the IMyCustomFactory interface, as opposed to the standard IClassFactory interface. In order to create a COM object that this custom factory manufactures, you must first acquire the IMyCustomFactory interface by using its identifier, IID_IMyCustomFactory
. You use all this information to make the call to CoGetClassObject, as follows:
// Get a custom interface pointer for object instantiation IMyCustomFactory *pCustomFactory = NULL; HRESULT hr = CoGetClassObject(CLSID_MyCustomFactory, CLSCTX_SERVER, NULL, IID_IMyCustomFactory, reinterpret_cast<void**> (&pCustomFactory));
Notice that you pass NULL
in the third parameter, neglecting to programmatically specify a server destination. This tells COM to look into the registry for target host information in the RemoteServerName
named value of the correct AppID
key (which is referred to by CLSID_MyCustomFactory
).
Upon successfully calling CoGetClassObject, you get back a pointer to the IMyCustomFactory interface, which supports object creation. You then use this interface to actually create the COM objects that you need. Assume that this custom interface supports the MakeMeAnObject method that does the actual object creation. Further, once this method creates a COM object, it returns an IMyInterfaceOnTheCreatedObject pointer. Given these assumptions, you can create a COM object and get back a pointer to it, as shown here:
if (SUCCEEDED(hr)) { // Now create a COM object using // the custom interface for object creation. IMyInterfaceOnTheCreatedObject *pMyInterfaceOnTheCreatedObject = NULL; hr = pCustomFactory->MakeMeAnObject(&pMyInterfaceOnTheCreatedObject); }
The pMyInterfaceOnTheCreatedObject
pointer points to an interface supported by the COM object that is instantiated by the custom factory. Once you have this interface pointer, you may use the services exposed by the instantiated object. For example, assuming that the IMyInterfaceOnTheCreatedObject interface supports a ShowMeSomeMagic method, you can do the following:
pMyInterfaceOnTheCreatedObject->ShowMeSomeMagic();
Other than the ability to create objects using a custom factory, CoGetClassObject possesses another key advantage over CoCreateInstance[Ex] when you create multiple objects of a specific type or COM class. This advantage deals with performance, because each call to CoGetClassObject or CoCreateInstance[Ex] requires the COM infrastructure to locate the component implementation. Thus, given the following code, you create two COM objects of a specific type, CLSID_OcrEngine
, but pay only a one-time component implementation lookup, since you call the CoGetClassObject API function only once.
// Overhead: locate the component implementation.
IClassFactory *pFactory = NULL ;
CoGetClassObject(CLSID_OcrEngine, CLSCTX_SERVER, NULL,
IID_IOcr, reinterpret_cast<void**>(&pFactory));
// Create the first object.
IOcr *pOne = NULL; pFactory->MakeMeAnObject(&pOne);
// Create the second object.
IOcr *pTwo = NULL; pFactory->MakeMeAnObject(&pTwo);
However, if you use CoCreateInstance[Ex] to create these two COM objects, you will pay for this lookup overhead each time you call CoCreateInstance[Ex], as shown here:
// Overhead: locate the component implementation. // Create the first object. IOcr *pOne=NULL ; HRESULT hr = CoCreateInstance(CLSID_OcrEngine, NULL, CLSCTX_SERVER, IID_IOcr, reinterpret_cast<void**>(&pOne)); // Overhead: locate the component implementation. // Create the second object. IOcr *pTwo=NULL ; HRESULT hr = CoCreateInstance(CLSID_OcrEngine, NULL, CLSCTX_SERVER, IID_IOcr, reinterpret_cast<void**>(&pTwo));
We can’t leave this topic without noting that CoCreateInstance and CoCreateInstanceEx are really just convenient functions, which internally call CoGetClassObject to request the IClassFactory interface, and then invoke the IClassFactory::CreateInstance method to create the actual COM object. Consider the following code, which uses the convenient CoCreateInstance function:
IOcr *pOcr=NULL ; HRESULT hr = CoCreateInstance(CLSID_OcrEngine, NULL, CLSCTX_SERVER, IID_IOcr, reinterpret_cast<void**>(&pOcr));
The earlier code is essentially equivalent to the following:
IClassFactory *pFactory = NULL ; // Get a custom interface pointer for object instantiation HRESULT hr = CoGetClassObject(CLSID_OcrEngine, CLSCTX_SERVER, NULL, IID_IClassFactory, reinterpret_cast<void**>(&pFactory)); if (SUCCEEDED(hr)) { IOcr *pOcr = NULL; // CreateInstance internally creates the object // and returns the requested interface pointer. hr = pFactory->CreateInstance(NULL, IID_IOcr, reinterpret_cast<void**>(&pOcr)); pFactory->Release(); }
The advantage of using CoGetClassObject is flexibility. If a factory exposes a private or custom interface for object creation, you must use CoGetClassObject. The more convenient CoCreateInstance and CoCreateInstanceEx API functions won’t do, because they both internally request for the IClassFactory interface when calling CoGetClassObject, as shown in the earlier code.
Another way to create an object is by using a class moniker, a naming object that refers to another COM object. In this sense, a moniker can hide information regarding an object to which it refers. If a client uses a moniker, it is liberated from having to know which object it needs to deal with, because the object that’s needed can be embedded inside a moniker. In order to be a moniker, an object must implement the standard IMoniker interface.
There are different types of monikers, but the one that deals with object instantiation is the class moniker. A class moniker portrays a class factory. If you have a class moniker, you can use it to bind to the factory that it represents. Once you’ve bound to the factory, you can request for an IClassFactory or a custom object creation interface. Through this interface, you can request the factory to manufacture objects for you.
There are several ways to do this, but this discussion will focus on one flexible technique. Before you can bind to a moniker, you must first create a bind context and obtain the IBindCtx pointer. You can easily do this by calling the CreateBindCtx API function.
HRESULT CreateBindCtx( DWORD reserved, // Reserved LPBC * ppbc // Return IBindCtx interface pointer );
Once you have obtained a bind context, you can use it to call a special COM API function (MkParseDisplayName) to convert a textual name into a moniker, which refers to an object represented by the textual name. This API function takes a bind context and a textual name that represents the object to be bound to. It returns the number of characters successfully parsed and the resulting IMoniker pointer on the created moniker object.[41]
HRESULT MkParseDisplayName( LPBC pbc, // Bind context LPCOLESTR szUserName, // Textual name ULONG FAR *pchEaten, // Characters successfully parsed LPMONIKER FAR *ppmk // Resulting IMoniker pointer );
After you call MkParseDisplayName, you can then bind to the target object. To do this, you simply invoke the BindToObject method of the IMoniker interface, which is obtained from calling MkParseDisplayName. This BindToObject interface method has the following signature:
HRESULT IMoniker::BindToObject( IBindCtx *pbc, // Bind context IMoniker *pmkToLeft, // In composites, points to preceding moniker REFIID riidResult, // IID void **ppvResult // Resulting interface pointer of type IID );
Monikers are very powerful and can refer to practically anything. If an object implements the IMoniker interface, it can be used as a moniker. In the context of object creation, here’s a demonstration of the use of a class moniker:
// Dynamically get the newest OCR Factory
wchar_t *pwszFactory = GetNewestOcrFactory();
The given code obtains a textual representation of a CLSID from some arbitrary function, called GetNewestOcrFactory. This function simply returns a human-readable representation of a CLSID, which corresponds to the class moniker syntax specified by COM. If you take a larger viewpoint, this function can be seen as something magical that miraculously gives out a textual representation of the newest OCR factory.
wchar_t *GetNewestOcrFactory() { static wchar_t *pwszChangeableFactory = TEXT("CLSID:DF22A6B2-A58A-11d1-ABCC-00207810D5FE:"); return pwszChangeableFactory; }
This function, or magical thing, shields a client from having to know a specific CLSID of the latest and greatest OCR factory, which is especially beneficial, because this CLSID, representing the latest OCR factory, can vary over time. For example, the current OCR engine supports optical character recognition for only the English alphabet, but two years later, a totally different OCR engine may support optical character recognition for the Chinese alphabet. Therefore, a moniker provides an additional level of indirection that frees the client from a particular implementation.
A client would simply call GetNewestOcrFactory to get the textual representation of the newest OCR factory. It then would create a bind context object using the CreateBindCtx API function. Once it has a bind context pointer, it simply converts the textual representation into a moniker object, using the MkParseDisplayName API function. With the received IMoniker pointer, it may call the BindToObject method to officially bind to the target object to which the moniker refers.
HRESULT hr = S_OK; // Create a bind context. IBindCtx *pBind=NULL; hr = CreateBindCtx(0, &pBind); // Create a class moniker based on the textual name. // A class moniker represents a factory. ULONG ulEaten = 0; IMoniker *pMon=NULL; hr = MkParseDisplayName(pBind, pwszFactory, &ulEaten, &pMon); // Bind to the factory and get the IID_IClassFactory interface. IClassFactory *pClassFactory=NULL; hr = pMon->BindToObject(pBind, 0, IID_IClassFactory, (void**)&pClassFactory); // Tell the factory to manufacture an object // and get back a pointer to its IOcr interface. IOcr *pOcr=NULL; hr = pClassFactory->CreateInstance(0, IID_IOcr, (void**)&pOcr); // We can now call IOcr's methods.
As seen in the earlier code fragment, you obtain an IClassFactory interface during the moniker binding process. Once you get this pointer, you can call its CreateInstance method to create the object you want. You may think that this is a complex and long process to create objects, but this technique shields a client from having to know a specific CLSID, which can be very beneficial.[42]
Creating an object is one thing, but initializing it is another. So far, you’ve only heard about creating uninitialized objects. If you want to support object initialization after objects are created, you must either implement your own code to support initialization or use one of the provided standard interfaces.
One simple way to support object initialization is by providing your own interface method, such as InitializeThySelf, which a client can call to initiate object initialization. When a client calls this interface method, you would then initialize your object however you choose. The problem with this approach is that you’re expecting your clients to know about this specific InitializeThySelf interface method. This is an extremely bad expectation in a distributed and component-based environment, because InitializeThySelf is just an ad hoc method that’s hacked up to support initialization.
To avoid such arbitrary implementations, COM provides a number of standard interfaces that support object initialization, and one of these interfaces is IPersistFile. A COM object that implements this interface supports object persistence using a persistent file—in other words, an object can save its states to a file for later retrieval. Saving an object’s states to persistent storage is important, because this allows you to bring the same object back to life years from now.
We’ll examine an API function that both creates and initializes a COM object from a persistent file: CoGetInstanceFromFile. After manufacturing an object, it calls IPersistFile::Load to initialize the object from a given file (see objidl.h for the definition of IPersistFile). The COM object must implement the IPersistFile interface, before you can successfully utilize this technique.
HRESULT CoGetInstanceFromFile( COSERVERINFO *pServerInfo, // Target host CLSID *rclsid, // CLSID pointer IUnknown *pUnkOuter, // Outer unknown used for aggregation DWORD dwClsCtx, // Server context DWORD grfMode, // Open mode OLECHAR *pwszName, // Persistent file name DWORD dwCount, // Size of MULTI_QI array MULTI_QI *pResults // Array of requested interfaces );
You’ve seen practically all of these parameters. The second parameter takes a pointer to a CLSID. If you don’t pass a CLSID pointer into this call, the API function will derive the CLSID from the persistent file name, passed in via pwszName
. CoGetInstanceFromFile internally determines the CLSID by calling a COM API function named GetClassFile.
The fifth parameter indicates the persistent file’s open mode, which includes values such as STGM_READ
, STGM_WRITE
, and so forth. As their names imply, STGM_READ
is used when reading from the opened file and STGM_WRITE
is used when writing to the file.
Here’s how we would use this function to create an object and initialize it from a file. The following code passes in the name of the persistent file, c:\test.doc, which you want a COM object hosted by Microsoft Word to load. Since you don’t pass in the CLSID, this API function will automatically figure out the correct CLSID based on the name of the persistent file by calling GetClassFile. You also query the IDataObject interface, which is a standard interface that supports uniform data transfer.
wchar_t *pwszPersist = TEXT("c:\\test.doc"); MULTI_QI mqi[] = { {&IID_IDataObject, NULL, S_OK} }; HRESULT hr = CoGetInstanceFromFile(NULL, NULL, NULL, CLSCTX_SERVER, STGM_READ, pwszPersist, sizeof(mqi)/sizeof(mqi[0]), mqi); IDataObject *pData=NULL; if (SUCCEEDED(hr)) { pData = static_cast<IDataObject*>(mqi[0].pItf); // use the IDataObject interface . . . . . . }
Instead of using CoGetInstanceFromFile, you can also use the following shorthand version, which does the same thing:
hr = CoGetObject(pwszPersist, 0, IID_IDataObject, (void**) &pData);
Upon successfully calling CoGetInstanceFromFile, you may use the IDataObject interface pointer for operations, such as to display all the text within this Microsoft Word document.[43]
After object creation, you may use the services exposed by the COM object. Recall that you use an object by calling methods supported by exposed interfaces. In COM programming, a client should never have access to a COM object in any way other than through interface pointers.
Typically, you receive a requested interface pointer to the instantiated object after calling one of the object creation API functions. Use the received interface pointer by invoking its supported methods. If you want to use a different service exposed by the object (i.e., if you want to use a different interface), use the QueryInterface method to query for another interface. Recall that every COM object implements QueryInterface because every interface derives from IUnknown.
When you use a COM object, you also have another important responsibility: you should always check the returned status code. Recall that even if the object returns a success status code, the RPC layer can potentially override this success status, so it’s extremely important to check all status codes. Consider the following simple code:
// Request for the IOcr (mqi[0]) and IPersist (mqi[1]) interfaces MULTI_QI mqi[] = { {&IID_IOcr, NULL, S_OK}, {&IID_IPersistFile, NULL, S_OK} }; COSERVERINFO csi = {0, TEXT("dog.dcom.com"), NULL, 0} ; HRESULT hr = CoCreateInstanceEx(CLSID_OcrEngine, NULL, CLSCTX_SERVER, &csi, sizeof(mqi)/sizeof(mqi[0]), mqi); if (SUCCEEDED(hr)) { // Did the call succeed? if (SUCCEEDED(mqi[0].hr)) { // Verify status code of IOcr. IOcr *pOcr = static_cast<IOcr *>(mqi[0].pItf); // Use the IOcr interface. ISpell *pSpell=NULL; // Query for ISpell. hr = pOcr->QueryInterface(IID_ISpell, reinterpret_cast<void**> (&pSpell)); if (SUCCEEDED(hr)) { // Make sure success before using pSpell. // Use the spell interface. . . . } . . . } if (SUCCEEDED(mqi[1].hr)) { // Verify the status code of IPersistFile. IPersistFile *pPersist = static_cast< IPersistFile *>(mqi[1].pItf); . . . } }
Upon object instantiation, you query for both the IOcr and the IPersistFile interfaces. After calling CoCreateInstanceEx to create the object, you must check the returned status code of this call. Even when this call succeeds, it’s possible that not all requested interfaces were returned. Therefore, before you can use one of the two requested interfaces, you must specifically check for its status code. For example, check for IOcr’s status code before using it.
Use pOcr
in the previous code to query for an ISpell interface. Recall that the CoOcrEngine object implements both of these interfaces. Again, you check for the status code of this query before you actually use the ISpell interface pointer. If you have an interface pointer, you can get to other interface pointers within the COM object through QueryInterface.
You also request for an IPersistFile interface in the earlier code snippet (mqi[1]
). Recall that if an object supports the IPersistFile interface, you may load its states from a persistent file. So here’s another way to initialize an object after creation. Notice that we check for IPersistFile’s status code before using it. This check will fail, since we haven’t implemented IPersistFile for the CoOcrEngine COM object.
You might have noticed that QueryInterface is inefficient if you want to query for 10 interfaces at one time, but there is a way to do this without compromising network efficiency. COM provides the IMultiQI interface, which is implemented by the local proxy object. To use this interface, you first query for it and then use it to query for a number of interfaces in a single network round-trip. Be aware that the IMultiQI interface exists only after you have successfully created a remote object, and therefore, its local proxy. Indeed, this happens only if the client and object live in different apartments (execution contexts). In the following snippet, you’ll assume that you have a valid IUnknown interface and that we’re using a remote object. Here’s how we would use the IMultiQI interface:
IMultiQI *pMulti=NULL; // Query for the IMultiQI interface, which is implemented by the local proxy. hr = pUnk->QueryInterface(IID_IMultiQI, reinterpret_cast<void**>(&pMulti)); assert(SUCCEEDED(hr)); MULTI_QI mqi[] = { {&IID_IOcr, NULL, S_OK}, {&IID_ISpell, NULL, S_OK} }; hr = pMulti->QueryMultipleInterfaces(sizeof(mqi)/sizeof(mqi[0]), mqi); assert(SUCCEEDED(mqi[0].hr)&&SUCCEEDED(mqi[1].hr)); . . .
You can query for the IMultiQI interface only if a local proxy for a remote object exists. This simple interface has a single method, QueryMultipleInterfaces, which queries for an array of interfaces in one single network round-trip.
To recap, using a COM object is straightforward. Just remember these rules:
In general, always check returned status codes.
Use QueryInterface to discover other services provided by the object.
If using a remote object, use IMultiQI for efficiency.
Other things you need to understand include memory management, which will be discussed in Chapter 5, and object lifetime management, which will be discussed next.
A client is obligated to partly assist objects in their lifetime management. The COM lifetime rules mandate that a client must release a requested interface after it is done using it. You need to obey the lifetime rules in order to allow your object’s existence to be correctly managed. This means that when you are done using an interface pointer, you must invoke Release upon the interface pointer to notify the target object of your intentions. If you fail to do this correctly, a distributed object will either live forever or die too soon, leading to possible resource leaks or access violations. In short, you need to obey the COM lifetime rules, discussed in the Objects section of Chapter 3.
You have seen the following code before without the calls to Release. Notice that after you’ve finished using an interface, it’s released. Notice also that you use and release an interface only when the status code is a success.
if (SUCCEEDED(mqi[0].hr)) { IOcr *pOcr = static_cast<IOcr *>(mqi[0].pItf); // Use the IOcr interface . . . ISpell *pSpell=NULL; hr = pOcr->QueryInterface(IID_ISpell, reinterpret_cast<void**>(&pSpell)); if (SUCCEEDED(hr)) { // Use the ISpell interface . . . pSpell->Release(); } // Use the IOcr interface some more . . . pOcr->Release(); } if (SUCCEEDED(mqi[1].hr)) { IPersistFile *pPersist = static_cast<IPersistFile *>(mqi[1].pItf); // Use the persist interface . . . pPersist->Release(); }
In addition, it is important to invoke Release upon the correct interface pointer. For example, after you use the ISpell interface, you release the ISpell interface (not some other interfaces, such as IOcr or IPersist). Since you have the internal knowledge that the CoOcrEngine object keeps an object-level reference count (because you developed it), you could have invoked Release three times upon the ISpell interface pointer. That is, you could have used pSpell
everywhere you see the Release method invocation in the earlier code. This would work for the CoOcrEngine object, because you know the internals of its implementation. However, this can be deadly because it may not work for other objects. Never assume that an object keeps an object-level reference count, but act as if all interfaces keep personal interface-level reference counts. This is a COM mandate—and you should discipline yourself to follow the rules of COM, in this case for uniformity. We shouldn’t assume the internals of any COM object. Invoke Release only upon the interface pointer returned to you, nothing else.
Following is more code you’ve seen. Notice that you release the IMultiQI interface after you’ve finished using it, and you cast each of the interfaces in the MULTI_QI
array to the correct interface pointer type before calling Release. As we’ve just noted, this is extremely important because you should not call Release upon any other interfaces exposed by the object, except the interface that’s given to you.
IMultiQI *pMulti=NULL; // Query for the IMultiQI interface, which is implemented by the local proxy hr = pUnk->QueryInterface(IID_IMultiQI, (void**)&pMulti); assert(SUCCEEDED(hr)); MULTI_QI mqi[] = { {&IID_IOcr, NULL, S_OK}, {&IID_ISpell, NULL, S_OK} }; hr = pMulti->QueryMultipleInterfaces(sizeof(mqi)/sizeof(mqi[0]), mqi); pMulti->Release(); assert(SUCCEEDED(mqi[0].hr)&&SUCCEEDED(mqi[1].hr)); // Release the correct interface. static_cast<IOcr *>(mqi[0].pItf)->Release(); // Release the correct interface. static_cast<ISpell *>(mqi[1].pItf)->Release();
There’s one last issue regarding developing COM clients. Clients should call the CoFreeUnusedLibrary API function periodically or during idle processing[44] to unload in-process servers that are no longer in use. Recall that all in-process servers implement the DllCanUnloadNow entry point to support the component’s unloading. The CoFreeUnusedLibrary API function calls DllCanUnloadNow on each in-process server, and unloads the ones that are no longer needed. An in-process server can be unloaded when its component-level reference count falls to zero.
Don’t do this for out-of-process servers, since these servers are totally different processes. They commit suicide when their component-level reference count falls to zero.
Tip
At this point, you have enough knowledge to build server and client components using COM. Refer to the Creating a Server Component sidebar for a checklist of creating server components, and refer to the Creating a Client Component sidebar for a checklist of creating client components.
[40] At the time of this writing, there is a bug regarding the use of a DNS name or an IP address that refers to the same machine as your client. This problem is fixed in Windows NT 4.0 Service Pack 4, Windows 2000, and DCOM 95 1.2.
[41] Instead of calling CreateBindCtx and MkParseDisplayName, you can simply call CreateClassMoniker. If you take this alternative, you can use CLSIDFromString to convert the textual representation of a CLSID into a real CLSID, which is input for the first parameter of CreateClassMoniker.
[42] This book does not cover standard interfaces in any great detail, as it only deals with distributed computing. Monikers are objects that implement the IMoniker interface, which deserves your attention if you plan to use naming objects in your distributed architectures. Refer to the SDK documentation for further details. From this short description, you should find a solid grounding from which to peruse other moniker-related topics, because the binding process is almost exactly the same. On a totally different note, you may want to look up other ways to create (and possibly initialize) a component object, including CoGetClassObjectFromURL and CoGetInstanceFromIStorage.
[43] Refer to the provided sample program, called Persist
, to see how this is done, as this is not relevant to the current discussion.
[44] In Windows programming, an empty message queue means that the application has no work to do (therefore, idle). The application is not busy handling window messages, so you can do whatever you want during this short period. This short period is intended for idle processing, and it should be kept short or else arriving window messages will not be handled in a timely manner.
Get Learning DCOM now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.