JavaScript EditorFree JavaScript Editor     Ajax Editor 



Main Page
  Previous Section Next Section

COM: Is It the Work of Microsoft… or Demons?

Computer programs today are easily reaching multimillion-line sizes, and large systems will soon reach to billions of lines of code. With programs this large, abstraction and hierarchy are of utmost importance. Otherwise, complete chaos would ensue. Kinda like customer service when you call the phone company.

The two most recent attempts at computer languages that foster more object-oriented programming techniques are, of course, C++ and Java. C++ is really an evolution (or maybe more a regurgitation) of C, with object-oriented hooks built into it. On the other hand, Java is based on C++ but is fully object-oriented and much cleaner. In addition, Java is more of a platform while C++ is simply a language.

Anyway, languages are great, but it's how you use them that counts in the long run. Alas, even though C++ is chock full of cool OO (object-oriented) features, many people don't use them or use them the wrong way. Thus, large-scale programs are still a bit of a problem. This is one of the difficulties that the COM model addresses.

COM was invented many years back as a simple white paper on a new software paradigm, which was similar to how computer chips or Lego blocks work. You simply plug them together and they work. Computer chips and Lego blocks know how to be computer chips and Lego blocks (since their interfaces are well defined), so everything works out. To implement this kind of technology with software, you need a very generic interface that can take on the form of any type of function set you can imagine. This is what COM does.

One of the cool things about computer chips is that when you add more of them to a design, you don't have to tell all the other chips that you've changed something. However, as you know, this is a little harder with software programs. You at least have to recompile to make an executable. Fixing this problem is another goal of COM. You should be able to add new features to a COM object without breaking the software that uses the old COM object. In addition, COM objects can be changed without recompiling the original program, which is very cool.

Since you can upgrade COM objects without recompiling your program, that means you can upgrade your software without patches and new versions. For example, say you have a program that uses three COM objects: one that implements graphics, one for sound, and one for networking (see Figure 5.3). Now imagine that you sell 100,000 copies of this program, but you don't want to send out 100,000 upgrades! To update the graphics COM object, all you do is give the users the new COM object for graphics and the program will automatically use it. No recompiling, no linking, no nothing. Easy. Of course, all this technology is very complex at the low level, and writing your own COM objects is a bit challenging, but using them is easy.

Figure 5.3. An overview of COM.

graphics/05fig03.gif

The next question is, how are COM objects distributed or contained, given their plug-and-play nature? The answer is that there are no rules about this, but in most cases COM objects are DLLs, or Dynamic Link Libraries, that can be downloaded or supplied with the program that uses them. This way they can be easily upgraded and changed. The only problem with this is that the program that uses the COM object must know how to load it from a DLL. But we'll get to that in the "Building a Quasi-COM Object" section later in this chapter.

What Exactly Is a COM Object?

A COM object is really a C++ class or a set of C++ classes that implement a number of interfaces. (Basically, an interface is a set of functions.) These interfaces are used to communicate with the COM object. Take a look at Figure 5.4. Here we see a single COM object that has three interfaces named IGRAPHICS, ISOUND, and IINPUT.

Figure 5.4. The interfaces of a COM object.

graphics/05fig04.gif

Each one of these interfaces has a number of functions that you can call (when you know how) to do work. So a single COM object can have one or more interfaces, and you may have one or more COM objects. Moreover, the COM specification states that all interfaces you create must be derived from a special base class interface called IUnknown. For you C programmers, all this means is that IUnknown is like a starting point to build the interface from.

Let's take a look at the IUnknown class definition:

struct  IUnknown
{

// this function is used to retrieve other interfaces
virtual HRESULT __stdcall QueryInterface(const IID &iid, (void **)ip) = 0;

// this is used to increment interfaces reference count
virtual ULONG __stdcall AddRef() = 0;

// this is used to decrement interfaces reference count
virtual ULONG __stdcall Release() = 0;

};

NOTE

Notice that all methods are pure and virtual. In addition, the methods use __stdcall in deference to the standard C/C++ calling convention. If you remember from Chapter 2, "The Windows Programming Model," __stdcall pushes the parameters on the stack from right to left.


Even if you're a C++ programmer, this class definition may look a bit bizarre if you're rusty on virtual functions. Anyway, let's dissect IUnknown and see what's up. All interfaces derived from IUnknown must implement, at very minimum, each of the methods QueryInterface(), AddRef(), and Release().

QueryInterface() is the key to COM. It's used to request a pointer to the interface functions that you desire. To make the request happen, you must have an interface ID. This is a unique number, 128 bits long, that you assign to your interface. There are 2128 different possible interface IDs, and I guarantee that we wouldn't run out in a billion years even if everybody on this planet did nothing but make COM objects 24 hours a day! More on the interface ID when we get to a real example a little later in the chapter.

Furthermore, one of the rules of COM is that if you have an interface, you can always request any other interface from it as long as it's from the same COM object. Basically, this means that you can get anywhere from anywhere else. Take a look at Figure 5.5 to see this graphically.

Figure 5.5. Navigating the interfaces of a COM object.

graphics/05fig05.gif

TIP

Usually, you don't have to call AddRef() yourself on interfaces or COM objects. It's done internally by the QueryInterface() function. But sometimes you may have to, if you want to increase the reference count to trick the COM object into thinking that there are more references to it than there really are.


AddRef() is a curious function. COM objects use a technique called reference counting to track their life. This is due to one of the specifications of COM: It's not language-specific. Hence, AddRef() is called when a COM object is created and when interfaces are created to track how many references there are to the objects. If a COM object were to use malloc() or new[], that would be C/C++-specific. When the reference count drops to 0, the objects are destroyed internally.

This brings us to a problem—if COM objects are C++ classes, how can they be created or used in Visual Basic, Java, ActiveX, and so on? It just so happens that the designers of COM used virtual C++ classes to implement COM, but you don't need to use C++ to access them or even to create them. As long as you create the same binary image that a Microsoft C++ compiler would when creating a virtual C++ class, the COM object will be COM-compliant. Of course, most compiler products have extras or tools to help make COM objects, so that's not too much of a problem. The cool thing about this is that you can write a COM object in C++, Visual Basic, or Delphi, and then that COM object can be used by any of those languages! A binary image in memory is a binary image in memory.

Release() is used to decrement the reference count of a COM object or interface. In most cases, you must call this function yourself when you're done with an interface. However, sometimes if you create an object and then create another object from that object, calling Release() on the parent will trickle down and Release() the child or derived object. But either way, it's a good idea to Release() in the opposite order that you queried.

More on Interface IDs and GUIDs

As I mentioned earlier, every COM object and interface thereof must have a unique 128-bit identifier that you use to request or access it. These numbers are called GUIDs (Globally Unique Identifiers) in general. More specifically, when defining COM interfaces they're called Interface IDs or IIDs. To generate them, you must use a program called GUIDGEN.EXE created by Microsoft (or a similar program that uses the same algorithm). Figure 5.6 shows GUIDGEN.EXE in action.

Figure 5.6. The GUID generator GUIDGEN.EXE in action.

graphics/05fig06.gif

What you do is select what kind of ID you want (there are four different formats), and then the program generates a 128-bit vector that is guaranteed to never be generated again on any machine at any time. Seem impossible? It's not. It's just math and probability theory. The bottom line is that it works, so don't get a headache asking why.

After you generate the GUID or IID, it's placed on the Clipboard and you can paste it into your programs by pressing Ctrl+V. Here's an example of an IID I just made while writing this paragraph:

// { C1BCE961-3E98-11d2-A1C2-004095271606}
static const <<name>> =
{ 0xc1bce961, 0x3e98, 0x11d2,
{ 0xa1, 0xc2, 0x0, 0x40, 0x95, 0x27, 0x16, 0x6 } };

Of course, you would replace <<name>> with the name you choose for the GUID in your program, but you get the idea.

GUIDs and IIDs are used to reference COM objects and their interfaces. So whenever you make a new COM object and a set of interfaces, these are the only numbers that you have to give to programmers to work with your COM objects. Once they have the IIDs, they can create COM objects and interfaces.

Building a Quasi-COM Object

Creating a full-fledged COM object is well beyond the scope of this book. You only need to know how to use them. However, if you're like me, you like to have some idea of what's going on. So what we're going to do is build up a very basic COM example to help you answer some of the questions that I'm sure I've created for you.

All right, you know that all COM objects contain a number of interfaces, but all COM objects must be derived from the IUnknown class to begin with. Then, once you have all your interfaces built, you put them all in a container class and implement everything. As an example, let's create a COM object that has three interfaces: ISound, IGraphics, and IInput. Here's how you might define them:

// the graphics interface
struct IGraphics : IUnknown
{
virtual int InitGraphics(int mode)=0;
virtual int SetPixel(int x, int y, int c)=0;
// more methods...
};

// the sound interface
struct ISound : IUnknown
{
virtual int InitSound(int driver)=0;
virtual int PlaySound(int note, int vol)=0;
// more methods...
};

// the input interface
struct IInput: IUnknown
{
virtual int InitInput(int device)=0;
virtual int ReadStick(int stick)=0;
// more methods...
};

Now that you have all your interfaces, let's create your container class, which is really the heart of the COM object:

class CT3D_Engine: public IGraphics, ISound, IInput
{
public:

// implement IUnknown here
virtual HRESULT __stdcall QueryInterface(const IID &iid,
                                        (void **)ip)
{ /* real implementation */ }

// this method increases the interfaces reference count
virtual ULONG __stdcall Addref()
                        { /* real implementation */}

// this method decreases the interfaces reference count
virtual ULONG __stdcall Release()
                        { /* real implementation */}

// note there still isn't a method to create one of these
// objects...

// implement each interface now

// IGraphics
virtual int InitGraphics(int mode)
                     { /*implementation */}
virtual int SetPixel(int x, int y, int c)
                     {/*implementation */}


// ISound
virtual int InitSound(int driver)
                     { /*implementation */}
virtual int PlaySound(int note, int vol)
                     { /*implementation */}

// IInput
virtual int InitInput(int device)
                     { /*implementation */}

virtual int ReadStick(int stick)
                     { /*implementation */}

private:

// .. locals

};

NOTE

You're still missing a generic way to create a COM object. This is a problem, no doubt. The COM specification states that there are a number of ways to do it, but none of them can tie the implementation to a specific platform or language. One of the simpler ways to do it is to create a function called CoCreateInstance() or ComCreate() to create the initial IUnknown instance of the object. The function usually loads a DLL that contains the COM code and works from there. Again, this technology is beyond what you need to know, but I just want to throw it out there for you. However, we're going to cheat a little to continue with the example.


As you can see from the example, COM interfaces and coding are nothing really more than slightly advanced C++ virtual classes with some conventions. However, true COM objects must be created properly and registered in the registry, and a number of other rules must be adhered to. But at the lowest level, they are simply classes with methods (or for you C programmers, structs) with function pointers, more or less. Anyway, let's take a brief step back and review what you know about COM.

A Quick Recap of COM

COM is a new way of writing component software that allows you to create reusable software modules that are dynamically linked at run-time. Each of these COM objects has one or more interfaces that do the actual work. These interfaces are nothing more than collections of methods or functions that are referenced through a virtual function table pointer (more on this in the next section).

Each COM object and interface is unique from the others due to the use of GUIDs, or Globally Unique Identifiers, that you must generate for your COM objects and interfaces. You use the GUIDs or IIDs to refer to COM objects and interfaces and share them with other programmers.

If you create a new COM object that upgrades an old one, you must still implement the old interfaces along with any new ones you might add. This is a very important rule: All programs based on COM objects should still work, without recompilation, with new versions of the COM object(s).

COM is a general specification that can be followed with any language on any machine. The only rule is that the binary image of the COM object must be that of a virtual class generated by a Microsoft VC compiler—it just worked out that way. However, COM can be used on other machines, like Mac, SGI, and so on, as long as they follow the rules for using and creating COM objects.

Finally, COM opens up the possibility of creating massive computer programs (in the multibillion-line range) by means of its component-level generic architecture. And of course, DirectX, OLE, and ActiveX are all based on COM, so you need to understand it!

A Working COM Program

As a complete example of creating a COM object and a couple of interfaces, I | have created DEMO5_1.CPP for you. The program implements a COM object called CCOM_OBJECT that is composed of two interfaces, IX and IY. The program is a decent implementation of a COM object, but of course it's missing some of the high-level details like being a DLL, loading dynamically, and so on. But the COM object is fully implemented as far as all the methods and the IUnknown class are concerned.

What I want you to do is look at it very carefully, play with the code, and see how it works. Listing 5.1 contains the entire source for the COM object and a simple C/C++ main() test bed to run it in.

Listing 5.1 A Complete COM Object Program
// DEMO5_1.CPP - A ultra minimal working COM example
// NOTE: not fully COM compliant

// INCLUDES ///////////////////////////////////////////////////

#include <stdio.h>
#include <malloc.h>
#include <iostream.h>
#include <objbase.h> // note: you must include this header it
                     // contains important constants
                     // you must use in COM programs

// GUIDS //////////////////////////////////////////////////////

// these were all generated with GUIDGEN.EXE

// {B9B8ACE1-CE14-11d0-AE58-444553540000}
const IID IID_IX =
{ 0xb9b8ace1, 0xce14, 0x11d0,
{ 0xae, 0x58, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };


// {B9B8ACE2-CE14-11d0-AE58-444553540000}
const IID IID_IY =
{ 0xb9b8ace2, 0xce14, 0x11d0,
{ 0xae, 0x58, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };

// {B9B8ACE3-CE14-11d0-AE58-444553540000}
const IID IID_IZ =
{ 0xb9b8ace3, 0xce14, 0x11d0,
{ 0xae, 0x58, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };


// INTERFACES /////////////////////////////////////////////////

// define the IX interface
interface IX: IUnknown
{

virtual void __stdcall fx(void)=0;

};

// define the IY interface
interface IY: IUnknown
{

virtual void __stdcall fy(void)=0;

};

// CLASSES AND COMPONENTS /////////////////////////////////////

// define the COM object
class CCOM_OBJECT :    public IX,
                    public IY
{
public:

    CCOM_OBJECT() : ref_count(0) {}
    ~CCOM_OBJECT() {}

private:

virtual HRESULT __stdcall QueryInterface(const IID &iid, void **iface);
virtual ULONG __stdcall AddRef();
virtual ULONG __stdcall Release();

virtual    void __stdcall fx(void)
              {cout << "Function fx has been called." << endl; }
virtual void __stdcall fy(void)
              {cout << "Function fy has been called." << endl; }

int ref_count;

};

// CLASS METHODS //////////////////////////////////////////////

HRESULT __stdcall CCOM_OBJECT::QueryInterface(const IID &iid,
                                              void **iface)
{
// this function basically casts the this pointer or the IUnknown
// pointer into the interface requested, notice the comparison with
// the GUIDs generated and defined in the beginning of the program

// requesting the IUnknown base interface
if (iid==IID_IUnknown)
    {
    cout << "Requesting IUnknown interface" << endl;
    *iface = (IX*)this;

    } // end if

// maybe IX?
if (iid==IID_IX)
    {
    cout << "Requesting IX interface" << endl;
    *iface = (IX*)this;

    } // end if
else  // maybe IY
if (iid==IID_IY)
    {
    cout << "Requesting IY interface" << endl;
    *iface = (IY*)this;

    } // end if
else
    { // cant find it!
    cout << "Requesting unknown interface!" << endl;
    *iface = NULL;
    return(E_NOINTERFACE);
    } // end else

// if everything went well cast pointer to
// IUnknown and call addref()
((IUnknown *)(*iface))->AddRef();

return(S_OK);

} // end QueryInterface

///////////////////////////////////////////////////////////////

ULONG __stdcall CCOM_OBJECT::AddRef()
{
// increments reference count
cout << "Adding a reference" << endl;
return(++ref_count);

} // end AddRef

///////////////////////////////////////////////////////////////

ULONG __stdcall CCOM_OBJECT::Release()
{
// decrements reference count
cout << "Deleting a reference" << endl;
if (—ref_count==0)
    {
    delete this;
    return(0);
    } // end if
else
    return(ref_count);

} // end Release

///////////////////////////////////////////////////////////////

IUnknown *CoCreateInstance(void)
{
// this is a very basic implementation of CoCreateInstance()
// it creates an instance of the COM object, in this case
// I decided to start with a pointer to IX — IY would have
// done just as well

IUnknown *comm_obj = (IX *)new(CCOM_OBJECT);

cout << "Creating Comm object" << endl;

// update reference count
comm_obj->AddRef();

return(comm_obj);

} // end CoCreateInstance

//////////////////////////////////////////////////////////////

void main(void)
{

// create the main COM object
IUnknown *punknown = CoCreateInstance();

// create two NULL pointers the IX and IY interfaces
IX *pix=NULL;
IY *piy=NULL;

// from the original COM object query for interface IX
punknown->QueryInterface(IID_IX, (void **)&pix);

// try some of the methods of IX
pix->fx();

// release the interface
pix->Release();
// now query for the IY interface
punknown->QueryInterface(IID_IY, (void **)&piy);

// try some of the methods
piy->fy();

// release the interface
piy->Release();

// release the COM object itself
punknown->Release();

} // end main

I have already precompiled the program for you into the executable DEMO5_1.EXE. However, if you want to experiment and compile DEMO5_1.CPP, remember to create a Win32 Console Application because the demo uses main() rather than WinMain() and is, of course, a text-based program.

      Previous Section Next Section
    



    JavaScript EditorAjax Editor     JavaScript Editor