COM objects are much like instantiated C++ classes. In fact, COM was designed with C++ programmers in mind. It supports encapsulation, polymorphism, and reusability.
However, COM was also designed to be compatible at the binary level and therefore has differences from a C++ object. As a programmer, you are aware that compiled programming languages such as C, C++ etc are machine-dependent. As a binary object, a COM object concerns itself with how it interfaces with other objects. When not used in the environment of its creator, an interface is exposed that can be seen in the non-native environment. It can be seen because it is a binary object and therefore not machine-dependent. This does not require the host environment or an interacting object to know anything about the COM object. When the object is created in the womb of its mother application, COM does not concern itself with how that object interacts within it. This interaction is between the mother application and the child object. When the object interacts with the rest of the world, however, COM is concerned about how to interface with that object.
It is important to note that COM is not a programming language; it is a binary standard that enables software components to interact with each other as objects. COM is not specific to any particular programming language. COM can work with any language that can support the binary layout of a COM object. It is a programming model to facilitate the programmability of this standard.
COM objects consist of two types of items: properties and methods.
- Properties are the data members, and
- Methods are member functions.
COM objects each have a common interface. No matter what they do, COM objects all have to implement the IUnknown interface. This interface is the main interface for all others and is the base class from which all other COM interfaces are derived. The IUnknown interface has the following member functions:
- ULONG AddRef(void)
- ULONG Release(void)
- HRESULT QueryInterface(REFIID id, void **ipv)
Each object implements a vtable. A vtable is nothing more than an array of pointers to member functions implemented in the object, see the following figure:
This vtable is shared between all the instances of the object also maintaining the private data of each object. A client application evokes an instance of the interface and gets a pointer to a pointer that points to the vtable. Each time a new interface to the object is instantiated, the reference count of objects is incremented with AddRef(). Conversely, each time a reference is destroyed, the reference counter is decremented with Release(). Once the reference count is zero, the object can be destroyed. In order to see what interfaces an object supports, you can use QueryInterface().
COM objects are never directly accessed. COM objects are always accessed through a pointer to an interface exposed by the object. The QueryInterface(REFIID riid, void **ipv) function takes a reference to an interface identifier (riid) and a void pointer. The REFIID is a 128-bit unique ID that identifies the interface you are retrieving. Notice the double indirection on the pointer: **ipv. The ipv pointer is where the pointer to the interface you are trying to retrieve is stored. Consider this code fragment:
IAnyInterface* pAny = NULL;
HRESULT hr = pUnknown->QueryInterface(IID_IAnyInterface, (void**)&pAny);
if(SUCCEEDED(hr))
{
pAny->DoAnyObjectWork();
pAny->Release();
}
pUnknown is a pointer to the object's IUnknown interface. DoAnyObjectWork() is the member function you want to use to perform some work. You access that function through the pointer to that object's interface pAny.