COM objects are basically black boxes that can be used by applications to perform one or more tasks. They are most commonly implemented as a DLL. Like a conventional DLL, COM objects expose methods that your application can call to perform any of the supported tasks. Applications interact with COM objects in somewhat the same way they do with C++ objects. However, there are some distinct differences:
This document contains the following sections.
It is important to understand the distinction between objects and interfaces. In casual usage, an object may sometimes be referred to by the name of its principle interface. However, strictly speaking, the two terms are not interchangeable.
Note If an object exposes an interface, it must support every method in the interface definition. In other words, you can call any method and be confident that it exists. However, the details of how a particular method is implemented may vary from object to object. For example, different objects may use different algorithms to arrive at the final result. There is also no guarantee that a method will be supported in a nontrivial way. Sometimes an object exposes a commonly-used interface, but needs to support only a subset of the methods. You will still be able to call the remaining methods successfully, but they will return E_NOTIMPL. You should refer to the documentation to see how an interface is implemented by any particular object.
The COM standard requires that an interface definition must not change once it has been published. You cannot, for example, add a new method to an existing interface. You must instead create a new interface. While there are no restrictions on what methods must be in that interface, a common practice is to have the next-generation interface include all the of the old interface's methods, plus any new methods.
It is not unusual for an interface to have several generations. Typically, all generations perform essentially the same overall task, but they will be different in detail. Often, an object will expose every generation of interface. Doing so allows older applications to continue using the object's older interfaces, while newer applications can take advantage of the features of the newer interfaces. Typically, a family of interfaces will all have the same name, plus an integer indicating the generation. For example, if the original interface were named IMyInterface, the next two generations would be called IMyInterface2 and IMyInterface3. DirectX typically labels successive generations of interfaces with the DirectX version number.
GUID are a key part of the COM programming model. At its most basic, a GUID is a 128-bit structure. However,GUIDs are created in such as way as to guarantee that no twoGUIDs are the same. COM usesGUIDs extensively for two primary purposes:
Note For convenience, documentation normally refers to objects and interfaces by a descriptive name such as IDirect3D9. In the context of the documentation, there is rarely any danger of confusion. However, strictly speaking, there is no guarantee that another object or interface does not have the same descriptive name. The only unambiguous way to refer to a particular object or interface is by its GUID.
AlthoughGUIDs are structures, they are often expressed as an equivalent string. The general format of the string form of a GUID is 32 hexadecimal digits in the format 8-4-4-4-12. That is, "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}," where each x corresponds to a hexadecimal digit. For example, the string form of the IID for the IDirect3D9 interface is:
{1DD9E8DA-1C77-4D40-B0CF-98FEFDFF9512}
Because the actual GUID is somewhat clumsy to use and easy to mistype, an equivalent name is usually provided as well. You can use this name instead of the actual structure when you call functions such as CoCreateInstance. The customary naming convention is to prepend either IID_ or CLSID_ to the descriptive name of the interface or object, respectively. For example, the name of the IDirect3D9 interface's IID is IID_IDirect3D9.
All COM methods return a 32-bit integer called an HRESULT. With most methods, the HRESULT is essentially a structure that contains two primary pieces of information:
Some methods return HRESULT values only from the standard set defined in Winerror.h. However, methods are free to return custom HRESULT values with more specialized information. These values are normally documented on the method's reference page.
Note The list of HRESULT values that you find on a method's reference page is often only a subset of the possible values that may be returned. The list typically covers only those values that are specific to the method and those standard values that have some method-specific meaning. You should assume that a method may return a variety of standard HRESULT values, even if they are not explicitly documented.
While HRESULT values are often used to return error information, you should not think of them as error codes. The fact that the bit that indicates success or failure is stored separately from the bits that contain the detailed information allows HRESULT values to have any number of success and failure codes. By convention, success codes are given names with an S_ prefix, and failure codes with an E_ prefix. For example, the two most commonly used codes are S_OK and E_FAIL, which indicate simple success or failure, respectively.
The fact that COM methods may return a variety of success or failure codes means that you have to be careful how you test the HRESULT value. For example, consider a hypothetical method with documented return values of S_OK if successful and E_FAIL if not. However, remember that the method may also return other failure or success codes. The following code fragment illustrates the danger of using a simple test, where hr contains the HRESULT value returned by the method:
if(hr == E_FAIL) { //Handle the failure } else { //Handle the success }
As long as the method returns only E_FAIL to indicate failure, this test will work properly. However, the method might also return an error value such as E_NOTIMPL or E_INVALIDARG. That value would be interpreted as a success, perhaps causing your application to fail.
If you need detailed information about the outcome of the method call, you will need to test each relevant HRESULT value. However, you may be interested only in whether the method succeeded or failed. A robust way to test whether an HRESULT value indicates success or failure is to pass the value to the one of the following macros, defined in Winerror.h:
You can fix the preceding code fragment by using the FAILED macro.
if(FAILED(hr)) { //Handle the failure } else { //Handle the success }
This code fragment properly treats E_NOTIMPL and E_INVALIDARG as failures.
Although most COM methods return structured HRESULT values, a small number use the HRESULT to return a simple integer. Implicitly, these methods are always successful. If you pass an HRESULT of this sort to the SUCCEEDED macro, the macro will always return TRUE. A commonly used example is the IUnknown::Release method. This method decrements an object's reference count by one and returns the current reference count. See Managing a COM Object's Lifetime for a discussion of reference counting.
If you view a few COM method reference pages, you will probably run across something like the following:
HRESULT CreateDevice(..., IDirect3DDevice9 **ppReturnedDeviceInterface);
While a normal pointer is quite familiar to any C/C++ developer, COM often uses an additional level of indirection. This second level of indirection is indicated by a "**" following the type declaration, and the variable name typically has a "pp" prefix. For the example given above, the ppReturnedDeviceInterface parameter is typically referred to as the address of a pointer to an IDirect3DDevice9 interface.
Unlike C++, you do not access a COM object's methods directly. Instead, you must obtain a pointer to an interface that exposes the method. To invoke the method, you use essentially the same syntax that you would to invoke a pointer to a C++ method. For example, to invoke the IMyInterface::DoSomething method, you would use the following syntax.
IMyInterface *pMyIface; ... pMyIface->DoSomething(...);
The need for a second level of indirection comes from the fact that you do not create interface pointers directly. You must call one of a variety of methods, such as the CreateDevice method shown above. To use such a method to obtain an interface pointer, you declare a variable as a pointer to the desired interface then pass the address of that variable to the method. In other words, you pass the address of a pointer to the method. When the method returns, the variable will point to the requested interface, and you can use that pointer to call any of the interface's methods. For more information about how to use interface pointers, see Using COM Interfaces.