Traditional operating systems only dealt with application binaries and not components. Hence, the benefits of good component-oriented design have till now never gone beyond the compilation step. In an object-centric world is’nt it still a puzzle why operating systems can’t recoganize objects? For a long time now, operating systems have been dealing only with application binaries (EXEs). Objects in one process could not communicate with objects in another process using their own defined methods. The operating system defined certain mechanisms of inter-process communication like DDE, TCP/IP, Sockets, memory-mapped I/O or named pipes etc. These objects needed to use these OS defined mechanisms to communicate with each other.
Components developed using Microsoft’s COM provide a way by which two objects in different object spaces or networks, could talk together by calling each other’s methods. This excellent technology forces the operating system to see applications as objects.
COM forces the OS to act as a central registry for objects. The OS takes the responsibility of creating objects when they are required, deleteing them when they are not, and handling communications between them, be it in the same or different processes or machines. One major advantage of this mechanism is versioning. If the COM object ever changes to a new version, the applications that use that object need not be recompiled.
The wonderful thing about COM components is that they are never linked to any application. The only thing that an application may know about a COM object is what functions it may or may not support. In fact, the object model is so flexible that applications can query the COM object at run-time as to what functionality it provides.
Garbage Collection is one other major advantage to using COM. When there are no outstanding references (a.k.a. pointers) to an object, the COM object destroys itself.
COM supports Marshalling. Distributed Computing purists will attest to the fact that marshalling is the process of packaging and transmitting data between different address spaces, automatically resolving pointer problems, preserving the data’s original form and integrity. Even though COM objects reside in separate processes or address spaces or even different machines, the operating system takes care of marshalling the call and calling objects running in a different application (or address space) on a different machine.
Over and above all this, COM is a binary standard. A fully compliant COM object can be written in any language that can produce binary compatible code. So you can write them using C, C++, Java, J++ or Visual Basic. All of the Windows NT shell has been written using COM.
What is COM?
COM stands for Component Object Model, and is a binary standard for getting pieces of code (components) to interact with each other, it’s layered on top of DCE RPC. Since COM is a binary standard, it’s language-independent, and we can write COM objects and clients in C, C++, Java, Visual Basic, and anything else that supports COM. Essentially it’s a form of object-oriented programming, since one creates objects, then invokes methods on them. It’s built it into Windows (which is where most people do COM programming), but will soon be available on other platforms (Software AG has already released a port of DCOM to Solaris and Linux, with (many) more on the way).
COM = OLE = ActiveX. (more or less)
COM (Component Object Model), OLE ( formerly Object Linking and Embedding, now it’s no longer an acronym), and ActiveX are the same thing. The only substantial difference that I’m aware of is that ActiveX objects are intended to be transfered across the networks (ie, the Internet), and so are optimized to be small in size.OLE is what grew out of inter-application data transfer (ie, how to get an Excel spreadsheet into a Word document), and tends to deal with those issues. COM is the architecture that underlies both ActiveX and OLE.
COM transparantly handles method invocation
Note that one uses COM to handle the details of getting method calls from clients to servers. Because COM handles these details, it’s possible for the method calls (messages) to be in the same address space (in process, or “in-proc”), different address spaces on the same computer (“local”), or different address spaces on different computers (“remote”)(this last case is handled by Distributed COM, or DCOM). The nice thing is that COM does this (pretty) transparantly, so that we could create an object with intention of running it as a local object, but move it to a remote computer later without having to change the code. This might not be optimal, but it’ll work.
Since COM was originally intended to be used in C++, we don’t have garbage collection (yet — COM+ is a new runtime component that may offer garbage collection), but we still need a way to keep track of when objects should be deallocated. Ideally, we’d like the objects to delete themselves, because multiple clients can use the same object, simultaneously, without knowing that the others are using it. If one client were to delete the object while the other clients were still trying to use the object, there would be massive problems. We solve this problem with reference counting — the object itself keeps a count of the number of clients using it (who are refering to it, thus the name), and when that count is decremented to 0, it deletes itself. Each time a client starts using an object, it increments a private, internal reference count within the object (by calling the public AddRef method on that object). When the client is finished using on object, it decrements the private, internal count on the object ( by calling the public Release method on that object). When the reference count is decremented to 0, the object calls delete on itself, and is removed from memory. It’s a pretty snazzy way to solve the problem of trying to figure out when to delete the object, but can be tough to debug. Also, once the object has executed the delete this statement, the object should be careful to not access any member variables.
Once you look at the code, you’ll note that all the methods have the __stdcall declaration. The __stdcall declaration specifies how arguments are pushed onto the stack when a method is invoked. Since you want your COM object to interoperate with other COM objects, you want to make sure that both your object and other COM objects use the same calling convention. Declaring them to use the __stdcall calling convention insures that your components call functions in the way that other components expect you call them. If you’re components won’t interact with anything else, then this doesn’t matter (this is why everything will work fine if you compile a component and a client, and forget to specify __stdcall with both of them).
We’ll ask COM to create a certain object for us, and then manipulate the object by invoking the object’s methods. Related methods are grouped together to form an “interface’, which serves to allow clients access to the object’s functionality without exposing implementation details. Clients are able to ask an object whch interfaces it offers for use by calling the QueryInterface method that every object has. Once an interface is released to the public, it should NEVER be changed, becuase it’s impossible to predict who is depending on the interface to be the way it is. And if the interface changes, some existing code somewhere could be broken. If you wanted a previously released interface to behave differently in the next version of your program you’d have to create a new interface, though the new interface could simply be an extension of the old one, with new methods tacked on to do whatever it is you left out of the first version.
Interfaces are not objects! It’s very easy to get these two concepts confused, especially when you’re first starting at this stuff. Remember to keep the concept of interface ( which is essentially a specification of how one object interacts with another) separate from the concept of object (the code that actually implements the interfaces)
Next, we’re going to briefly look at some specific interfaces that every object will have to worry about:: IUnknown and IClassFactory. First, IUnknown. Every object must support IUnknown, as it provides the interface for reference counting (AddRef and Release methods), as well as a mechanism for asking an object if it supports an arbitrary interface. The interface looks like the following:
HRESULT QueryInterface( REFIID iid, //Identifier of the requested interface
void ** ppvObject //Indirect pointer to the object );
Once you’ve written the code for your object, you’ll need something to control the creation of it. While this might sound redundant, it’s actually very useful. If you want to create a single object that will service all requests, you can have the class factory create only a single instance of the object. If you want to do load-balancing among objects, the class factory may be the place to pick which object to use. Also, since COM is language independent, this allows one to create an object without knowing anything about the specifics of how a particular language wants to allocate objects (for example, when one creates COM objects in C, the class factory has to fill in the vtable manually, fake a constructor by initializing variables, etc). Note that class factories are in 1-to-1 correspondence with COMponents: one (and only one) class factory creates a certain type of COM object. The actual IClassFactory interface looks like:
HRESULT QueryInterface( REFIID iid, void ** ppvObject );
HRESULT CreateInstance( IUnknown * pUnkOuter, REFIID riid, void ** ppvObject );
HRESULT LockServer( BOOL fLock );
The first three methods are IUnknown, while the last two are what actually do work. CreateInstance asks the classfactory to create a new instance of the object that the class factory is responsible for creating. pUnkOuter is used when aggregating objects (the COM equivalent of inheritance, which will be covered elsewhere). For simple stuff, this should be set to NULL. riid is a reference to an IID (a reference variable in C++, a pointer in C), which is the Interface that is requested on the created object. ppvObject is a pointer to a pointer that we’ll fill in with the interface if the call to CreateInstance is successful, and that we’ll zero out if it fails. Thus, we could create an instance of CMike, and ask to have the class factory return the interface for IFoo on the object with the following line:
CreateInstance( NULL, IFoo, ppvObj );
Class factories are unusual in that they can be unloaded from memory even when it’s reference count is nonzero. I don’t know why, but that’s how it is. If we want to make sure that the server isn’t unloaded until we want (eg, if we know that we’ll be creating a number of objects), we call the LockServer method. Calling LockServer( TRUE ) indicates that the object shouldn’t be unloaded until a corresponding LockServer(FALSE) is called. Since these calls can be nested, this generally means that a reference count is kept, with LockServer(TRUE) incrementing the count and LockServer(FALSE) decrementing it.