Developers Workshop to COM and ATL 3.0

 < Free Open Study > 


Now that you understand stubs, proxies, and your marshaling options we need some information to marshal and a process to marshal from. Recall that marshaling is usually not necessary for in-process servers, as communication happens directly through interface pointers. Our next task is to learn how to create a local (EXE) server. Once we create a local component home for CoHexagon and its class factory we leverage our knowledge of the LoadTypeLibEx() function to automatically register the necessary library and interface information to make use of the universal marshaler (therefore we do not need to build and register our own stub/proxy DLL).

When we create COM servers of the EXE variety, we have some new issues to contend with. Most notably, we do not have a way to export functions from an EXE, such as DllGetClassObject(), DllCanUnloadNow(), DllRegisterServer(), or DllUnregisterServer(). As you know, the functionality provided by these four exports is core to a COM DLL's implementation, as they provide a way to install and remove registry information, create class objects, and unload the server when it is no longer in use.

How, then, can we expose this same functionality from an EXE? Unlike a COM DLL, EXE servers have a single entry point: WinMain(). As we will see, the logic of registration and class object creation is handled based on command line parameters. Furthermore, EXE servers are in charge of unloading themselves from memory (rather than being unloaded by the COM runtime).

Exposing Class Factories from a Local Server

Because SCM needs to locate IClassFactory pointers for a client regardless of the binary packaging, local COM servers advertise their class objects using two COM library functions, rather than implementing DllGetClassObject(). CoRegisterClassObject() is used to register a given class factory with an entity named the class object table. CoRegisterClassObject() takes five parameters:

// Used to register a class factory from a local server. STDAPI CoRegisterClassObject( REFCLSID rclsid, // Class identifier (CLSID) to be registered. IUnknown * pUnk, // Pointer to the class object. DWORD dwClsContext, // Context for running executable code. DWORD flags, // How to connect to the class object. LPDWORD lpdwRegister); // Registration ID.

The first and second parameters are and should be self-explanatory: the CLSID of the coclass created by a given class factory (parameter 1) and the IClassFactory interface of the associated class factory (parameter 2). The third and fourth parameters are used in conjunction to describe the exact behavior of the class factory. For example, do we want to have our class factory create objects exclusively for external clients, or can the server process itself create instances as well? Do we want to have a single server work with multiple clients, or should the class factory launch a new server process for each? These issues are addressed with various combinations of the CLSCTX and REGCLS flags. These combinations can be hairy, so let's look at the most practical combinations.

If the third parameter is set to CLSCTX_INPROC_SERVER, we inform SCM that only the server itself can access the posted class factory. If the third parameter is CLSCTX_LOCAL_SERVER, this allows a class object to be used only by external clients. Thus, in remote or locally accessed EXE coclasses, this flag is a must. But what if you want the best of both worlds? What if you want to allow the server to create objects for its own use, while still allowing clients to access them? Simple. Just OR together each of the CLSCTX flags as the third parameter.

Parameter 4 is a member of the REGCLS enumeration used to specify how the class factory creates the related coclasses. The REGCLS enumeration defines the following activation flags:

// The REGCLS enumeration specifies how a class factory is created for a client. typedef enum tagREGCLS { REGCLS_SINGLEUSE = 0, REGCLS_MULTIPLEUSE = 1, REGCLS_MULTI_SEPARATE = 2, REGCLS_SUSPENDED = 4, REGCLS_SURROGATE = 8 } REGCLS;

We have indirectly already used the REGCLS_SURROGATE flag, as this was the value used when dllhost.exe posted our server's class object. You will never have to use REGCLS_SURROGATE in your own EXE component housing, unless you are creating your own custom surrogate. REGCLS_SINGLEUSE informs SCM to launch a new instance of the server for each client request. REGCLS_MULTIPLE_SEPARATE specifies that any number of clients can share a single server until the class object is revoked from the class table by the EXE using CoRevokeClassObject().

The fifth and final parameter to CoRegisterClassObject() is a COM-provided registration ID (a numerical cookie). Be sure to hold onto this value, as it allows you to tell SCM when a given class object has left the stage and is no longer taking client requests. Here is a common way to register a class factory using CoRegisterClassObject(), allowing both the server and the client to share a single instance of the CoHexClassFactory:

// To post your class factories to the class table, call CoRegisterClassObject() CoHexClassFactory HexFact; DWORD hexID; CoRegisterClassObject(CLSID_CoHexagon, (IClassFactory*)&HexFact, CLSCTX_LOCAL_SERVER | CLSCTX_INPROC_SERVER, REGCLS_MULTIPLE_SEPARATE, &hexID);

Because this above combination of flags is so common, a shorthand notation may be used. The call above to CoRegisterClassObject() can be replaced by the following:

// This is the most standard way to register your class objects from an // EXE server. Both the server and external clients can create the objects // and a single EXE is used for all active clients. CoRegisterClassObject(CLSID_CoHexagon, (IClassFactory*)&HexFact, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &hexID);

Revoking Your Class Objects

When your EXE application is about to shut down, you will need to call CoRevokeClassObject() before WinMain() terminates. This informs SCM that a given class object is no longer available, and removes the factory from the class table. The sole parameter to this COM library function is the registration ID obtained by the final parameter of CoRegisterClassObject():

// Clients can no longer create hexagons. CoRevokeClassObject(hexID);

Registering Multiple Class Factories

Recall what a typical implementation of DllGetClassObject() looks like under the hood. A class factory contained in a server can be created given the correct CLSID, after which we hand off the IClassFactory pointer to a client. From this pointer, clients can create a given coclass instance using CreateInstance(). Let's assume Shapes.dll server contains two creatable objects:

// In-process servers export DllGetClassObject() to allow SCM to grab IClassFactory // pointers on behalf of a COM client. // The implementation may test the REFCLSID parameter to determine which // class factory to create. STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv) { HRESULT hr; CoHexClassFactory *pHexFact = NULL; CoParallelogramClassFactory *pPGramFact = NULL; // Which coclass is the client trying to create? if(rclsid == CLSID_CoHexagon ){ pHexFact = new CoHexClassFactory(); hr = pHexFact -> QueryInterface(riid, ppv); if(FAILED(hr) { delete pHexFact; return hr; } } else if (rclsid == CLSID_CoPGram ){ pPGramFact = new CoParallelogramClassFactory (); hr = pPGramFact -> QueryInterface(riid, ppv); if(FAILED(hr) { delete pPGramFact; return hr; } } else return CLASS_E_CLASSNOTAVAILABLE; }

Local servers register class objects if and only if they are told to do so by SCM. SCM sends a command line argument through the LPSTR parameter of WinMain(). If this string contains the "-Embedding" or "/Embedding" substrings, you are being launched by COM and you need to register your class objects. The following implementation of WinMain() re-creates the logic used by DllGetClassObject() with in-process servers:

// EXE servers do not export a function to register class factories, but rather test the // LPSTR parameter of WinMain(), and call CoRegisterClassObject() // accordingly. int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // Let's see if COM started us. if(strstr(lpCmdLine, "/Embedding") || strstr(lpCmdLine, "-Embedding")) { CoInitialize(NULL); // Here are the class objects. CoHexClassFactory HexFact; CoParallelogramClassFactory PGramFact; // Tokens identifying the registration ID DWORD hexID; DWORD PGramID; // Post the class factories with SCM. CoRegisterClassObject(CLSID_CoHexagon, (IClassFactory*)&HexFact, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &hexID); CoRegisterClassObject(CLSID_CoPGram, (IClassFactory*)&ParallelogramClassFactory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &PGramID); // ^^^^^ OK, now what? ^^^^^ // // Remove class factories from class object table. CoRevokeClassObject(hexID); CoRevokeClassObject(PGramID); CoUninitialize(); return 0; } // Not launched by COM, so do whatever.      return 0; };

Well, this seems to be a step in the right direction. Each class object contained in this EXE is registered with the class object table, after which it is shut down instantaneously. We better have some really fast clients, as they only have nanoseconds to call CreateInstance() from the IClassFactory pointer before the class objects are dead! Of course we need a bit more boilerplate code here to allow a client to make calls to "posted" class objects.

The Ultimate Breakpoint: GetMessage()

Unlike a DLL server, an EXE server is responsible for shutting itself down when detecting there are no active objects and no server locks. Until that point, the EXE server must remain loaded in memory. The easiest way to ensure our server stays in memory is to create a dummy message pump in the implementation of WinMain(), right after registering the class factories:

// Once all class objects are registered just wait... MSG ms; while(GetMessage(&ms, 0, 0, 0)) { TranslateMessage(&ms); DispatchMessage(&ms); }

GetMessage() will now whirl away until a quit message has been posted to the message queue. This ensures that our factories are made available throughout the server's lifetime. Next question: How to terminate the EXE when we have no active objects.

Understanding Object Lifetime in EXE Servers

We have seen that an in-process server returns S_OK from DllCanUnloadNow() whenever the lock count and object count are both at zero. This informs SCM that the DLL may be safely unloaded from memory, as it has no active objects and no client locks. The in-proc class objects adjust the lock count via LockServer() while each coclass and class factory adjust the object count in their constructors and destructors. This deactivation strategy changes just a bit when implementing a local server.

Coclasses contained in EXE servers act the same as an in-process implementation and can simply increment or decrement the global object count accordingly. The class factories, however, must be accessible as long as the dummy message pump is whirling away. As we have created a locally scoped class object in WinMain() we should not "delete this" during the final release. Furthermore, the constructors and destructors of a class object do not adjust the global object count in their constructors and destructors, as they are typically created in the scope of WinMain() and must remain in existence until the final coclass fades away.

In effect, class factories created in local function scope are not really using the internal reference count to monitor self-destruction (this little fact is hidden from our client). Class factories used within an EXE can return fixed values from AddRef() and Release() to pacify any client that happens to examine the returned ULONG. Here is how we can modify CoHexClassFactory's reference counting scheme for use in our EXE server:

// EXE class factories do not destroy themselves when the reference count reaches // zero, but are removed from the class object table when the server is shutting down. STDMETHODIMP_(ULONG) CoHexClassFactory::AddRef() { return 10; // ++m_refCount; } STDMETHODIMP_(ULONG) CoHexClassFactory::Release() { // We are not 'deleting this' here, as // the class factory is scoped to WinMain(). return 20; // --m_refCount; }

The next trick used when implementing a local server is to ensure that when the global object count and global lock count are both at zero, a WM_QUIT message is posted, which will cause our dummy message loop to fail. This in turn revokes the class objects and shuts down the server. To simplify this process, it is helpful to create some global functions that all class factories and custom coclasses may access. Furthermore, it is often easier to have a single server-wide counter to represent the combination of locks and active objects, as opposed to two unique counters:

// Locks.cpp ULONG g_allLocks; // Server locks + active objects. void Lock() // Called by coclass constructors and LockServer(TRUE) { ++g_allLocks; } void UnLock() // Called by coclass destructors and LockServer(FALSE) { --g_allLocks; if(g_allLocks == 0) PostQuitMessage(0); // Kill the message pump! }

Registering Local COM Servers

Once you have assembled your component housing for a COM EXE, you need to ensure that the corresponding REG file is modified to specify a LocalServer32 subkey rather than InprocServer32. Recall that COM-based DLLs use InprocServer32 to point to the physical path to the dll in question (e.g., C:\MyDLL.dll). Local EXE servers must also direct SCM to the correct physical path using LocalServer32 (e.g., C:\MyEXE.exe). For example:

; Be sure your EXEs specify LocalServer32! ; HKEY_CLASSES_ROOT\LocalShapes.CoHexagon\CLSID = {<guid>} HKEY_CLASSES_ROOT\CLSID\{<guid>} = LocalShapes.CoHexagon HKEY_CLASSES_ROOT\CLSID\{<guid>}\LocalServer32 = C:\LocalShapes\Debug\LocalShapes.exe

The next lab will allow you to assemble your own EXE server, which will be accessed from a remote COM client later in this chapter.

Lab 5-1: Developing a Local Server in C++

In this lab, we continue on towards the initial promise of Chapter 1: porting a C program into a remote DCOM server. Here, you will be building a local component house (EXE) to hold your existing CoCar and the retrofitted class factory. This will give you a chance to work with CoRegisterClassObject() and CoRevokeClassObject() as well as programmatic manipulation of type information. This lab will also allow you to configure your custom interfaces to make use of the universal marshaler. Once this lab is completed, you will be able to reuse this EXE as a remote server later in the chapter.

 On the CD   The solution for this lab can be found on your CD-ROM under:Labs\Chapter 05\CoCarEXELabs\Chapter 05\CoCarEXE\VBClient

Step One: Create the Project Workspace

Fire up Visual C++ and create a new Win32 Application workspace, selecting a simple application named CoCarEXE. This will give you a default implementation of WinMain() as well as precompiled headers (stdafx.h and stdafx.cpp). Locate your previous CoCarInProcServer project folder (from Chapter 4) from the Windows Explorer and copy over the CoCar, CoCarClassFactory, and CoCarInProcServer.idl files. Insert the *.cpp and *.idl files into your new project workspace. Now, update each custom interface to make use of the [oleautomation] attribute:

// Because we will be leveraging the universal marshaler, we must mark our interfaces // as variant compliant. [object, uuid(A533DA31-D372-11d2-B8CF-0020781238D4), oleautomation, helpstring("Get info about this car")] interface IStats : IUnknown { HRESULT DisplayStats(); HRESULT GetPetName([out, retval] BSTR* petName); }; [object, uuid(A533DA30-D372-11d2-B8CF-0020781238D4), oleautomation, helpstring("Rev your car & slow it down")] interface IEngine : IUnknown { HRESULT SpeedUp(); HRESULT GetMaxSpeed ([out, retval] int* maxSpeed); HRESULT GetCurSpeed ([out, retval] int* curSpeed); }; [object, uuid(A533DA32-D372-11d2-B8CF-0020781238D4), oleautomation, helpstring("This lets you create a car")] interface ICreateCar : IUnknown { HRESULT SetPetName([in]BSTR petName); HRESULT SetMaxSpeed([in]int maxSp); };

Next, bring over your REG existing file from CoCarInProcServer. We do not need to list any LIBID or Interface entries in the *.reg file, as we will do this step programmatically. However, modify the path for this new server, and change the listing from InprocServer32 to LocalServer32 (recall that EXE servers make use of the LocalServer32 subkey when specifying their path):

REGEDIT HKEY_CLASSES_ROOT\CoCarEXE.CoCar\CLSID = {C8376C06-F1FA-11d2-B8E0-0020781238D4} HKEY_CLASSES_ROOT\CLSID\{C8376C06-F1FA-11d2-B8E0-0020781238D4} = CoCarEXE.CoCar HKEY_CLASSES_ROOT\CLSID\{C8376C06-F1FA-11d2-B8E0-0020781238D4} \LocalServer32 = <your path>\Debug\CoCarEXE.exe

Go ahead and merge this file into the system right now. As a final step, be sure you modify the behavior of your car's class factory such that AddRef() and Release() return simple fixed values and that the implementation of Release() does not "delete this."

// Update the class factory. // STDMETHODIMP_(ULONG) CoCarClassFactory::AddRef() { return 10; // ++m_refCount; } STDMETHODIMP_(ULONG) CoCarClassFactory::Release() { // We are not 'deleting this' here, as // the class object is scoped to WinMain! return 20;      // --m_refCount; }

Step Two: Assemble the EXE Component Housing

When you call CoRegisterClassObject() from within an EXE server, this informs SCM your CoCarClassFactory is ready to take client requests. In your WinMain() loop, check the LPSTR parameter for the "Embedding" substring and register your class factory only if launched by COM:

// include <string.h> to use the strstr() function! // Don't forget to call CoInitialize() from within WinMain(). if(strstr(lpCmdLine, "/Embedding") || strstr(lpCmdLine, "-Embedding")) { // Create the class factory. CoCarClassFactory CarClassFactory; // Registration cookie. DWORD regID = 0; // Post the class factory. CoRegisterClassObject(CLSID_CoCar, (IClassFactory*)&CarClassFactory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &regID); }

Next, create a dummy message pump. The purpose of this loop is to keep your EXE server alive as long as we have active objects and/or server locks. After declaring the message pump, remove your class factory using CoRevokeClassObject(), passing in the DWORD cookie obtained from CoRegisterClassObject():

// This message pump will process messages until you post a WM_QUIT message. MSG ms; while(GetMessage(&ms, 0, 0, 0)) { TranslateMessage(&ms); DispatchMessage(&ms); } CoRevokeClassObject(regID); // Server dying, so remove class object.

As a final administrative duty for the local server, we need to register your server's type library and interface information to make use of the universal marshaler. Configure your server to register these entries automatically by making a call to LoadTypeLibEx(), just before the command line test:

// Automatic interface and type library registration! ITypeLib* pTLib = NULL; LoadTypeLibEx( L"CarServerEXETypeInfo.tlb", // Your file name may vary! REGKIND_REGISTER, &pTLib); pTLib->Release();

Recall that you could choose to check for the –regserver and /regserver command line parameters and register your type information only if told to do so. Here we ensure all necessary entries are always registered each time the application runs. Here is a complete implementation of your EXE's component housing:

// The EXE component housing. int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // Initialize COM. CoInitialize(NULL); // Let's register the type library and interfaces. ITypeLib* pTLib = NULL; LoadTypeLibEx(L"CoCarEXETypeInfo.tlb", REGKIND_REGISTER, &pTLib); pTLib->Release(); // Let's see if we were started by SCM. if(strstr(lpCmdLine, "/Embedding") || strstr(lpCmdLine, "-Embedding")) { // Create the Car class factory. CoCarClassFactory CarClassFactory; // Register the Car class factory. DWORD regID = 0; CoRegisterClassObject(CLSID_CoCar, (IClassFactory*)&CarClassFactory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &regID); // Now just run until a quit message is sent. MSG ms; while(GetMessage(&ms, 0, 0, 0)) { TranslateMessage(&ms); DispatchMessage(&ms); } // All done, so remove class object. CoRevokeClassObject(regID); } // Terminate COM. CoUninitialize(); return 0; }

Step Three: Create and Utilize Global Locking Functions

Local servers need to unload themselves from memory when they detect no active objects and no server locks. Create a brand new header file named locks.h that defines two global function prototypes:

// Locks.h #ifndef _MYLOCKS #define _MYLOCKS void Lock(); // Increment lock. void UnLock(); // Decrement lock. #endif // _MYLOCKS

Now create a corresponding locks.cpp file, which implements these functions and defines a single global variable that holds the sum of all locks and active objects in the server. Be sure that your UnLock() method posts a WM_QUIT message into the message pump when the final release has occurred:

// Locks.cpp #include "stdafx.h" // For precompiled headers. #include "locks.h" ULONG g_allLocks; // locks + objects // Called by cocar constructor and LockServer(TRUE) void Lock() { ++g_allLocks; } // Called by cocar destructor and LockServer(FALSE) void UnLock() { --g_allLocks; // Shut down the dummy message pump. if(g_allLocks == 0) PostQuitMessage(0); }

The constructor of CoCar as well as LockServer(TRUE) each must call the global Lock() method. The destructors of CoCar as well as LockServer(FALSE) must call UnLock():

// Modifications to the coclass. CoCar::CoCar() : m_refCount(0), m_currSpeed(0), m_maxSpeed(0) { m_petName = SysAllocString(L"Default Pet Name"); // Bump up the 'all locks' counter. Lock(); } CoCar::~CoCar() { // Take down the 'all locks' counter. UnLock(); if(m_petName) SysFreeString(m_petName); } // Modifications to the class factory. STDMETHODIMP CoCarClassFactory::LockServer(BOOL fLock) { if(fLock) Lock(); else UnLock(); return S_OK; }

Now, compile and run your server once to enter the type information into the registry. Take the time to hunt down the entries made for you by the LoadTypeLibEx() function under HKCR\Interface and HKCR\TypeLib.

As well, remember that once we have registered our custom interfaces, the OLE/COM Object Viewer is able to display each supported interface by name.

Also recall that we have configured our server to use type library marshaling, and therefore we do not need to build a custom stub/proxy DLL.

This completes the EXE server-side programming. If you are unable to find the correct entries in the registry, be sure you have run your EXE once to hit the LoadTypeLibEx() logic.

Step Four: Build a Client for the Local Server

In the previous chapter we examined how to create Java, Visual Basic, and C++ smart pointer clients. Any of these languages would do in order to test our local server and you may pick your language of choice in this step (the CD solution uses a VB front end). If you create a C++ client, be sure to specify a local class context:

// To access an EXE server, use the CLSCTX_LOCAL_SERVER flag. hr = CoCreateInstance(CLSID_CoCar, NULL, CLSCTX_LOCAL_SERVER, IID_ICreateCar, (void**)&pCCar);

If you pick Java or VB as your target client, just be sure to select the correct *.tlb file from your current project directory, for example:

Figure 5-23: Referencing our local CoCar from VB.

Next, it's time to learn how to remotely access your new local car server à la DCOM.


 < Free Open Study > 

Категории