An Introduction to COM

You've heard the term often enough lately. Now find out a few basic facts about COM.


January 01, 1998
URL:http://drdobbs.com/an-introduction-to-com/184403442

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:

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].

January 1998/An Introduction to COM/Listing 1

Listing 1: COM-like architecture in C++

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

//Note that the syntax and function names
//are only approximations of COM and serve
//only as an example of how COM might appear
//in a C++ Class. We will see "real COM" later

class InterfaceToGroupOne
{
public:
  InterfaceToGroupOne(){};
  void GroupOneFunc1()
    {cout<<"Group 1 func1"<<endl;}
  void GroupOneFunc2()
    {cout<<"Group 1 func2"<<endl;}
};

class InterfaceToGroupTwo
{
public:
  InterfaceToGroupTwo(){};
  void GroupTwoFunc1()
    {cout<<"Group 2 func1"<<endl;}
  void GroupTwoFunc2()
    {cout<<"Group 2 func2"<<endl;}
};

class COMObject
{
private:
  InterfaceToGroupOne m_I1;
  InterfaceToGroupTwo m_I2;

public:
  int GetInterface(enum InterfaceTypes type,
                   void *p)
  {
    if(type==IGROUP1)
    { p=&m_I1;
      return ALLS_WELL;
    }
    else
    {
      if (type==IGROUP2)
      {
        p=&m_I2;
        return ALLS_WELL;
      }
      else
      {
        p=NULL;
        return INTERFACE_NOT_IMPLEMENTED;
      }
    }
  };

void main()
{
  COMObject k;
  InterfaceToGroupTwo * i2;
  InterfaceToGroupOne *i1;

  k.GetInterface(IGROUP2, i2);
  k.GetInterface(IGROUP1, i1);

  i2->GroupTwoFunc1();
  i1->GroupOneFunc1();
}

OUTPUT:

Group 2 func1
Group 1 func1

//End of File

January 1998/An Introduction to COM/Listing 2

Listing 2: The COMCalc interfaces

//iclient.h

interface IAddSub : IUnknown
{
  virtual int pascal Add(int x, int y)=0;
  virtual int pascal Subtract(int x, int y)=0;
};

interface IMulDiv : IUnknown
{
  virtual int pascal Multiply(int x, int y)=0;
  virtual int pascal Divide(int x, int y)=0;
};

interface ITrigonometry : IUnknown
{
  
};
//End of File

January 1998/An Introduction to COM/Listing 3

Listing 3: The client implementation

//
// Client.cpp - client implementation
//
#include <iostream.h>
#include <objbase.h>

#include "Iclient.h"

void trace(const char* msg) {cout << "Client: \t\t" << msg << endl;}

//  All these GUIDS where generated using GUIDGEN.EXE and are 
//  globally unique for each interface.  

// {59FA0200-13FF-11d1-883A-3C8B00C10000}
extern "C" const IID IID_IAddSub = 
  {0x59fa0200, 0x13ff, 0x11d1, 
    {0x88, 0x3a, 0x3c, 0x8b, 0x0, 0xc1, 0x0, 0x0}};

// {59FA0201-13FF-11d1-883A-3C8B00C10000}
extern "C" const IID IID_IMulDiv = 
  {0x59fa0201, 0x13ff, 0x11d1,
    { 0x88, 0x3a, 0x3c, 0x8b, 0x0, 0xc1, 0x0, 0x0}};

// {59FA0202-13FF-11d1-883A-3C8B00C10000}
extern "C" const IID IID_ITrigonometry = 
  {0x59fa0202, 0x13ff, 0x11d1,
    { 0x88, 0x3a, 0x3c, 0x8b, 0x0, 0xc1, 0x0, 0x0}};

// {59FA0203-13FF-11d1-883A-3C8B00C10000}
extern "C" const CLSID CLSID_ClassFactory =
  { 0x59fa0203, 0x13ff, 0x11d1, 
    { 0x88, 0x3a, 0x3c, 0x8b, 0x0, 0xc1, 0x0, 0x0 } };

class client_out
{
public:
  void operator<<(char*c)
  {
     cout<<"COMCalc Client:\t\t"<<c;
  }
};

client_out out;

int main()
{
  // Initialize COM Library
  CoInitialize(NULL) ;

  cout << "The client calls CoCreateInstance to create "
       << "a COMCalc object\n" << endl;
  cout << "  and get interface IAddSub.\n" << endl;
  
  IAddSub* pIAddSub = NULL ; 
  HRESULT hr = ::CoCreateInstance(CLSID_ClassFactory,
          NULL,  
          CLSCTX_INPROC_SERVER,
          IID_IAddSub, 
          (void**)&pIAddSub) ;
  if (SUCCEEDED(hr))
  {
    out << "Succeeded getting IAddSub. Now we can reference "
        << "its member functions\n" ;
  
    // Use interface IAddSub.
    cout << "3+3=" << pIAddSub->Add(3,3) << endl;
    cout << "3-3=" << pIAddSub->Subtract(3,3) <<endl;
    
    out << "Ask for interface IMulDiv.\n" ;
    IMulDiv* pIMulDiv = NULL ;
  
    hr = pIAddSub->QueryInterface(IID_IMulDiv, (void**)&pIMulDiv) ;
    
    if (SUCCEEDED(hr))
    {
      out<<"Got IMulDiv.\n" ;
      // Use interface IMulDiv.
      cout << "3*3=" << pIMulDiv->Multiply(3,3) << endl;
      cout << "3/3=" << pIMulDiv->Divide(3,3) << endl;

      pIMulDiv->Release() ;
      out<<"Release IMulDiv interface.\n" ;
    }
    else
    {
      out<<"Could not get interface IMulDiv.\n" ;
    }

    out << "Ask for interface ITrigonometry.\n" ;

    ITrigonometry* pITrigonometry = NULL ;

    hr = pIAddSub->QueryInterface(IID_ITrigonometry, 
                                     (void**)&pITrigonometry) ;
    if (SUCCEEDED(hr))
    {
      out << "Got interface ITrigonometry.\n" ;

      pITrigonometry->Release() ;
      out << "Release ITrigonometry interface.\n" ;
    }
    else
    {
      out << "Could not get interface ITrigonometry.\n" ;
      out << "Perhaps a later version of comCalc will support it.\n";
    }

    out << "Release IAddSub interface.\n" ;
    pIAddSub->Release() ;
  }
  else
  {
    cout << "Could not create component. hr = "
         << hex << hr << endl ;    
  }

  // Uninitialize COM Library
  CoUninitialize() ;

  return 0 ;
}
//End of File

January 1998/An Introduction to COM/Listing 4

Listing 4: The COMCalc object

//
// COMCalc.cpp
//

#include <iostream.h>
#include <objbase.h>

#include "Iserver.h"      // Interface declarations
#include "Registry.h"   // Registry helper functions

///////////////////////////////////////////////////////////
//
// Global variables
//
static HMODULE g_hModule = NULL ;   // DLL module handle
static long g_cComponents = 0 ;     // Count of active components
static long g_cServerLocks = 0 ;    // Count of locks

// Friendly name of component
const char g_szFriendlyName[] = "COMCalc Server" ;
// Version-independent ProgID
const char g_szVerIndProgID[] = "Example.COMCalc" ;
// ProgID
const char g_szProgID[] = "Example.COMCalc.1" ;

//
// Interface GUID definitions -- same as in client.cpp
// Not shown here
// ...
//
// Component 
//
class CA : public IAddSub,
     public IMulDiv 
{
public:
  // IUnknown
  virtual HRESULT 
    __stdcall QueryInterface(const IID& iid, void** ppv);
  virtual ULONG __stdcall AddRef() ;
  virtual ULONG __stdcall Release() ;

  // Interface IaddSub
  virtual int __stdcall Add(int x,int y)
    { IAddSub_last_result=x+y; return IAddSub_last_result; }
  virtual int __stdcall Subtract(int x,int y)
    { IAddSub_last_result=x-y; return IAddSub_last_result; }
  
  // Interface ImulDiv
  virtual int __stdcall Multiply(int x,int y)
    { IMulDiv_last_result=x*y; return IMulDiv_last_result; }
  virtual int __stdcall Divide(int x,int y)
    { IMulDiv_last_result=x/y; return IMulDiv_last_result; }

  // Constructor
  CA() ;

  // Destructor
  ~CA() ;

private:
  // Reference count
  long m_cRef ;
} ;

//
// Not shown: CA constructor and destructor
// ...
//

//
// IUnknown implementation
//
HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv)
{    
  if (iid == IID_IUnknown)
  {
    *ppv = static_cast<IAddSub*>(this) ; 
  }
  else if (iid == IID_IAddSub)
  {
    *ppv = static_cast<IAddSub*>(this) ;
    cout<<"COMCalc:\t\tReturn pointer to IAddSub.\n" ; 
  }
  else if (iid == IID_IMulDiv)
  {
    *ppv = static_cast<IMulDiv*>(this) ; 
    cout<<"COMCalc:\t\tReturn pointer to IMulDiv.\n" ; 
  }
  else
  {
    *ppv = NULL ;
    return E_NOINTERFACE ;
  }
  reinterpret_cast<IUnknown*>(*ppv)->AddRef() ;
  return S_OK ;
}

//
// Not shown: CA::AddRef() and CA::Release(), 
// ...
//
//End of File

January 1998/An Introduction to COM/Sidebar

What is DCOM?


DCOM, or Distributed COM, is essentially a "network enabled" COM. A DCOM client and object can communicate over a network as transparently as a regular COM object and client can communicate on a single machine. From a programming perspective, writing a DCOM object is identical to writing a COM object with one additional step: DCOM places a remote procedure call (RPC) layer beneath COM interfaces, so that member functions of an interface map to remote procedure calls. These RPCs "marshal" the function's arguments and return values across a network from a DCOM client to a DCOM server. DCOM accomplishes this marshalling process via the industry standard (with a few Microsoft "enhancements") RPC mechansism. If you've ever worked with RPC, you'll have no problem with this addition to COM. If you haven't, it's probably best to learn basic COM first, and then the RPC layer will be a relatively small additional step. o

January 1998/An Introduction to COM/Sidebar

In-Process vs. Out-of-process COM Servers


COM Servers come in two flavors: in-process and out-of-process. Basically, the in-process server is a DLL (Dynamic Link Library) in Win32 or a shared library on other operating systems. By contrast, an out-of-process server is an executable. Each have their advantages and disadvantages. The COM in-process server is actually loaded into the process space of the calling application, which means greater speed, but also means the calling application will crash if the the COM object misbehaves. Out-of-process servers exist in a different process space from the calling client, which means all function arguments and return values must be "marshalled" between the two address spaces. This in turn means slower speed but a more robust implementation. DCOM servers are often out-of-process servers (EXEs), but an in-process COM compononent can act as a DCOM server if it is "wrapped" in a special type of executable called a surrogate. COM provides a default surrogate for in-process COM servers that want to be DCOM servers; or you can write your own. o

January 1998/An Introduction to COM/Sidebar

COM: Where to Go from Here


While mastering COM may take time, take heart! There are many books on the subject including one by Microsoft Press titled, Inside COM. I find it a readable and useful reference. Also, check out The Essence of OLE with ActiveX by David Platt after you get basic COM under your belt. He uses a workbook step-by-step approach that leverages your knowledge of COM and illustrates some key components of OLE and ActiveX. It's an excellent sample source. And, of course, there is always Kraig Brockshmidt's authoritive work on OLE entitiled, Inside OLE. It's big, it's dense, it can be tough reading, but everything is in there.

Since Microsoft is not only preaching COM, but writing applications based on it, you might take a look at Microsoft Transaction Server (based on COM) to get a sense of COM in a commercial context. The Windows 95/NT shell is also written in COM, so any programming books you encounter on the subject will have a lot of COM content.

Also, if you are interested in developing COM objects for non-Windows platforms, or integrating COM with other distributed object architectures, you might find the following articles of interest:

Feel free to visit my web site, www.infusiondev.com for the COMCalc sample source code files, Windows development information, and shareware. o

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.