Fortunately, changing an in-process component into an executable component, or vice-versa, is not all that difficult because it primarily involves changing the housing for your coclasses. Nevertheless, if after careful consideration of the alternatives you decide to proceed with an executable component, keep in mind that executable components differ from in-process components in several ways. One of the major differences is that EXEs cannot export functions as DLLs can, which means that the well-known entry points DllGetClassObject, DllCanUnloadNow, DllRegisterServer, and DllUnregisterServer are specific to in-process components only. An executable component must have a main or WinMain start-up function where execution begins and must include special code to deal with termination. The remaining concerns with executable components relate to race conditions that can arise in multithreaded EXEs; these are explained later in this chapter.
Originally, DLLs worked simply by exporting functions. Other programs could load a DLL and ask for a pointer to a particular function either by name or by number. In-process components use the IUnknown::QueryInterface method to return interface pointers through which calls can be made, while COM+ relies on standard exported functions such as DllGetClassObject to retrieve the initial interface pointer.
Executable files, unlike DLLs, do not export functions. An executable file can hand out function pointers only while it is running. COM+ addresses this difference between DLLs and EXEs by using command-line parameters that are communicated to the EXE on start-up. Thus, the four functions exported by in-process components are replaced by three command-line arguments that can be supplied to executable components. The following table lists the exported functions and their equivalent command-line arguments:
Function Exported from an In-Process Component | Equivalent Command-Line Argument Passed to an Executable Component |
---|---|
DllGetClassObject | –Embedding |
DllCanUnloadNow | No equivalent; see the section titled "Managing the Lifetime of an Executable Component" later in this chapter |
DllRegisterServer | –RegServer |
DllUnregisterServer | –UnregServer |
Note that when an executable is launched in the normal way (in response to the user clicking an icon), no special command-line arguments are passed to the application, so the application can easily determine whether it has been launched in response to a client request or by the user. Typical executable components, such as Microsoft Internet Explorer or Microsoft Word, do not show any user interface when they are launched via COM+, so a client process can request services from these applications without the desktop becoming unnecessarily cluttered. Of course, most executable components also provide methods that enable the client to request that they show themselves to the user.
When you develop components that will run remotely, displaying a user interface is usually not a good idea. Due to the COM+ security architecture, most components executing remotely run on a desktop separate from that displayed on the server.2 Therefore, ignoring the fact that the user might be a great distance from the server, the user interface might not even be visible on the server. The following start-up code is typical of that normally executed by executable components in their main function:
int main(int argc, char** argv) { if(argc > 1) { char* szToken = strtok(argv[1], "-/"); if(_stricmp(szToken, "RegServer") == 0) return RegisterComponent(...); if(_stricmp(szToken, "UnregServer") == 0) return UnregisterComponent(...); if(_stricmp(szToken, "Embedding") == 0) { // Launched by COM+! // Don't show a user interface. // Then register a class factory. } } // No COM+ arguments; run in the normal fashion. // Show user interface here. return 0; } |
This code determines whether more than one command-line argument is present, since the first command-line argument (argv[0]) is always the name of the application itself. The strtok function finds the first dash (—) or slash (/) character in the first command-line argument; either character is a legal prefix to the command-line arguments provided by the system. Next, the –stricmp function compares lowercase versions of the command-line argument with the three standard strings that can be provided by COM+. If –RegServer or –UnregServer is found, the appropriate registration steps are performed and execution is terminated. That's right—if an executable component is executed using either of the registration commands, the program exits immediately after performing the requested action. This design allows setup programs to launch an executable component and instruct the component to register itself without any danger that it will display a user interface or take some other action.
Note that you must call CoInitializeEx prior to the registration or deregistration routine if type libraries are to be registered using LoadTypeLibEx or RegisterTypeLib or unregistered using UnRegisterTypeLib. Although the samples on the companion CD always refresh their registry settings when they are launched, this is for convenience only and is not generally recommended for production code. Depending on the security environment, a component might not have write access to critical areas of the registry.
The actual registration of an executable component is similar to that of an in-process object. The main difference is that instead of using the InprocServer32 subkey, executable components are registered in a LocalServer32 subkey. The same coclass can be registered in both the InprocServer32 and the LocalServer32 subkeys. In such cases, COM+ provides the client with whatever form of the object was requested via the class context values (CLSCTX_LOCAL_SERVER or CLSCTX_INPROC_SERVER). If the client passes the value CLSCTX_SERVER or CLSCTX_ALL, COM+ defaults to the most efficient implementation of the coclass: an in-process component. If an in-process implementation is not available—in other words, if the InprocServer32 key is not found—COM+ looks for local and then remote components.
The RegisterServer3 function automatically checks its first parameter to determine whether an EXE or a DLL component is being registered. For example, if the RegisterServer function is called using the name of an executable file, the component is registered in a LocalServer32 subkey. In-process (DLL) components are registered in the InprocServer32 subkey. The following code fragment is from the RegisterServer function:
// Add the component's filename subkey under the CLSID key. // Is it an EXE? if(strstr(szModuleName, ".exe") == NULL) { // No; use InprocServer32. setKeyAndValue(szKey, "InprocServer32", szModule); // More code here... } else // Yes; use LocalServer32. setKeyAndValue(szKey, "LocalServer32", szModule); |
If the –Embedding flag is located in the command-line arguments, the component knows that it is being launched in response to a client's activation request. The name of this flag originates from the linking and embedding days of OLE. The component should respond to the –Embedding flag by registering its class object using the CoRegisterClassObject function and taking other steps similar to those performed by a DLL when the DllGetClassObject function is called. If no COM+ command-line argument is detected, the executable component should assume that it has been launched at the request of the user and thus behave like a normal application. For executable components designed to be activated only via COM+, the absence of the –Embedding flag should also be interpreted as the user's attempt to launch the application and an appropriate message should be displayed, after which the application should terminate.
Once the processing of command-line parameters is out of the way, executable components must call CoInitializeEx. Recall that in-process components do not normally call CoInitializeEx because they run in the process of their caller, which has already called CoInitializeEx. Besides initializing COM+, CoInitializeEx gives the component an opportunity to specify its supported threading model. In-process components are forced to do this via the ThreadingModel value in the registry. An executable component can use the COINIT_MULTITHREADED flag of CoInitializeEx to declare itself as multithreaded, as shown in the following code:
CoInitializeEx(NULL, COINIT_MULTITHREADED); |
In place of the DllGetClassObject function, an executable component uses the CoRegisterClassObject function to inform COM+ about the objects it supports. Each call to CoRegisterClassObject registers one class identifier (CLSID) and its associated class object. If a component supports multiple coclasses, it must call CoRegisterClassObject once for each object. CoRegisterClassObject takes the CLSID of the coclass being registered as its first parameter and a pointer to the coclass's class object as its second parameter. Thus, you must first create an instance of the class object using the C++ new operator, as shown here:
IClassFactory *pClassFactory = new CFactory(); DWORD dwRegister; CoRegisterClassObject(CLSID_InsideCOM, pClassFactory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &dwRegister); |
Each class object can specify certain settings via the third and fourth parameters of the CoRegisterClassObject function. The third parameter determines whether the class object is visible only to client code running in the process space of the component (CLSCTX_INPROC_SERVER) or to clients running in a separate address space on the local machine or a remote machine (CLSCTX_LOCAL_SERVER). The fourth parameter accepts flags from the REGCLS enumeration, as shown here:
typedef enum tagREGCLS { REGCLS_SINGLEUSE = 0, REGCLS_MULTIPLEUSE = 1, REGCLS_MULTI_SEPARATE = 2, REGCLS_SUSPENDED = 4, REGCLS_SURROGATE = 8 } REGCLS; |
The REGCLS_SINGLEUSE flag is a legacy setting that enables only one client to access the class object. Subsequent client requests cause COM+ to load a new instance of the entire component. The REGCLS_MULTIPLEUSE flag is more commonly used because it enables multiple client applications to connect to a single instance of a class object. In addition, class objects registered with the REGCLS_MULTIPLEUSE flag are automatically accessible to code running in the process space of the component itself, regardless of whether the CLSCTX_INPROC_SERVER flag was specified in the third parameter of CoRegisterClassObject.
The REGCLS_MULTI_SEPARATE flag is a variation on the flag REGCLS_MULTIPLEUSE, which restricts client code running in the process of the component from accessing the class object. This flag enables a component to register different class objects for in-process clients (CLSCTX_INPROC_SERVER) and out-of-process clients (CLSCTX_LOCAL_SERVER). COM+ specifically allows multiple registrations of the same class object; each registration is independent and returns a unique key via the fifth parameter of the CoRegisterClassObject function. A class object registered with the CLSCTX_LOCAL_SERVER class context and REGCLS_MULTI_SEPARATE registration flag would exhibit rather odd behavior. Out-of-process clients would share one instance of the component, but client code running in the process of the component would cause COM+ to launch another instance of the component!
The following table describes the behavior exhibited by various combinations of the class context and registration flags. For information about the REGCLS_SURROGATE flag, see Chapter 12. We'll discuss the flag REGCLS_SUSPENDED later in this chapter.
Registration Flags | |||
---|---|---|---|
Class Context Flags | REGCLS_SINGLEUSE | REGCLS_MULTIPLEUSE | REGCLS_MULTI_SEPARATE |
CLSCTX_INPROC_SERVER | Illegal | In-process | In-process |
CLSCTX_LOCAL_SERVER | Local | In-process or local | Local |
CLSCTX_INPROC_SERVER/CLSCTX_LOCAL_SERVER | Illegal | In-process or local | In-process or local |
While you can control many basic COM+ settings directly using the registry editor or indirectly by using the Distributed COM Configuration utility, components written today generally do not rely on these methods. Modern applications want precise run-time control over which server they're connected to and over what security settings are involved. The CoCreateInstanceEx function creates a single object on a specified machine, whether local or remote. The ability to create an object on a remote machine is an extension of the functionality available in the CoCreateInstance function, which creates an object on the local machine only.
In addition, rather than requesting a single interface and obtaining a single pointer to that interface, you can use CoCreateInstanceEx to request multiple interface pointers in a single call. Although QueryInterface calls to in-process components are fast, calling QueryInterface multiple times to request many different interface pointers from a component that is located on another machine is much less efficient. The CoCreateInstanceEx function offers an optimization that enables a developer to obtain multiple interface pointers from an object with a single remote call.
CoCreateInstanceEx lets you specify an array of MULTI_QI structures, each containing a pointer to an interface identifier (IID). The MULTI_QI structure optimizes QueryInterface so that fewer round-trips are made between machines. When CoCreateInstanceEx returns, each MULTI_QI structure contains (if available) a pointer to the requested interface and the return value of the QueryInterface call for that interface. A return value of E_NOINTERFACE means that no interface pointers were available; the distinguished error CO_S_NOTALLINTERFACES means that the object did not implement all of the requested interfaces. The MULTI_QI structure used by CoCreateInstanceEx is shown in the code below.
typedef struct tagMULTI_QI { const IID* pIID; IUnknown* pItf; HRESULT hr; } MULTI_QI; |
CoCreateInstanceEx supports this functionality because of the IMultiQI interface, shown in Interface Definition Language (IDL) notation in the following code. Like the CoCreateInstanceEx function, the IMultiQI::QueryMultipleInterfaces method accepts an array of MULTI_QI structures and in a single remote call can obtain multiple interface pointers from an object. In fact, the internal implementation of CoCreateInstanceEx actually calls the QueryMultipleInterfaces method to provide its functionality.
interface IMultiQI : IUnknown { HRESULT QueryMultipleInterfaces( [in] ULONG cMQIs, [in, out] MULTI_QI* pMQIs); } |
You do not have to implement the IMultiQI interface; together, the proxy and the stub conspire to implement this interface for all objects that use standard marshaling or type library marshaling. You can simply call the IUnknown::QueryInterface method to request the IMultiQI interface and then use the resultant interface pointer to call the IMultiQI::QueryMultipleInterfaces method, as shown in the following code fragment. Note that calling QueryInterface to request the IMultiQI interface pointer does not execute a round-trip to the server because the in-process proxy implements this interface.
MULTI_QI qi[2]; qi[0].pIID = &IID_IHello; qi[0].pItf = 0; qi[0].hr = S_OK; qi[1].pIID = &IID_IGoodbye; qi[1].pItf = 0; qi[1].hr = S_OK; IMultiQI* pMultiQI = 0; pUnknown->QueryInterface(IID_IMultiQI, (void**)&pMultiQI); pMultiQI->QueryMultipleInterfaces(2, &qi); |
The CoCreateInstanceEx function also accepts an argument of the type COSERVERINFO, as shown in the following code. COSERVERINFO identifies the remote machine on which the object should be instantiated as well as the security provider to be used. The most important field of this structure is the pwszName variable, which refers to the name of the remote machine. The name used to identify a machine depends on the naming scheme of the underlying network transport. By default, all Universal Naming Convention (UNC) paths, such as \\server or server, and Domain Name Service (DNS) names, such as server.com, www.server.com, and 144.19.56.38, are allowed.
typedef struct _COSERVERINFO { DWORD dwReserved1; LPWSTR pwszName; COAUTHINFO* pAuthInfo; DWORD dwReserved2; } COSERVERINFO; |
Note that CoCreateInstanceEx does not search the client's registry for the desired CLSID but instead contacts the Service Control Manager (SCM) residing on the specified remote machine and requests that it search the registry on the server. This technique is advantageous because it means that the CLSID of the remote component does not need to be registered on the client's machine. It also obviates the need for the RemoteServerName entry in the AppID section of the registry. Of course, using CoCreateInstanceEx doesn't relieve you from having to provide marshaling code for any custom interfaces consumed by the client.
The following code shows the use of the CoCreateInstanceEx function; you can use it to replace calls to CoCreateInstance in a client-side application. Notice the use of the CLSCTX_REMOTE_SERVER flag.
COSERVERINFO ServerInfo = { 0, L"Remote_Computer_Name", 0, 0 }; MULTI_QI qi = { &IID_IUnknown, NULL, 0 }; CoCreateInstanceEx(CLSID_InsideCOM, NULL, CLSCTX_REMOTE_SERVER, &ServerInfo, 1, &qi); pUnknown = qi.pItf; |
Since we're building an executable component, marshaling is a concern. Of the three basic marshaling options—standard, type library, and custom—we strongly recommend standard marshaling or type library marshaling for most components. The easiest way to use standard marshaling is simply to build and register a proxy/stub DLL from the files generated by the MIDL compiler following the steps described in Chapter 12. To use type library marshaling, you simply make sure that the interface is marked with the oleautomation attribute in the IDL file and that the type library is properly registered. No further work is required if you're using type library marshaling because the type library marshaler is available as part of the system. If, however, you use standard marshaling with a proxy/stub DLL, the DLL must be copied to every machine and properly registered using the RegSvr32 utility.
It is sometimes desirable to avoid distributing and registering a proxy/stub marshaling DLL on every computer that runs either the client application or the component itself. The main reason for wanting to avoid this drudgery is simply to minimize the amount of baggage that must be distributed and installed with an application. Of course, the simplest way to obviate the need for a proxy/stub DLL is to use the type library marshaling facility built into the system. If this technique proves unacceptable due to performance or other reasons, it is possible to integrate the proxy/stub code generated by MIDL directly into the client and component executables instead of building a marshaling DLL. You do this by statically linking the proxy/stub code generated by MIDL into the client and component executables.
Be forewarned that integrating proxy/stub code directly with an application is normally frowned upon. For one thing, it means that other client programs cannot access the services of the component because they won't have access to the marshaling code. Also, it becomes crucial that the proxy/stub code statically linked into the client and component executables be synchronized. If the component's stub code is updated, the client's proxy code must be updated as well.
If after careful consideration of the alternatives you decide to integrate the proxy/stub code directly with the application's executable file, follow the steps below. After following these steps, you can run and test the application. Everything should work as it did before the changes except that the proxy/stub DLL will no longer be needed.
int main(int argc, char** argv) { CoInitializeEx(NULL, COINIT_MULTITHREADED); IUnknown* pUnknown; DWORD dwUnused; DllGetClassObject(IID_ICustom, IID_IUnknown, (void**)&pUnknown); CoRegisterClassObject(IID_ICustom, pUnknown, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, &dwUnused); CoRegisterPSClsid(IID_ICustom, IID_ICustom); |
Normally, COM+ locates the proxy/stub marshaling code for a custom interface by searching the HKEY_CLASSES_ROOT\Interfaces section of the registry. As a subkey of the desired interface, the ProxyStubClsid32 key includes the CLSID of the in-process component containing the marshaling code. COM+ uses the CoGetPSClsid function to help retrieve this information from the registry. In some cases, however, an application might not want to store this information in the registry. A component might have been downloaded across a network and due to security settings might not have permission to access the local registry. Or, as in the situation discussed here, the application might already contain the necessary proxy/stub code.
In such cases, you can call the CoRegisterPSClsid function used in the preceding code fragment to register the proxy/stub marshaling code for custom interfaces within the context of a running process. The CoGetPSClsid function used by COM+ to locate the marshaling code returns the CLSID of the proxy/stub code registered using the CoRegisterPSClsid function. Thus, you can use CoRegisterPSClsid to perform temporary run-time registration that remains in effect only as long as the process is running, in place of a more permanent registration that would actually be written to the system registry.
Note that in the preceding code, the IID IID_ICustom is passed as a CLSID parameter to the DllGetClassObject, CoRegisterClassObject, and CoRegisterPSClsid functions. By convention, the IID of a custom interface is used as the CLSID of that interface's proxy/stub marshaling code. Since both IIDs and CLSIDs are 128-bit GUIDs, this seemingly odd conversion presents no difficulty to the compiler.