O'Reilly logo

Learning DCOM by Thuan L. Thai

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Components

In this chapter:

In the last chapter, you learned how to package a COM object from a number of interfaces. You also learned how to build a class factory that can dynamically create your COM objects in a distributed environment. You wrote the code, but you couldn’t test the COM object just yet, because you haven’t put the code for the COM object and its class factory inside a binary module, such as a DLL or an EXE, that can be executed.

In this chapter, you will learn how to build these binary modules, which can both use and expose COM objects. We call these binary modules COM components. As shown in Figure 4-1, a client component can be any COM application that uses a COM object’s interfaces. Server components, on the other hand, are the ones that host COM objects and their associated class factories.

Client and server components

Figure 4-1. Client and server components

Tip

The C++ code that you see in this chapter can be automated by Visual C++ and the ATL wizards with a few mouse clicks. However, knowing what’s needed to create a component from scratch will empower you. There’s no magic in the wizard-generated code, shown in Chapter 6 and onwards.

By the time you finish this chapter, you will know how to package COM objects into a COM application suitable for a distributed environment. You will also know how to write client applications that make use of remote COM objects. Furthermore, you’ll know how to dynamically reuse COM objects that are already developed and deployed.

Here’s a list of what’s covered in this chapter.

  1. Servers—Class factories and objects cannot execute alone. They need either a house (EXE) or a shed (DLL) to host them. Like an EXE, a house stands alone, whereas, a shed—like a DLL—is useful only next to a structure. Recall that components are DLLs, OCXs, and EXEs. You’ll learn the necessary code for developing a component in this section. Specifically, we’ll examine the required code for component initialization, termination, dynamic activation, lifetime, and registration.

  2. Clients—Clients are components that use a different component’s objects. You’ll learn the necessary code for developing a COM compatible client in this section. These clients must abide by the initialization, termination, object creation, interface usage, and lifetime management rules of COM.

  3. Object Orientation—Having learned the basics of COM, we can judiciously discuss the object orientation aspects of COM. In this section, you’ll also learn COM’s component reuse mechanisms: containment and aggregation.

This chapter is a sequel to the previous one. The previous chapter and this chapter capture all the basic elements for building distributed components. Like the last chapter, you should read this chapter from beginning to end for understanding. Then, review the relevant sections in this chapter, as well as the last chapter, when you build your own distributed components.

Servers

In the last chapter, you developed a class factory that could create COM objects upon request. However, the class factory and the objects themselves could not execute alone. They need to be packaged together into a component, such as an EXE or a DLL. With the hospitality of a component, class factories can register themselves to the SCM so that clients anywhere in the universe can access them, barring security constraints of course. A typical component, more often referred to as a server, may package together multiple factories that can create different object types. Recall that a class factory can create instances of a particular COM class, which has an associated class identifier (CLSID). Clients use the CLSID to find the correct factory that can create the associated COM object.

In this section, you’ll learn how to share your factories and their associated objects with the world. You’ll assemble the code for your factories and their associated objects into a COM compatible server.[37] First, you’ll learn the preliminary terms related to server types, including out-of-process (outofproc) and in-process (inproc) servers, because server requirements differ slightly for each server type. Then you’ll learn the necessary server initialization and termination requirements. Next, we’ll look at managing a server’s lifetime, as this is important in terms of system resources and types of execution contexts. Finally, you’ll learn how to add support for dynamic activation, in addition to registering your classes against the implementation repository (the Windows Registry).

Server Types

There are two general categories of servers in the world of COM: out-of-process and in-process servers. They differ in many aspects, including initialization, dynamic activation, implementation registration, and component lifetime management. First there will be a brief discussion of the types. Second, we will focus on the specific differences.

Out-of-process servers

Before out-of-process servers are discussed, two important terms need to be defined. This book uses the term execution context to refer to an encapsulated execution scope, which can be a single thread, a group of threads, a process, or the whole machine. COM formally calls an execution context an apartment.

Out-of-process servers are standalone executables. As such, they run in a different execution context from the client executable. These servers can reside on a client’s local machine or on a remote machine. Servers with the EXE extension are out-of-process servers. See Figure 4-2. To be specific, there are two different types of out-of-process servers. Executables that reside and execute on a client’s machine are called local servers, whereas ones that reside and execute on a remote machine are called remote servers.

An out-of-process server

Figure 4-2. An out-of-process server

When a client invokes a method in an out-of-process server, the method invocation will cross apartment, process, and possibly machine boundaries, which means that marshaling is involved. In standard marshaling, COM sets up a proxy on the client side and a stub on the object side. The proxy lives locally in the client’s address space. The client talks directly to this proxy, the proxy talks to the stub, and the stub talks to the target object. The out-of-process server is a separate process that runs in a totally different execution context from the client. All method invocations are dispatched using ORPC across different execution contexts. For this reason, out-of-process servers offer poorer performance than in-process ones, since interprocess communication is involved, on top of security, network bandwidth, protocol activation, and so forth. The impact is far more brutal for remote servers than for local servers.[38] However, out-of-process servers provide scalability and fault tolerance, because they can be isolated or distributed on many different physical hosts. If you’re interested in seeing representative data regarding in-process and out-of-process performance, see Appendix B.

In-process servers

As shown in Figure 4-3, in-process servers need a hosting executable in order to be active. The hosting executable is the client that uses the in-process server. In other words, in-process servers run in the same process as a client executable. They are dynamic link libraries with extensions such as DLL and OCX. Because they execute within your client’s process, you experience the best possible performance. All method invocations are directly made using vptrs and vtbls. This is an efficient way to invoke an interface method, as it is equivalent to a C++ virtual function call. Be forewarned that this is true only when both the client and the server component share the same execution context, as discussed in Chapter 5.

An in-process server

Figure 4-3. An in-process server

If you need to use in-process servers remotely, you need a surrogate EXE and its required registry settings on the server side. This surrogate hosts the in-process server. On the client side, you need the required registry settings to point to the remote host.

There are also other variants of in-process servers. These are called in-process handlers, which are DLLs that represent custom marshalers. These DLLs contain code to perform custom marshaling for an object’s supported interfaces.

Initialization and Termination

As you’ve just seen, a component is either an EXE or a DLL. Before you can publish your components, you must follow a number of rules, one of which deals with initialization and termination. COM initialization and termination are required at the thread level. That is, every thread in a process that uses COM must initialize the COM library. Before a COM thread vanishes, it must uninitialize the COM library to correctly release COM related resources.

In 32-bit Windows, every process starts off with one thread—the process is the first thread, which is commonly referred to as the main thread. Therefore, the lifetime of the first thread is the lifetime of the process. For simplicity, you’ll be thread illiterate in this chapter, since Chapter 5 exploits the threading models of COM.

Out-of-process servers

Every process must initialize COM before it can call any other COM API functions or do COM related work. There’s only one exception: you may call COM memory allocation routines (CoGetMalloc, CoTaskMemAlloc, CoTaskMemFree, etc.) before initializing COM. Other than this exception, every thread that uses COM must initialize COM by calling one of the following COM API functions:

// To use CoInitializeEx, you must define _WIN32_DCOM
// pvReserved - Reserved and must be NULL
// dwCoInit   - Indicates the concurrency model of the current execution
//                 context
//            - Either COINIT_APARTMENTTHREADED or COINIT_MULTITHREADED
HRESULT CoInitializeEx(LPVOID pvReserved, DWORD dwCoInit);

HRESULT CoInitialize(LPVOID pvReserved);

CoInitialize is the original initialization function, replaced by its more flexible successor, CoInitializeEx. CoInitialize is essentially a shorthand for CoInitializeEx, as it calls CoInitializeEx with the COINIT_APARMENTTHREADED flag. Your COM applications should use CoInitializeEx to initialize COM, as this function allows you to explicitly specify the concurrency model of your current execution context.

Once you have specified the concurrency model for your execution context, you may not change it. In other words, you cannot do the following:

// Thread A first calls
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
// Thread A later tries to change the concurrency model
CoInitializeEx(NULL, COINIT_MULTITHREADED);  // ERROR:  RPC_E_CHANGED_MODE

Just leave these models alone for now; you’ll use them later in Chapter 5.

You should call CoUninitialize when you finish using COM in order to free up COM related resources used by the corresponding execution context. You must do this for each successful call to CoInitialize or CoInitializeEx. This API function is trivial, because it takes no argument.

Having seen these API functions, you can now add COM initialization and cleanup to your server. The following shows a shell of a simple COM server:

void main(int argc, char **argv)
{
   // Initialize COM
   HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);

   // ...

   // Uninitialize COM
   if (SUCCEEDED(hr))
      CoUninitialize();
}

As you can see, a simple COM server must initialize and clean up COM. You have called CoInitializeEx with COINIT_MULTITHREADED to simplify your server code—you’ll see why in Chapter 5. Other important things, such as server lifetime management and factory registration, are also necessary and will be discussed in a moment.

In-process servers

In-process servers are hosted by a client executable, since they can’t execute by themselves. For this reason, in-process servers do not call CoInitialize[Ex] or CoUninitialize, since the hosting client should make these calls. However, an in-process server can still specify the concurrency model of its COM objects using a registry key, but you’ll learn how to do to this in Chapter 5.

Dynamic Activation Support

Once you have initialized COM, you may publish your factories to the world dynamically at runtime. The way you do this depends on the type of server you’re dealing with (out-of-process or in-process). Each class factory is mapped to a particular COM class’s CLSID, and the CLSID is what clients use to get to a particular class factory. So when you publish your factories dynamically at runtime, you associate the factories with their related CLSIDs. By the same token, when your server shuts down, you must revoke this announcement. Examine in the next two sections how to do this for both out-of-process and in-process servers.

Out-of-process servers

For an out-of-process server, you publish your factories dynamically by registering them with the Service Control Manager (SCM). Recall that the SCM is part of the COM runtime that plays a vital role in dynamic and remote activation. In order to help the SCM do its job, you must notify the SCM that your class factories are ready to manufacture COM objects. Typically you do this upon server startup by calling the CoRegisterClassObject COM API function:

HRESULT CoRegisterClassObject(
  REFCLSID rclsid,      // CLSID to be registered
  IUnknown * pUnk,      // Pointer to the factory's IUnknown
  DWORD dwClsContext,   // One of the values from the CLSCTX enumeration
  DWORD flags,          // Type of connections to the factory
  LPDWORD lpdwRegister  // Pointer to returned cookie
);

This API function basically enters the class factory associated with a given CLSID into a global class table maintained by COM. The first parameter lets you specify the CLSID associated with the class factory that you are registering, and the second parameter lets you pass in a pointer to the class factory’s IUnknown. In essence, these two parameters let you assign or map a CLSID to a class factory, thus saying that the class factory can create instances of a COM class identified by said CLSID. This allows a client anywhere in cyberspace to ask COM for this class factory using the associated CLSID. For example, when a client makes an activation request using this CLSID, the SCM can return to the client a pointer to this class factory’s IUnknown.

In the third parameter of CoRegisterClassObject, you may pass any combination of the following values:

CLSCTX_INPROC_SERVER
CLSCTX_INPROC_HANDLER
CLSCTX_LOCAL_SERVER
CLSCTX_REMOTE_SERVER

This parameter allows you to specify how clients can use the registered class factory. For example, if you specify CLSCTX_INPROC_SERVER, the class factory will be registered to support in-process activation requests. This means that COM will return the registered IUnknown pointer to the client that requests in-process (in-process server) activation of this class factory. Likewise, if you specify CLSCTX_LOCAL_SERVER, the class factory will be registered to support local out-of-process (local server) activation requests. This means that COM will return the registered IUnknown pointer to the client that requests for local out-of-process activation. And so on.

Because it is possible to combine these values, COM defines three shortcuts as shown:

#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)

Given these shortcuts, if you specify CLSCTX_SERVER, the class factory will be registered to support in-process, local out-of-process (local server), and remote out-of-process (remote server) activation requests. This means that COM will return the registered IUnknown pointer to the client that requests for in-process, local, or remote activation. For simplicity, you normally pass CLSCTX_SERVER into the third parameter (more on this later in the discussion of “Clients”).

It’s important to note the fourth parameter, since it indicates how the class factory is to behave. A server may pass any one of the following registration flags into the fourth parameter of CoRegisterClassObject:

REGCLS_SINGLEUSE
REGCLS_MULTIPLEUSE
REGCLS_MULTI_SEPARATE
REGCLS_SURROGATE
REGCLS_SUSPENDED

When you register a class factory with the REGCLS_SINGLEUSE registration flag, you are specifying that the factory can be used only once. Since factories are singletons, this implies that a new client request for the factory will cause the SCM to launch a new out-of-process server to handle the new request. This can be a nice functionality for some applications, but it can be a resource hog for others, because every single client request means spawning a new server process. For these other implementations, we can use the REGCLS_MULTIPLEUSE or REGCLS_MULTI_SEPARATE flag to indicate that our registered class factory can be used multiple times. For simplicity, use REGCLS_MULTIPLEUSE since there are subtle differences between these two flags.

The subtlety appears when you pass CLSCTX_LOCAL_SERVER as the third parameter. If you specify CLSCTX_LOCAL_SERVER and REGCLS_MULTIPLEUSE, the class factory will be automatically registered to support in-process activation requests. In this case, when client code within the same server requests for in-process activation of the registered class factory, COM will return a pointer to the registered IUnknown pointer.

However, if you specify CLSCTX_LOCAL_SERVER and REGCLS_MULTI_SEPARATE, the class factory will not be automatically registered to support in-process activation requests. In this case, when client code within the same server requests for in-process activation of the registered class factory, COM will not return the registered IUnknown pointer. Instead, COM will load an in-process server that supports the requested class factory into the client process. It will then return the IUnknown pointer obtained from the loaded in-process server to the client code, which happens to exist in the same process as the loaded in-process server. So, the REGCLS_MULTI_SEPARATE registration flag allows finer control over a class factory’s behavior.

The REGCLS_SURROGATE registration flag is used for custom surrogate processes, in case you don’t want to use the default system surrogate (dllhost.exe). Remember from Chapter 2 that a surrogate server is used to host a remote in-process server, as an in-process server cannot run alone. When you use the REGCLS_SURROGATE registration flag to register a class factory, you are telling COM that the factory is a generic factory for the surrogate process. As such, this class factory is a shell that delegates all factory calls to the actual class factory within the hosted in-process server.

The REGCLS_SUSPENDED registration flag is important in out-of-process servers. It allows you to register all factories exposed by the server without making them active. This is important to prevent certain race conditions that may occur in a multithreaded environment. For example, assume you have two factories to register, and you have registered only the first one. If you don’t suspend the first factory, COM will immediately allow incoming activation requests to be serviced. Before you can register the second factory, an event might have signaled a request to shut down the server, causing possible access violations in future calls to use the second class factory. To prevent race conditions, COM allows you to register all your factories using the REGCLS_SUSPENDED flag. Once you have registered all supported factories, you tell COM to reveal all your factories to the world at once by calling the following simple COM API function:

HRESULT CoResumeClassObjects();

A successful call to CoRegisterClassObject returns a registration cookie that you must hold onto. When you want to shut down the factory (normally right before your application exits), you would use this cookie to withdraw or revoke the class factory from public view. Failing to properly revoke your class factories prior to application shutdown can be a fatal mistake, as the registered IUnknown pointers for these class factories are no longer valid. To revoke a class factory, you would call the following API function, passing in the cookie:

HRESULT CoRevokeClassObject(DWORD dwRegister);

Here’s an example of an out-of-process server that registers and revokes class factories to help COM support dynamic activation. Notice that you globally instantiate two factories, COcrEngineFactory and CRoboCOMFactory. Since they are globally instantiated, they are singletons that live for the lifetime of your server. Each of these factories can dynamically manufacture their associated COM objects upon client request.

COcrEngineFactory g_OcrEngineFactory;
CRoboCOMFactory g_RoboCOMFactory;

void main(int argc, char **argv)
{
   // Initialize COM
   HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);


   // Call CoRegisterClassObject for each exposed factory and save
   // the associated returned cookie for unregistration.
   DWORD dwRoboCOMCookie = 0;
   DWORD dwOcrCookie = 0;
   // Register COcrEngineFactory with COM, but don't make it active.
   // This can be done using the REGCLS_SUSPENDED flag.
   hr = CoRegisterClassObject(CLSID_OcrEngine,
                              &g_OcrEngineFactory,
                              CLSCTX_SERVER,
                              REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED,
                              &dwOcrCookie);
   // Register CRoboCOMFactory with COM, but don't make it active.
   // This can be done using the REGCLS_SUSPENDED flag.
   hr = CoRegisterClassObject(CLSID_RoboCOM,
                              &g_RoboCOMFactory,
                              CLSCTX_SERVER,
                              REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED,
                              &dwRoboCOMCookie);

        // Register other supported factories.

   // Let all the factories loose.
   hr = CoResumeClassObjects();

        // ...

   // Revoke our factories.
   // Call CoRevokeClassObject for each factory,
   // passing in the associated cookie, to
   // revoke the class factory from public view.
   hr = CoRevokeClassObject(dwOcrCookie);
   hr = CoRevokeClassObject(dwRoboCOMCookie);

   // Uninitialize COM
   CoUninitialize();
}

After initializing the COM library, call CoRegisterClassObject to register each factory that you want to make public, in order to support dynamic activation. Since you have two factories, you call this function twice passing in the appropriate information and getting back two different cookies. You need to save these cookies so that you can later revoke the factories. Note that you pass REGCLS_MULTIPLEUSE|REGCLS_SUSPENDED in the fourth parameter. This indicates that the g_OcrEngineFactory can be used multiple times. In addition, it must not be active until you call the CoResumeClassObjects API function to prevent activation-related race conditions. After you let the factories loose, they’re ready to service activation and object creation requests. Before you close down your server, you must revoke all the registered class factories using the associated cookies that you have previously saved.

In-process servers

Unlike out-of-process servers, in-process servers are loaded by a client process. Therefore, in-process servers typically do not register their supported factories with the SCM. The COM runtime requires in-process servers to expose a DLL entry point called DllGetClassObject, which will be called by COM. This entry point returns a requested class factory interface pointer to a client, if the in-process server exposes the requested factory. Here’s an example of DllGetClassObject that must be supported by an in-process COM server:

COcrEngineFactory g_OcrEngineFactory;
CRoboCOMFactory g_RoboCOMFactory;

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
   HRESULT hr = S_OK;

   if (rclsid == CLSID_OcrEngine) {
      hr = g_OcrEngineFactory.QueryInterface(riid, ppv);
   } else if (rclsid == CLSID_RoboCOM) {
      hr = g_RoboCOMFactory.QueryInterface(riid, ppv);
   } else {
      hr = CLASS_E_CLASSNOTAVAILABLE; *ppv = NULL ;
   }

   return hr;
}

Again, notice that your factories are global variables, because they’re singleton objects. The DllGetClassObject function takes a reference to a CLSID that identifies the factory. It also takes an IID that represents the interface that a caller is requesting. Typically, this IID will be IID_IClassFactory, which is the standard interface for object creation. The resulting interface pointer will be returned to the caller via the third parameter. The code for this function is straightforward. For each CLSID that you support, you simply delegate the interface pointer request to the corresponding class factory’s QueryInterface function. As you have learned earlier, the target factory’s QueryInterface function will return the resulting interface pointer via *ppv upon success or return an E_NOINTERFACE status code upon failure. If you don’t support a requested CLSID, you simply return the CLASS_E_CLASSNOTAVAILABLE status code. In addition, you must also set the resulting pointer to NULL.

Typically, you don’t call CoRegisterClassObject within in-process servers. However, no one’s stopping you from doing so. You can call CoRegisterClassObject (and company) after the in-process server is loaded into memory. For example, you may do this by spawning a thread, which enters into an execution context. Within this execution context, you may register and unregister factories as you’ve seen earlier.

Component Lifetime

Now that you know how to support dynamic activation, turn your attention to server management. Initialization and termination were talked about earlier, but they are only half the story to server management. Before you can uninitialize COM, you must make sure that no external sources are holding references to your server. Simply put, you must make sure that it’s safe to shut down your server before attempting the shutdown. As a general rule, you must keep a component alive if any of the following holds true:

  • There are living COM objects.

  • There are locks on the server.

  • There is user interaction.

  • There are references to any class factory.

Of these, you must take into account the first two cases. You need to worry about the third rule only when your component is in fact a user interface application (for instance, a spreadsheet application). In this case, even if all conditions are met for server shutdown, your server may not initiate shutdown if a user is interactively working with it. In simple implementations, you normally don’t worry about the fourth rule, since a factory is a usually a singleton object. This means that typically only one instance of a factory exists during the lifetime of a given server. In this sense, managing reference counts for factories is unnecessary.

Knowing these kinks, here is a general rule for component lifetime management: keep the server alive if there are living COM objects or outstanding locks on the server. To manage living COM objects, call a special function named ComponentAddRef in the constructors of all your COM objects, except the factory’s constructor. Likewise, call ComponentRelease in all destructors of your COM objects. For example, here are the constructor and destructor of the CoOcrEngine COM object, which you have seen earlier in Chapter 3:

CoOcrEngine::CoOcrEngine() : m_lRefCount(0) { ComponentAddRef(); }
CoOcrEngine::~CoOcrEngine() { ComponentRelease(); }

ComponentAddRef and ComponentRelease are global helper functions that you write. They increment and decrement a global component-level reference count. A component-level reference count includes the counts of both living COM objects and server locks.

To manage server locks, you will need to add the following code, which you’ve also seen earlier, to your factory’s LockServer method:

STDMETHODIMP LockServer(BOOL fLock)
{
   if (fLock) {
      ComponentAddRef();
   } else {
      ComponentRelease();
   }
   return S_OK;
}

When this method is called with a TRUE argument, meaning someone asks you to lock your server in place so that it won’t die unexpectedly, you increment the component-level reference count. When it is called with a FALSE argument, meaning someone releases a server lock, you decrement the component-level reference count.

You will implement ComponentAddRef and ComponentRelease next, since their implementations depend on server types.

Out-of-process servers

For a multithreaded COM server, managing server lifetime can be simply done using a Win32 event object. For instance, here’s the newly added code to manage the lifetime of your server component:

HANDLE g_hExitEvent = NULL;
COcrEngineFactory g_OcrEngineFactory;

void main(int argc, char **argv)
{
   // Server lifetime management.
   g_hExitEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
   if (g_hExitEvent == NULL) assert(false);

   // Initialize COM
   HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);


   // Register COcrEngineFactory with COM, but don't make it active.
   // Call CoRegisterClassObject for each exposed factory and save
   // the associated returned cookie for unregistration.
   DWORD dwOcrCookie = 0;
   hr = CoRegisterClassObject(CLSID_OcrEngine,
                              &g_OcrEngineFactory,
                              CLSCTX_SERVER,
                              REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED,
                              &dwOcrCookie);

    // Register other supported factories.

   // Let the all the factories loose.
   hr = CoResumeClassObjects();

   // Server lifetime management: sit and wait until exit event is signaled.
   WaitForSingleObject(g_hExitEvent, INFINITE);

   // Revoke our factories.
   // Call CoRevokeClassObject for each factory,
   // passing in the associated cookie, to
   // revoke the class factory from public view.
   hr = CoRevokeClassObject(dwOcrCookie);

   // Uninitialize COM
   CoUninitialize();
   CloseHandle(g_hExitEvent);
}

As you can see, server management using a global event is flairy simple. In main, you create a Win32 event object, which represents an exit event. After you have registered and resumed all the supported factories, you sit and wait until this event is signaled. Once you have received this signal, you revoke your factories and uninitialize COM. This is all fine so far, but who does the signaling? ComponentRelease will do the signaling at the right time.

COM supports a global per-process reference count, which is maintained internally by the COM library for each out-of-process server. An out-of-process server needs to increment and decrement this per-process reference count to correctly manage server lifetime in a multithreaded environment. To atomically increment or decrement this process-wide reference count, you must use the following COM API functions:

ULONG CoAddRefServerProcess();
ULONG CoReleaseServerProcess();

CoAddRefServerProcess atomically increments the per-process reference count, which is managed internally by COM. CoReleaseServerProcess atomically decrements this reference count. The real reason for using these API functions is to prevent race conditions dealing with untimely activation requests.

Assume for the moment that you’re not using these atomic operations. In a multithreaded environment, it is possible for a component-level reference count to become zero, and for a thread switch to occur before a server revokes a given class factory. During this minute window, a new activation request may arrive targeting the still valid factory. This can potentially cause an access violation, since the server is about to die off. Aware of this problem, COM provides the CoReleaseServerProcess API function to suspend all factories in the server, preventing these new activation requests from coming in. COM does this when it detects that the per-process reference count becomes zero. If a request comes at this moment, the SCM will launch a new server to support the new activation request, since it sees a disabled factory, which is suspended by COM. COM knows when the per-process reference count becomes zero, because it is responsible for maintaining this count. Your job is just to correctly call these atomic operations at the appropriate time (i.e., in a COM object’s constructor and destructor and in a factory’s LockServer method).

Knowing these API functions, you can implement the ComponentAddRef and ComponentRelease helper functions, which respectively increment and decrement the component-level reference count. The ComponentAddRef helper function is straightforward; it just increments the per-process reference count managed by COM. Recall that ComponentAddRef is called by LockServer(TRUE) and by the constructors of all COM objects.

// Return value use only for debugging purposes
inline ULONG ComponentAddRef()
{
   return (CoAddRefServerProcess());
}

The ComponentRelease method is the reverse of ComponentAddRef, as it decrements the per-process reference count that’s maintained by COM. Recall that ComponentRelease is called by LockServer(FALSE) and by COM objects’ destructors. In other words, your server decrements the per-process reference count when a COM object dies off or when a lock is being released. ComponentRelease will also signal server shutdown at the appropriate time. In particular, it sets the exit event when the per-process reference count falls to zero; that is, when there are no longer any locks or living COM objects. Thus, you can tell the server to shut down by signaling an exit event.

inline ULONG ComponentRelease()
{
   ULONG ul = CoReleaseServerProcess();
   if (ul==0) { SetEvent(g_hExitEvent);  }
   return ul ;
}

That, in a nutshell, is all there is to lifetime management of a multithreaded out-of-process server. There are subtle differences between single-threaded apartment (STA) and multithreaded apartment (MTA) servers, but these topics will be discussed in Chapter 5.

In-process servers

Component lifetime management for in-process servers is managed differently than for out-of-process servers. You don’t use the CoAddRefServerProcess and CoReleaseServerProcess API functions, because an in-process server is usually hosted by its client, the EXE that’s loading and using it.

In order to implement lifetime management of in-process servers, you typically keep a global variable that represents the component-level reference count. This component-level reference count keeps a count of both the living objects and outstanding locks, as shown here:

// Counts the number of living objects and outstanding locks.
LONG g_lComponentRefCounts = 0;

You can then replace the ComponentAddRef and ComponentRelease helper functions as follows:

// Return values for debugging purpose only
inline ULONG ComponentAddRef()
{
   return (InterlockedIncrement(&g_lComponentRefCounts));
}

inline ULONG ComponentRelease()
{
   return (InterlockedDecrement(&g_lComponentRefCounts));
}

ComponentAddRef atomically increments the component-level reference count, since it’s using the thread-safe InterlockedIncrement Win32 API function. Likewise, ComponentRelease atomically decrements the component-level reference count. Unlike the earlier implementation, which is geared toward out-of-process servers, ComponentRelease doesn’t set an event to end the server; it just decrements the component-level reference count. So how can the hosting process unload this in-process server when the component-level reference count falls to zero?

COM requires that all in-process servers implement a special DLL entry point called DllCanUnloadNow for this specific purpose. A hosting process may call this function at arbitrary intervals to ask whether it can unload the in-process server. When this function returns S_OK, indicating that there is no longer any living object or server lock, the hosting process unloads the in-process server. This function is simple:

STDAPI DllCanUnloadNow(void)
{
   if (g_lComponentRefCounts==0) {
      return S_OK;
   } else {
      return S_FALSE;
   }
}

Implementation Registration

You know how to dynamically publish the availability of your factories. You also know how to correctly manage the lifetime of your server. In order for COM to find a COM object requested by clients, COM must know about the location and configuration information of the COM object’s associated COM class. On a local workstation, all the configuration information regarding a COM class is stored in an implementation repository called a registry.[39] All COM related configuration information on the local workstation is stored in the following registry keys:

// Windows NT 4.0
[HKEY_LOCAL_MACHINE\Software\Classes]

// HKEY_CLASS_ROOT is a short cut for the above.
// HKEY_CLASS_ROOT is also retained for backwards compatibility.
[HKEY_CLASSES_ROOT]

// Windows 2000
[HKEY_CURRENT_USER\Software\Classes]

Although Windows NT 4.0 supports COM configuration only at the machine level, Windows 2000 supports COM configuration at the user level. This finer support allows easier COM-related security configurations.

COM looks for a component’s configuration information to correctly load it. Configuration information mainly deals with a particular COM class, identified by a specific CLSID. In addition, COM also looks for interface and security-related configuration information in the registry to manage marshaling and distributed access.

There is quite a bit of configuration information regarding a component. One way to configure a component is to create a registry script file and merge it with the system registry. You may also use the registry editor to enter all the configuration data into the registry by hand, but this is extremely tedious and error prone. Therefore, COM servers should support self registration in order to minimize installation complexity.

In the next section, you’ll learn how to support self registration. As you do this, you’ll also pick up the necessary registry entries that COM looks for. Like everything else, self registration and registry entries are different for out-of-process and in-process servers.

Out-of-process servers

To support self registration, an out-of-process server should support the following command-line options:

-RegServer
-UnRegServer

The -RegServer switch indicates that the server must enter all supported COM-related configuration information (specific to the server of course) into the registry. The -UnRegServer switch indicates that the server must remove all COM-related configuration information from the registry. Here’s example code in main to support these options:

void main(int argc, char **argv)
{
   // Implementation repository registration.
   if (argc > 1)
   {
      if (_stricmp(argv[1], "-RegServer")==0 ||
          _stricmp(argv[1], "/RegServer")==0) {
         RegisterComponent();
         return ;
      }
      if (_stricmp(argv[1], "-UnRegServer")==0 ||
          _stricmp(argv[1], "/UnRegServer")==0) {
         UnregisterComponent();
         return ;
      }
   }

   // Initialize COM
   // ...
   // Uninitialize COM
}

Before you implement the RegisterComponent and UnregisterComponent helper functions, study the minimum registry entries needed for an out-of-process server.

An out-of-process server must provide configuration information for each supported COM class. If you were to register the OcrEngine COM class into the registry, you would need at least the following registry entries. Each entry in brackets is a registry key. Each entry without brackets is called a named value. The @ sign indicates the default value of a given key.

[HKEY_CLASSES_ROOT\CLSID\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}]
@="OcrEngine"
[HKEY_CLASSES_ROOT\CLSID\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}\
     LocalServer32]
@="c:\dcom\ocrsvr.exe"

You may be wondering about OcrEngine. This is simply a human-readable description for the given CLSID; as such, you can give it any string value you like. These two registry entries say that ocrsvr.exe is an out-of-process server, which exposes a COM class, identified by the [DF22A6B2-A58A-11d1-ABCC-00207810D5FE] CLSID. You must register the path to your out-of-process server under LocalServer32, so that COM can find your server during a client activation request. Basically, this is all you need to register a COM class for an out-of-process server.

However, in order to support distribution, you should provide an application identifier (AppID). AppIDs are saved under the following key:

[HKEY_CLASSES_ROOT\AppID]

An AppID is a GUID, which can be generated using the guidgen.exe utility. This GUID groups together a bunch of COM classes that belong to the same COM server. In this sense, this grouping virtually represents a COM application (therein lies the name AppID). COM uses this grouping in order to simplify the administration of common security and remote server settings. If you don’t specify an AppID, the DCOM Configuration (dcomcnfg.exe) tool will automatically add an AppID entry for an out-of-process server to support distributed computing. All CLSIDs that point to the same AppID will share the same remote configuration settings. As shown below, our OcrEngine COM class points to the AppID [EF20ACA0-C12A-11d1-ABF6-00207810D5FE], and therefore uses the remote configuration settings specified under such AppID:

[HKEY_CLASSES_ROOT\CLSID\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}]
@="OcrEngine"
"AppID"="{EF20ACA0-C12A-11d1-ABF6-00207810D5FE}"

[HKEY_CLASSES_ROOT\AppID\{EF20ACA0-C12A-11d1-ABF6-00207810D5FE}]
@="OcrEngine"

[HKEY_CLASSES_ROOT\AppID\ocrsvr.exe]
"AppID"="{EF20ACA0-C12A-11d1-ABF6-00207810D5FE}"

The last entry shown above identifies an EXE that maps to a specific AppID. This specific entry is not totally necessary, but good to maintain as a cross-reference. For example, COM looks at this entry to get the AppID for a given executable file name, as discussed in Chapter 5.

Now that you know the basic registry entries required by COM, you may write your RegisterComponent helper function.

void RegisterComponent()
{
   wchar_t wszKey[MAX_PATH];
   wchar_t wszValue[MAX_PATH];
   HKEY hKey = 0;

   // Create the key to hold our CLSID.
   // HKCR is a common shorthand used in written text for HKEY_CLASSES_ROOT.
   // HKCR\CLSID\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}
   wcscpy(wszKey, TEXT("CLSID\\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}"));
   RegCreateKey(HKEY_CLASSES_ROOT, wszKey, &hKey);

   // Add the following default value to represent the name of our COM class.
   // @="OcrEngine"
   wcscpy(wszValue, TEXT("OcrEngine"));
   RegSetValueEx(hKey, 0, 0, REG_SZ, (BYTE*)wszValue, ByteLen(wszValue));

   // Associate this COM class with an AppID.
   // "AppID"="{EF20ACA0-C12A-11d1-ABF6-00207810D5FE}"
   wcscpy(wszValue, TEXT("{EF20ACA0-C12A-11d1-ABF6-00207810D5FE}"));
   RegSetValueEx(hKey, TEXT("AppID"), 0, REG_SZ,
                 (BYTE*)wszValue, ByteLen(wszValue));
   RegCloseKey(hKey);

   // Create a key under the CLSID entry to store the local server path.
   // HKCR\CLSID\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}\LocalServer32
   wcscpy(wszKey, TEXT("CLSID\\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}\\")
          TEXT("LocalServer32"));
   RegCreateKey(HKEY_CLASSES_ROOT, wszKey, &hKey);

   // Dynamically determine and add the server path.
   // @="...path...\ocrsvr.exe"
   GetModuleFileName(0, wszValue, MAX_PATH);
   RegSetValueEx(hKey, 0, 0, REG_SZ, (BYTE*)wszValue, ByteLen(wszValue));
   RegCloseKey(hKey);

   // Create key to store AppID information for
   // security and remote configuration.
   // HKCR\AppID\{EF20ACA0-C12A-11d1-ABF6-00207810D5FE}
   wcscpy(wszKey, TEXT("AppID\\{EF20ACA0-C12A-11d1-ABF6-00207810D5FE}"));
   RegCreateKey(HKEY_CLASSES_ROOT, wszKey, &hKey);
   // Default value
   // @="OcrEngine"
   wcscpy(wszValue, TEXT("OcrEngine"));
   RegSetValueEx(hKey, 0, 0, REG_SZ,
                 (BYTE*)wszValue, ByteLen(wszValue));
   RegCloseKey(hKey);

   // Not necessary but nice to have as a cross-reference.
   // Add a cross-reference mapping of an executable to its associated AppID.
   // HKCR\AppID\ocrsvr.exe
   wcscpy(wszKey, TEXT("AppID\\ocrsvr.exe"));
   RegCreateKey(HKEY_CLASSES_ROOT, wszKey, &hKey);
   // "AppID"="{EF20ACA0-C12A-11d1-ABF6-00207810D5FE}"
   wcscpy(wszValue, TEXT("{EF20ACA0-C12A-11d1-ABF6-00207810D5FE}"));
   RegSetValueEx(hKey, TEXT("AppID"), 0, REG_SZ,
                 (BYTE*)wszValue, ByteLen(wszValue));
   RegCloseKey(hKey);
}

This function is fairly straightforward. Simply use the Win32 registry API functions to create keys and add the necessary named values into the registry. To create a registry key, use the RegCreateKey API function, which returns a handle to the created registry key (HKEY). You can use this handle to further create other keys or named values under this registry key. To create a named value, call the RegSetValueEx API function. The RegCloseKey API function is used to close the opened registry handle. The wcscpy function is the wide-character version of the ANSI strcpy function.

Now, let’s breeze through the UnregisterComponent helper function. This function is extremely simple, because it deletes all the keys created in the RegisterComponent function.

void UnregisterComponent()
{
   RegDeleteKey(HKEY_CLASSES_ROOT, TEXT("CLSID\\")
             TEXT("{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}\\")
             TEXT("LocalServer32"));
   RegDeleteKey(HKEY_CLASSES_ROOT, TEXT("CLSID\\")
             TEXT("{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}"));
   RegDeleteKey(HKEY_CLASSES_ROOT, TEXT("AppID\\")
             TEXT("{EF20ACA0-C12A-11d1-ABF6-00207810D5FE}"));
   RegDeleteKey(HKEY_CLASSES_ROOT, TEXT("AppID\\ocrsvr.exe"));
}

You should note one subtlety with this function though. You must delete all child registry keys before you can delete a parent registry key. That’s why we delete LocalServer32 before we actually delete the CLSID key [DF22A6B2-A58A-11d1-ABCC-00207810D5FE] itself.

Once you have implemented all this for your out-of-process server, you may simply register and unregister all COM-related configuration information by executing the following at the command prompt, assuming the executable name of our server is ocrsvr.exe.

ocrsvr.exe -RegServer (To register the ocrsvr.exe out-of-process component)

ocrsvr.exe -UnRegServer (To unregister the ocrsvr.exe out-of-process component)

In-process servers

You now will learn how to add registration and unregistration support for in-process servers. Unlike out-of-process servers, which use command switches, in-process servers expose two DLL entry points for server registration and unregistration. These functions are DllRegisterServer and DllUnregisterServer.

DllRegisterServer adds configuration information into the registry. An in-process server may host a number of COM classes. You must register all COM classes into the registry if you want COM to find and dynamically load your in-process server. As seen in the following code, this function simply calls the RegisterComponent helper function to register all the COM classes supported by the in-process server:

// Adds entries to the system registry.
STDAPI DllRegisterServer(void)
{
   RegisterComponent();
   return S_OK;
}

The DllUnregisterServer function will undo what has been done by DllRegisterServer. Particularly, it will clean up registry entries no longer needed. This function calls the helper function, UnregisterComponent, to accomplish this.

// Removes entries from the system registry.
STDAPI DllUnregisterServer(void)
{
   UnregisterComponent();
   return S_OK;
}

For an in-process server, you need to add a CLSID entry into the registry for all COM classes your DLL supports. In addition, each one of these entries must point to the location of the in-process server. The location of the in-process server must be registered under the InprocServer32 subkey, as follows:

[HKEY_CLASSES_ROOT\CLSID\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}]
@="OcrEngine"
[HKEY_CLASSES_ROOT\CLSID\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}\
     InprocServer32]
@="c:\dcom\inproc.dll"

Here’s a simple implementation of the RegisterComponent helper function for an in-process server:

void RegisterComponent()
{
   wchar_t wszKey[MAX_PATH];
   wchar_t wszValue[MAX_PATH];
   HKEY hKey = 0;

   // Create the key to hold our CLSID.
   // HKCR\CLSID\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}
   wcscpy(wszKey, TEXT("CLSID\\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}"));
   RegCreateKey(HKEY_CLASSES_ROOT, wszKey, &hKey);

   // Add the following default value to represent the name of our COM class.
   // @="OcrEngine"
   wcscpy(wszValue, TEXT("OcrEngine"));
   RegSetValueEx(hKey, 0, 0, REG_SZ, (BYTE*)wszValue, ByteLen(wszValue));
   RegCloseKey(hKey);

   // Create a key under the CLSID entry to store the local server path
   // HKCR\CLSID\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}\InprocServer32
   wcscpy(wszKey, TEXT("CLSID\\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}\\")
          TEXT("InprocServer32"));
   RegCreateKey(HKEY_CLASSES_ROOT, wszKey, &hKey);

   // Add the in-process server path.
   // @="...path...\inproc.dll"
   // We can obtain the module name of the DLL by calling
   // GetModuleName in DllMain,
   // save it in a global variable, and use it here.
   wcscpy(wszValue, g_wszModuleName);
   RegSetValueEx(hKey, 0, 0, REG_SZ, (BYTE*)wszValue, ByteLen(wszValue));
   RegCloseKey(hKey);
}

This function is straightforward and really deserves no further discussion. The implementation for UnregisterComponent is also straightforward, as shown here:

void UnregisterComponent()
{
   RegDeleteKey(HKEY_CLASSES_ROOT,
                TEXT("CLSID\\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}")
                TEXT("\\InprocServer32"));
   RegDeleteKey(HKEY_CLASSES_ROOT,
                TEXT("CLSID\\{DF22A6B2-A58A-11d1-ABCC-00207810D5FE}"));
}

You have exposed the two entry points required for in-process server self-registration support, but when will these entry points be called? You can either write a small utility to load the in-process server and invoke these functions, or you can use an SDK utility that does it for you. This utility is called regsvr32.exe. To register and unregister an in-process server using this utility, you would invoke the following commands at the command prompt:

regsvr32 inproc.dll (To register the inproc.dll in-process server)

regsvr32 -u inproc.dll (To unregister the inproc.dll in-process server)

Registering interface marshalers

The last thing that you need to register is the proxy/stub DLL, so that COM can marshal your supported custom interfaces (IOcr and ISpell). Recall that you used MIDL and its compiler to specify and compile the interfaces that make up the DLL. Since these are custom interfaces, you have to use interface marshalers for out-of-process requests. Because the MIDL compiler generates registration and unregistration code for the proxy/stub in-process server, all you would have to do to register this in-process server is use the regsvr32.exe utility. For example, to register your proxy/stub DLL, you would execute the following command:

regsvr32 ocrps.dll

When you do this, the in-process server adds the following registry entries for each interface under [HKEY_CLASSES_ROOT\Interface]:

[HKCR\Interface\{D9F23D61-A647-11D1-ABCD-00207810D5FE}]
@="IOcr"
[HKCR\Interface\{D9F23D61-A647-11D1-ABCD-00207810D5FE}\NumMethods]
@="5"
[HKCR\Interface\{D9F23D61-A647-11D1-ABCD-00207810D5FE}\ProxyStubClsid32]
@="{D9F23D61-A647-11D1-ABCD-00207810D5FE}"

These entries are added for the IOcr interface. The first two lines say that the [D9F23D61-A647-11D1-ABCD-00207810D5FE] interface is named IOcr. The second two lines say that it has five methods. The last two lines say that the CLSID for the proxy and stub implementation for the IOcr interface is [D9F23D61-A647-11D1-ABCD-00207810D5FE]. That is, the lines tell COM to go look at that CLSID for the in-process server that implements the proxy and stub for IOcr. Now that you know how to read the earlier six lines, you should be able to read the following six lines without difficulty:

[HKCR\Interface\{D9F23D63-A647-11D1-ABCD-00207810D5FE}]
@="ISpell"
[HKCR\Interface\{D9F23D63-A647-11D1-ABCD-00207810D5FE}\NumMethods]
@="4"
[HKCR\Interface\{D9F23D63-A647-11D1-ABCD-00207810D5FE}\ProxyStubClsid32]
@="{D9F23D61-A647-11D1-ABCD-00207810D5FE}"

The following is registered information regarding the CLSID to which both the IOcr and ISpell interface refer. As expected, there is an InprocServer32 entry that holds the destination of the resulting in-process server that implements the proxy and stub code for both interfaces. This is the DLL that is loaded by COM each time a proxy or stub is needed for marshaling:

[HKCR\CLSID\{D9F23D61-A647-11D1-ABCD-00207810D5FE}]
@="PSFactoryBuffer"
[HKCR\CLSID\{D9F23D61-A647-11D1-ABCD-00207810D5FE}\InProcServer32]
@="c:\dcom\ocrps.dll"
"ThreadingModel"="Both"

(You will examine the named value ThreadingModel in Chapter 5.)



[37] This section deals only with the basics of a COM server. Chapter 5, Infrastructure, deals with concurrency management, which requires changes to the COM server in order to correctly function with the COM runtime.

[38] COM can optimize marshaling for local servers using lightweight RPC (LRPC). LRPC is based on window messages.

[39] In Windows 2000, there is a global implementation repository that is called the active directory.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required