Charlie Kindel
Program Manager/OLE Evangelist, Microsoft Developer Relations Group
Created: March 10, 1994
Revised: October 18, 1994
The Component Object Model (COM) provides a single programming model for interacting with objects that live within the calling program's address space, in the address space of a process on the same machine, and in the address space of a process running on a different computer. While the programming model is identical for each of these three situations, there are obviously differences in performance. This article discusses some of the differences and provides techniques for designing objects so the impact of these differences is minimized.
A Component Object Model (COM) object that is executing in the same process space as the user of that object is called an In Process object (InProc for short). Interface method invocations on the object by the user are just as fast as a standard C++ virtual function call.
Figure 1. InProc interface method invocations do not cross process boundaries.
In the case where the user code is executing in the process space of Application A, and the COM object is executing in the process space of Application B, interface method invocations by A on interfaces implemented on B clearly must cross process boundaries. In this case, the COM objects in Application B are cross-process objects, or in OLE 2.0 terms, Local objects.
Figure 2. Local interface method invocations do cross process boundaries.
Future versions of OLE will provide support for accessing objects running on a different machine. Objects that are running on a different machine from the user of the object's interfaces are called Remote objects. This is illustrated in Figure 3 below. (Because OLE with distributed services is not generally available, this article focuses on Local and InProc issues only.)
Figure 3. Remote interface method invocations cross process and machine boundaries.
As mentioned above, the Component Object Model provides the exact same programming model for accessing objects that are in the same process, local to the current machine but out of process, and running remotely on another machine. The only difference is where the object physically runs and how it registers itself.
In a perfect world, calling a method on a COM interface would be as fast as a function call. In many cases, objects can be implemented as InProc objects, and method invocations will be as fast as direct function calls. However, because of real-world requirements, objects often must be implemented as .EXE applications and thus live in separate process spaces. Here are some of the reasons why objects might be implemented in Local servers today:
Developers have a choice between developing their objects in Local servers or InProc servers. The points listed above argue for implementing Local servers. Performance is the primary argument for implementing InProc servers. The following section details a technique that allows the advantages of both Local and InProc servers in certain scenarios.
The "OLE Component Design Issues" article illustrates a potential design for real-time market data applications. In that design (shown below in Figure 4), one or more "container applications" serve as "frames" for one or more "service objects." The containers are relatively dumb and exist primarily to provide unified screen real estate to the service objects. The service objects do useful user interface things such as drawing graphs and allow for the manipulation of data. The service objects get their data from "Data Source objects," which, in turn, get their data from some external source. The following technique assumes that it is more important to have high-performance data transfer between the Data Source objects and the service objects than between the service objects and the container.
Figure 4. Container embeds service objects, which get data from data source objects.
In the scenario above, an instance of a service object is a consumer or user of interfaces from both the container and the Data Source. In the same vein, both the container and the Data Source are users of interfaces supplied by a given instance of a service object. An instance of an object must always live within the process space of only one process. This fact comes from the basic truth about Microsoft® Windows® DLLs: A DLL's code and local heap may be shared between processes, but the code in the DLL always executes using the stack of the caller. Thus, there must be a process boundary either between the container and the service object, or between the service object and the Data Source object.
The interfaces used between the container and service objects are used for client site interactions, while the interfaces between the service objects and the Data Source object are for data transfer. In most real-time data access designs, it is more important to have high-performance data transfer than client site interactions, particularly if the container is really just a "dumb" container of "smart" objects.
Figure 5. Service object running in container's process space
Figure 6 shows the scenario where the service objects live in the container application's address space. In this case, the container application creates an instance of the service object via standard OLE 2.0 "Insert Object" mechanisms, which really boil down to the container calling CoGetClassObject on the service object's CLSID. Upon returning from the CoGetClassObject call, the container will have a pointer to the service object's IClassFactory interface. (Once the service object has been created via IClassFactory::CreateInstance, it obtains a pointer to an interface supplied by the Data Source via some well-known mechanism. There are many ways it can do this, including looking in the running object table.) Because the service object is implemented in an InProc server (that is, in a DLL), the pointer to the IClassFactory interface does not cross any process boundaries. The same is true for the interface pointers the container retrieves by calling IClassFactory::CreateInstance. Therefore, in Figure 6, the client site interactions are fast, but the performance of the data transfer may suffer.
Figure 6. Service object running in data source's process space
Figure 6 shows the scenario where the service objects are actually implemented as InProc servers but are exposed to containers as though they were Local servers. From this picture, it is clear that interface method invocations between the Data Source and the service object will not require marshaling and those between the service object and the container will. Thus, data transfer calls can be made as fast as direct function calls, but client site interactions require the relatively slower marshaled calls.
To ensure that the service objects live in the Data Source's process space, the Data Source application loads each of the service objects into its address space when it starts, then registers their class factory objects as Local.
As described in the OLE 2.0 Software Development Kit, local servers register their class factory objects by calling the CoRegisterClassObject function. InProc servers do not call this function, but instead they export a function named DllGetClassObject. OLE 2.0 provides a core function, CoGetClassObject, for accessing the class factory interface of a given object class. When this function is called, the following happens:
If the class is implemented by a Local server, the OLE libraries cause the named EXE to be executed.
The steps above provide a simplistic description of what happens. In reality, the procedure used by CoGetClassObject is more complex. In particular, the procedure given here assumes that CoGetClassObject was called with the dwClsContext parameter equal to CLSCTX_LOCAL_SERVER | CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, which is the most common case for container applications. The net result of this procedure reaffirms the statement made at the beginning of this article: Users of objects do not know whether the interface pointers they have to those objects are Local, InProc, or Remote. The CoGetClassObject function makes the distinction opaque to callers.
The process described above provides some insight into how a set of InProc objects can be made to appear "Local" to some users and "InProc" to others. A user who requires that the objects be InProc (for example, a Data Source object) should call CoGetClassObject for each service object it is interested in when it starts up. It should then call CoRegisterClassObject for each of the IClassFactory interfaces returned by the calls to CoGetClassObject, but should specify the CLSCTX_LOCAL_SERVER flag.
Because the Data Source uses the CLSCTX_LOCAL_SERVER flag when calling CoRegisterClassObject for each service object, any container that calls CoGetClassObject will get back a marshaled (cross-process) IClassFactory pointer to the service object's class object. When the container calls IClassFactory::CreateInstance to actually create an instance of the service object, it is returned a marshaled interface pointer.