Microsoft DirectX 8.1 (C++) |
Component Object Model (COM) objects are reusable software components that adhere to the COM specification. Adherence to this specification ensures that COM objects work well together and can be easily incorporated into applications. From a practical standpoint, a COM object is basically a black box that your application can use to perform one or more tasks.
COM objects are most commonly implemented as dynamic link libraries (DLLs). Like a conventional DLL, COM objects expose methods that your application can call to perform any of the supported tasks. Your application interacts with a COM object in somewhat the same way it would with a C++ object. However, there are some distinct differences.
Contents
It is important to understand the distinction between objects and interfaces. In casual usage, an object is sometimes referred to by the name of its principal 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 can vary from object to object. For example, different objects might use different algorithms to arrive at the final result. There is also no guarantee that a method will be supported in a non-trivial way. Sometimes an object exposes a commonly used interface but needs to support only a subset of the methods. You should still be able to call the remaining methods successfully, but they will return E_NOTIMPL. To see how an interface is implemented by a particular object, refer to the documentation.
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 an interface, a common practice is to have the next generation interface include all 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 differ in detail. Often, an object exposes 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 have the same name, plus an integer indicating the generation. For example, if the original interface is named IMyInterface, the next two generations are called IMyInterface2 and IMyInterface3. DirectX typically labels successive generations of interfaces with the DirectX version number.
Globally unique identifiers (GUIDs) 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 two GUIDs are the same. COM uses GUIDS extensively for two primary purposes:
Note For convenience, documentation normally refers to objects and interfaces by a descriptive name such as IDirect3D8. 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.
Although GUIDs are structures, they are often expressed as an equivalent string. The general format of the string form of a GUID is "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}", where x corresponds to a hexadecimal integer. For example, the string form of the IID for the IDirect3D8 interface is:
{1DD9E8DA-1C77-4D40-B0CF-98FEFDFF9512}
Because the actual GUID is long 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 CLSID_ or IID_ to the descriptive name of the object or interface, respectively. For example, the name of the IDirect3D8 interface's IID is IID_IDirect3D8.
All COM methods return a 32-bit value called an HRESULT. With most methods, the HRESULT is essentially a structure that contains two separate 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 might be returned. Typically, the list 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 might return a variety of standard HRESULT values, even if the values are not explicitly documented.
While HRESULT values are often used to return error information, you should not think of them as error codes. Because the bit that indicates success or failure is stored separately from the bits that contain the detailed information, HRESULT values can have any number of success and failure codes. By convention, success codes are given names with an S_ prefix and failure codes names 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 can 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. Remember that the method might also return other success or failure codes. The following code fragment illustrates the danger of using a simple test. The hr value is the HRESULT that was 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 on the outcome of the method call, you will need to test each relevant HRESULT value. However, you might 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 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 SUCCESS 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 the Object's Lifetime for a discussion of reference counting.
If you look at a few COM method reference pages, you will probably run across something like the following:
HRESULT CreateDevice( . . . IDirect3DDevice8 **ppReturnedDeviceInterface );
While a normal pointer is familiar to any C or C++ developer, COM often uses an additional level of indirection. This second level of indirection is indicated by a double asterisk (**) following the type declaration and the variable name typically has a "pp" prefix. In the previous example, the ppReturnedDeviceInterface parameter is typically referred to as the address of a pointer to an IDirect3DDevice8 interface.
Unlike in 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 previously. To use such a method to obtain an interface pointer, you declare a variable as a pointer to the desired interface, and pass the address of that variable to the method. In other words, you pass the method the address of a pointer. 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.