Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

An Introduction to COM


January 1998/An Introduction to COM


COM: A Distributed Object Architecture

C++ objects provide for good reusable design in the human world of .CPP files, but this makes little difference on the other end of a compile: the operating system will never know your objects. It will simply run the machine code your C++ compiler generated. The benefits of good object-oriented design never transcend the compilation step because most operating systems deal with applications (EXEs), not objects. If you want an object in one process space to communicate with an object in another (perhaps on another machine), you're forced to embed each object in an application "blanket." These objects must then communicate, not by their respective methods, but by some operating-system-defined interprocess (or internetwork) method such as DDE, TCP/IP, Sockets, shared memory, etc.

Microsoft's Component Object Model (COM) provides one avenue of escape from this limitation. COM enables the operating system to see applications as the one or many objects that constitute them. Futhermore, COM allows the operating system to act as a kind of central directory for objects, takes care of creating objects whenever they're needed, destroys them when they're not, and handles all communications between them whether they are in different processes or even different machines. And, perhaps best of all, COM draws a substantial part of its design architecture from C++, so if you're already fluent with the language, you have a head start in understanding COM.

Here are some of the main benefits of COM:

  • C++ class implementations, which exist in libraries or .CPP files, must be compiled and linked into your executable. By contrast, COM objects are never linked at build time to your application. (In fact, the only thing your application ever knows about a COM object is what functions it may or may not support. More on this soon.)
  • The operating system provides a directory service to all COM objects and is responsible for creating, destroying, and keeping track of the actual executable code of COM objects on behalf of an application. One advantage of this method is that if the COM object ever undergoes a change or a new version becomes available, you do not need to recompile your application.
  • Applications that use COM objects can ask the object at run time what functionality it provides. In fact, an application may not even know whether a COM object has the functionality it needs until after it has, in effect, asked the object, "Hey, do you support the abc function? No? Well how about the def function? Yeah? Great, I'll call that then please."
  • COM objects perform a limited kind of "garbage collection." When there are no outstanding pointers to a COM object, it destroys itself.
  • COM objects can reside in separate process spaces, and even on different machines! (See sidebar on DCOM.) The operating system will marshall calls between a COM object and calling applications in different processes. (Marshall is a distributed computing term for packing and sending discrete data between different address spaces, automatically resolving pointer problems and preserving the data's original form). COM objects of course can also exist within the process space of a calling application. COM objects are called "out-of-process servers" or "in-process servers" depending upon whether they reside outside or inside the caller's process space. I discuss these two kinds of servers further in the DCOM sidebar.
  • The operating system is responsible for creating and destroying COM objects. COM objects are never created using C++'s new and delete.
  • COM is a binary standard. A COM object may be written in any language that can produce compatible binary code, including C, C++, Java, and Visual Basic.

To see how the COM architecture would appear in straight C++, examine the classes in Listing 1. This code is, I hope, pretty straightforward, even if the reasons behind its structure are not yet apparent.

Constructing COM Objects

COM involves a lot of new concepts, and it will be easier to understand how to construct COM objects if I demonstrate those concepts in stages. So I first show how to construct a COM-like, or "pseudo-com" object. I'll get to the real thing towards the end of this article.

Among the methods used to construct COM objects, one of the most common is multiple inheritance. Therefore, I use multiple inheritance to construct my pseudo-COM object COMObject:

class COMObject: public InterfaceToGroupOne,
                 public InterfaceToGroupTwo
{
public:

  COMObject(){};
  int GetInterface(enum InterfaceTypes type, void **p)
  {
    if(type==IGROUP1)
    { *p=(InterfaceToGroupOne*)this;
      return ALLS_WELL;
    }
      else
    if (type==IGROUP2)
    {
      *p=(InterfaceToGroupTwo*)this;
      return ALLS_WELL;
    }
      else
    {
      *p=NULL;
      return INTERFACE_NOT_IMPLEMENTED;
    }
  }
}

main()
{
  COMObject com;
  InterfaceToGroupTwo * i;

  com.GetInterface(IGROUP2, (void**)&i);

  i->GroupTwoFunc1();
}

Although this implementation would differ slightly if it used nested classes instead of multiple inheritance, its basic structure would be consistent with the COM model in either case. To help you understand why, I review a few of the goals of COM architecture:

1) An application using a COM object should not have to know what interfaces (i.e., groups of functions) that object supports ahead of time. You can think of the base classes InterfaceToGroupOne and InterfaceToGroupTwo as two separate interfaces containing two separate group of functions. The calling application can query COMObject for the interfaces it supports by calling GetInterface. Using GetInterface, the calling application can ask for a pointer to an IGROUP1, IGROUP2, or IGROUP3, all enumerated types declared in:

enum InterfaceTypes { IGROUP1, IGROUP2, IGROUP3,
                      INTERFACE_NOT_IMPLEMENTED };

The body of GetInterface evaluates the type argument, and simply fills p with the address of the appropriate base component of class COMObject. (From here on I refer to this address as the interface.)

If the calling application asks for IGROUP1, p will get a pointer to the base class InterfaceToGroupOne. If the caller asks for IGROUP2, p will get a pointer to InterfaceToGroupTwo. If the caller asks for IGROUP3, p will get a value of NULL and GetInterface returns the value INTERFACE_NOT_IMPLEMENTED.

This ability of a COM object to indicate that an interface is not implemented is important. (A real COM object uses the same mechanism as my pseudo-COM object, but returns the enum E_NOINTERFACE.) This allows a COM object to say to a client, "I understand that you would like me to support this kind functionality, but I don't right now. Why don't you try another interface?"

This ability is necessary if you are writing OLE COM objects. OLE is built on top of COM, and there are a large number of standard OLE interfaces that an OLE COM object may or may not support. For example, an OLE application may want to know if your object can save itself within a file it has already created (for example, an Excel spreadsheet object in a Word document). The OLE application will ask your object if it supports the interface IPersistStream. If your object does not support IPersistStream, the application may then ask if it supports IPersistFile so that it can save itself into a file of its own. Using this procedure, an application can explore all the possible ways that a COM object can perform a task.

2) All COM interfaces should have a globally unique identifier. When an application queries an object for an interface, it can refer to that interface by its globally unique identifier (GUID). In my example, the various interfaces' GUIDs are simply enumerated values; in the real COM world, GUIDs are 128-bit numbers generated by a utility called GUIDGEN. Microsoft guarantees that GUIDGEN will generate a unique GUID for you throughout the world, provided you have a network card installed. The network card requirement is because GUID generation algorithms use various bits of system information including the network card ID to derive a unique number. If you don't have a network card, you will probably still be fine but you are not "guaranteed" to get a unique ID. GUIDGEN ships with Visual C++.

Coming Closer To COM

Having read the preceding source code, you now understand COM architecture at its most basic level. There's a tendency for books and documentation to make it seem like something more theoretical and difficult than it really is: an object-oriented paradigm in which objects exist independently in the operating system and are therefore available to all applications that want to use them. And, to support this promise, COM objects hide their member variables from the host application, and group their methods by functionality into separate compartments called interfaces. This enables a host application to query an object at run time to determine what interfaces it does and does not support. If you understand these concepts, then you understand the raison d'etre of COM. Everything else has to do with implementation (sometimes complex), but it is all very fathomable if taken in small bits.

I need to discuss two more basic concepts of COM architecture before a real-world COM demonstration will make perfect sense. Like all the parts of COM, these concepts are not difficult if taken individually.

The first of these concepts is COM reference counting. Recall that earlier I said that, unlike C++, COM performs a limited kind of garbage collection: a COM object will automatically destroy itself when there are no more outstanding references to it. The way COM keeps track of outstanding references is exceedingly simple, as shown in the following code example:

class ref
{
public:
  int outstanding_refs;

  ref(){ outstanding_refs=0;}

  void AddRef() { outstanding_refs++;}
  void Release()
  {
    outstanding_refs--;
    if( outstanding_refs==0)
    {
      delete this;
      cout<<"object deleted"<<endl;
    }
  }
};

main()
{
  ref *p1,*p2, *p3;

  p1=new ref;
  p1->AddRef(); //because we have just created a new ref

  p2=p1;
  p2->AddRef();  //There are now two references to ref;
  p2->Release();  //p2 is done pointing to ref.
                  //We must decrement the count
  p2=NULL;

  cout<<"about to delete"<<endl;
  p1->Release();  //ref would now destroy itself because
                //there are no outstanding refs
  p1=NULL;
}

The above code demonstrates that COM's garbage collection is pretty straightforward. The client must inform the COM object's interface of every reference it has to to that interface. The client does so by calling the interface's AddRef function.

Similarly, the client must inform the COM object's interface every time an outstanding reference no longer points to that interface. In this case the client calls the Interface's Release function. When a so-informed COM object realizes that no client still holds a reference to one of its interfaces, it knows it can safely delete itself. Obviously, improper use of reference counting on the part of the client can lead to COM objects that either never delete themselves or delete themselves too early, so remember the following rules:

1) Every time you set a pointer equal to the address of a COM object interface, call AddRef on that interface. When you set this same pointer to some other value, be sure to call Release.

2) When you create a COM object and obtain a pointer to an interface via GetInterface, you don't need to call AddRef, because this function calls AddRef automatically.

3) Every interface in a COM object has its own AddRef and Release function. Remember that it is the interface you are informing as to outstanding references, not the COM object itself. The COM object will destroy itself only when it sees that there are no outstanding references to any of its interfaces.

Class Factories

If you remember that COM objects are created externally to the client application, and may, in fact, be created on another machine entirely, you can see why it doesn't make sense to create a COM object with new or malloc. If the client used one of these standard allocation routines it would imply that 1) the COM object existed in the client's address space — not always true; and 2) the client application owned the COM object — also not true.

A COM object can be in a different address space from the client and may even outlive the application that created it. Given that we cannot simply create a COM object off the local heap, we need a general mechanism for creating COM objects. This mechanism is called a class factory. A class factory is an object your client application creates, which is used to create one or more COM objects.

If you are developing COM objects, it is your responsibility to provide a class factory that knows how to create your objects. That is, the factory must know what construction/initialization arguments are necessary. Having said that, class factories are simple to write, and once written, you never have to deal with them directly again.

The COM library can take care of creating and using your class factory to create COM objects behind the scenes, via the function CoCreateInstance (which I examine in a few short paragraphs). Basically, all you need remember is that a class factory is itself a COM object whose entire reason for being is to create other COM objects. In fact, a class factory is the only way to create COM objects.

Real COM At Last!

Now that I've discussed the "how" of COM, a real-world COM example will demonstrate the "why." The various components I've covered, that is, interfaces, reference counting, and class factories, will make sense in the context of COMCalc, a COM-based calculator object. COMCalc has three interfaces: one for adding and subtracting, one for multiplying and dividing, and another for performing trigonometric functions. By design, the trigonometric functions aren't implemented yet and this third interface tells the client so. Certainly there are better, lower overhead alternatives for your mathematical needs, but COMCalc exists to demonstrate COM in action as simply as possible. Before launching into the code, here is some basic COM boilerplate functionality you'll need to know about:

#include <objbase.h>  //necessary COM headers
CoInitialize();   //Initializes the COM libraries.
                  //This is almost always your first
                  //function call on the client.
CoUninitialize(); //Uninitializes COM libraries.
CoCreateInstance(); //creates the COM object you request.
QueryInterface();   //requests a pointer to a COM interface.

Also, note that all COM objects used in the example are derived from the virtual base class IUnknown. All interfaces in COM must be derived from IUnknown. IUnknown provides the AddRef and Release referencing counting functions, and the QueryInterface function — which is the only way to get the pointer to another interface a COM object supports. In fact, IUnknown is the first interface a client requests from a COM object, because it is the only interface you can be certain a COM object will definitely support. IUnknown therefore has an esteemed place in the COM hierarchy.

Listings 2, 3, and 4 show an abridged version of the source code required for client.exe, the COM client, and COMCalc, an in-process COM server. There's not room to show everything in the magazine, but the full source code is available on the CUJ ftp site. (See p. 3 for downloading instructions.) What's missing here are definitions for the COM server constructor and destructor, the class factory implementations, and lots of auxiliary functions to make COM, DLLs, and the Windows Registry work together.

Class COMCalc multiply inherits from all of the previously mentioned interfaces; and each of them are derived from IUnknown. Remember, IUnknown is king in COM. By inheriting from IUnknown (and every interface must) each interface comes into the world with an AddRef, Release, and QueryInterface function, all vital and irreplacible functions in COM.

Note that the header file, iclient.h (Listing 2) lists only the interfaces and their virtual member functions, not the member variables of IAddSubtract or IMuldiv. This demonstrates that the header files used by COM clients are necessary only for name and linking resolution. They do not, in fact should not, contain anything other than virtual functions.

And now, I turn you loose on the example. If, at this point, you feel that you understand COM somewhat, I have more than done my job. COM is a big topic and this article and its sample only scratches the surface. Check out the sidebar "COM: Where to go from here." And with that, fellow traveler, I wish you the best in your COM endeavors. o

Gregory Brill develops commercial applications and training curriculum for MediaServ, a Manhattan/Toronto based Microsoft Solutions Provider. He has an M.S. in Computer Science from the Rochester Institute of Technology, and he teaches professional and university courses in C, C++, COM, Microsoft Windows Development, transaction processing, and 3-tiered architectures. He can be reached at [email protected].


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.