Where the Wild Things Are (with Apologies to Maurice Sendak)

So what exactly is a custom component? In Chapter 1, we saw how OLE is made up of a fair number of services, or components. Each component is composed of one or more objects, and each object has any number of interfaces that describe its functionality and content.

Now, OLE itself cannot possibly implement every service that clients might want. To meet the demand for additional services, a developer can create a custom component that extends the services that OLE already makes available. Through the standard mechanisms described by COM, a client written to use components of a generic prototype can create an instance of that new component and use its services without knowing anything more specific about it. For example, if you write a client that knows how to browse through an object's type information, that client can work with any component—regardless of its CLSID or anything else it does—whose objects implement IProvideClassInfo, through which you can obtain an ITypeInfo pointer. As new components are added to the system, additional entries in the system registry appear in order to identify their servers. Your client, without modification, can then immediately begin to use those new components without trouble.

So a custom component is any set of objects with any set of interfaces that are wrapped up inside some server module. This component is identified with a unique CLSID, and registry entries provide the association between the CLSID and the path of the server module. A client with that CLSID can then ask COM to access that component. COM does whatever it takes to make this happen and then gets out of the way (except for any necessary marshaling support). This creation process is illustrated in Figure 5-1, which shows how there really isn't much between the client and the object.

Figure 5-1.

A client uses COM to access the first interface pointer for an object in some server's component.

COM's Local/Remote Transparency allows any code acting as a client to access and use the services of components without regard to the boundaries between client and component. Some components will be located in DLLs (in-process), others in EXEs (local), and still others in such modules running on other machines (remote) by which a remote component itself might be distributed across many machines. How a server runs—in-process, local, or remote—is called its execution context.

A client doesn't have to care about the execution context because COM ensures that any interface calls, made in either direction, are marshaled across the applicable boundary using the appropriate magic, as illustrated in Figure 5-2. When an in-process component is in use, pointers to the interfaces of the objects within it are in the same address space as the client, so calls through such pointers are direct calls into the object code. When a greater boundary separates the client and the component, COM performs its Local/Remote Transparency magic through proxies and stubs to marshal the call to another process. The greater the distance or separation between client and component, the slower the marshaling of function calls will be, of course.

Figure 5-2.

Clients can use other components across any boundary.

Another term used to describe the role of a proxy is that of object handler. Structurally, a handler is no different than a proxy or an in-process server. But whereas a standard proxy usually forwards every call to a local or remote server, and whereas an in-process server completely implements the object, a handler is everything in the middle. A handler usually exposes an object with all the interfaces the client expects, but the handler itself only provides a partial object implementation of that object. For example, a handler might implement the performance-critical members of certain interfaces, delegating the remainder to its corresponding local server, usually through a standard proxy. A handler is also useful when an object requires an interface that can only be implemented in-process. Some interfaces involve arguments to member functions that simply cannot be shared across process boundaries (like an HDC), and some interfaces do not have marshaling support available (either by design or because such support hasn't been shipped yet). For whatever reason, a handler is necessary; it completes its implementation by establishing communication with its own local server, just as any other client would, but only when absolutely necessary. This cuts down on the total memory overhead necessary for the component at any given time.

However you choose to implement a component with whatever type of server and handler, the objects in that component can, for the most part, support any interfaces you want. The only exceptions are those interfaces that have no marshaling support, in which case you must implement those with an in-process server or handler.