Do not overuse COM

§    What is COM for?

COM allows different threads, processes, and machines to negotiate a connection. Different modules may have been written and compiled independently, but they should be able to use COM to establish that first connection to each other. Once a connection is established, modules have great latitude in the way they interact. By no means are they required to use COM for every subsequent interaction.

Microsoft's "COM Specification" states, "COM, like a traditional system service API, provides the operations through which a client of some service can connect to multiple providers of that service in a polymorphic fashion. But once a connection is established, COM drops out of the picture. COM serves to connect a client and an object, but once that connection is established, the client and the object communicate directly without having to suffer overhead of being forced through a central piece of API code..."

Here "polymorphism" means that different implementations of the same groups of functions can be used interchangeably.

Reading the first chapter of Don Box's book "Essential COM," you understand that COM also serves a more mundane purpose. On Unix, dynamically loaded libraries (*.so) must be present at compile time and their position must be resolved once from a static LD_LIBRARY_PATH when an executable is loaded. COM allows such libraries (*.dll) to be dynamically replaced during execution time. The windows registry is queried to locate particular implementations.

§    How does COM work?

COM assumes that all programming languages can construct an array of function pointers. Compiled binaries of different languages should represent this array of function pointers identically. Any function arguments should also have the same binary form. Thus, two independently compiled and linked modules (programs or shared libraries) can access and call each others' functions. One module can receive a pointer to a position in another process's memory, cast to the appropriate array of pointers, then begin call the functions. This is what the Microsoft means when they say that COM is a language-independent binary standard.

Arrays of function pointers are called interfaces. This is a more restrictive definition of interface than found elsewhere, but the purpose is similar. Interfaces allow a natural association of related chores, without specifying an implementation. The underlying implementation of an interface is usually called a "component." A component may have been written in an object-oriented language and may have persistent state, but not necessarily. All COM interfaces also have one method QueryInterface that lets you access other interfaces associated with the current one. These other interfaces may share the same underlying object, but again not necessarily.

§    What are the drawbacks of COM?

In short, you cannot do anything with a COM interface that you cannot do in plain C with an array of function pointers. This is a huge constraint. COM interfaces do not have the full flexibility of a class in an object-oriented language.

In C++, an interface is a pure virtual base class. In Java, an interface is a supported type that describes class methods without any implementation. Java and C++ classes can implement any number of interfaces. An instance of a derived class can be used anywhere one of those interfaces is required. Users of interfaces can specify the minimal amount of functionality they require. Providers of interfaces can export predictable services with very different implementations. Bridge/Impl classes can make easy-to-implement interfaces behave like convenient full-featured interfaces. These are the benefits of true polymorphism.

Many programmers have never before recognized the power of pure-virtual classes in C++, so they immediately begin to recode many existing classes to exploit this pattern. Unfortunately they unnecessarily assume many of the additional constraints of COM.

In COM, one never accesses instances of objects directly. One sees only arrays of functions that may share state with an underlying object. Through QueryInterface, one can access associated interfaces, but these other interfaces do not necessarily describe views of a single common object. Each QueryInterface requires error code checking and reference counting. This is not true polymorphism. A single instance name cannot be used interchangeably for different interfaces.

Many COM constraints seem arbitrary. A COM interface can derive from only one other COM interface. Interfaces cannot have members, static or otherwise. COM interfaces cannot have virtual destructors, so they cannot be deleted polymorphically. Interfaces cannot be nested. Exceptions are forbidden, so error checking is intrusive and error-prone. Interfaces can never be modified. To add a method, you must define a new interface to supplement the old. The old interface can never be retired.

You cannot instantiate COM interfaces as if they were objects with constructors. Instantiation must occur in global static class factories or from other interface methods. You manage only the reference counts of pointers to interfaces. These interfaces must be managed like distinct entities. Underlying objects, if they exist, are left to your imagination.

§    Reduce the surface area of interfaces

If your code has complicated interfaces that pass large amounts of data, then you very likely have not properly separated the functionality of your components. Components should be as self-sufficient as possible. COM interfaces resemble user interfaces in this way. Fewer controls make interfaces easier to use.

Avoid passing large data directly through a COM interface. You will not have local/remote transparency. If in-process, you would prefer to pass a pointer to an array. If remote, you would be forced to implement IEnum. You do not want an array to be serialized as one big chunk.

The simplest possible COM interface would have a single method that accepts a command language. Such an interface has very extensible functionality, without adding new methods or new interfaces. This is the thinnest possible interface that exposes the functionality of your component.

A fat interface has many methods and passes large data objects. If you must pass large data, then think carefully.

In "Effective COM," Don Box and friends write "prefer typed data to opaque data." Avoid IDataObject and IStream and define your own structures or classes that are understood by both ends of a COM connection. They write "Note that IStream may make sense in cases where truly byte streams are required (for instance, transmitting large medical images), but it could be argued that simply dropping down to sockets for transferring this type of data would be preferable anyway. There is nothing wrong with using COM to transmit a TCP endpoint to set up a transient connection for purposes like this."

You must make it possible for someone to connect to your component with a COM interface. Immediately thereafter, however, find a way for them to use ordinary objects that encapsulate your component. Your COM interface should be able to pass enough information for the user to call the constructor of an ordinary C++ or Java class. The user's object can then use any implementation or protocol you prefer to communicate with your component.

§    This goes double for IDispatch

Don't support IDispatch unless you really need it. IDispatch is required only for typeless scripting languages like VB script. These scripting languages are useful only for managing graphical user interfaces. GUI's do not require large amounts of data. (A 3D graphics component would be written in C++, not VB or VB Script.)

Don Box wrote "Curse the Visual Basic team for the abomination that is IDispatch." With IDispatch, most arguments must be passed with typeless VARIANTs. Arrays must be passed with the clumsy SafeArray. Strings are a nightmare. Functionality that is already present in IUnknown must be reproduced. Connections are expensive.

Ordinary Visual Basic can use IUnknown interfaces exclusively, but unfortunately VB encourages reckless introduction of IDispatch interfaces, or worse, dual interfaces that implement both.

The size of the VB market allows VB to drive the expected behavior of COM interfaces. The convenience of C++ users gets little attention. You should choose carefully what functionality needs to be exported to a GUI-building language like VB.

§    COM isn't enough

COM solves a very specific problem, that of negotiating connections across some compiler boundary. Remember that there are many problems not solved by COM. You still need to figure out how to use Microsoft's non-standard threading model, their non-Posix interaction with the OS and file system, and their huge API for GUI development. Microsoft may provide many tools through COM, but you don't have to buy everything they are selling.

It is still good programming style to segregate your code from dependencies on the operating system and third-party libraries. Those services will change over time, whereas your code may not. Isolate the COM syntax and revert to more portable code elsewhere.

Bill Harlan, May 1999


Return to parent directory.