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.
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.
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.
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.
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 VARIANT
s. 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 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.