A COM object encapsulates data and exposes public methods that applications can use to instruct the object to perform various tasks. COM objects are similar to the objects used by other OOP models, especially C++. However, COM differs in some important ways from conventional C++ programming.
Language independence
COM is not based on a particular programming language; it's a binary standard. Technically, COM objects can be implemented with any of several languages, including C. As a practical matter, C++ provides the most straightforward way to implement COM objects and is the language that is normally used for UMDF drivers.
Object implementation and class relationships
COM objects are normally implemented as C++ classes, but the object's class structure is invisible to the object's clients. As a practical matter, to use a COM object, you do not need to know anything about the object's underlying implementation. All you need to know is what interfaces the object exposes and how to use those interfaces.
The relationship between the object as it is exposed publicly and the underlying C++ class or classes is not necessarily simple. As the developer, you determine the internal implementation details. However, a good COM design principle is to have a class represent a logical entity.
Object lifetime
COM clients create COM objects and manage their lifetimes with COM-specific techniques, not with the C++ new and delete operators.
Inheritance
COM does not support conventional OOP inheritance. Inheritance is commonly used to implement COM objects, but that implementation detail is not externally exposed.
Encapsulation
COM enforces stricter encapsulation on its objects than is the case for regular C++ objects. COM clients do not use "raw" object pointers to access COM objects. You cannot simply instantiate a COM object and call any public method on the object. Instead, COM groups its public methods into interfaces. You must obtain a pointer to an interface before you can use any of its methods. Sometimes, you do not know or need to know which object exposes a particular interface.
Interfaces and objects
All COM interfaces derive from IUnknown, the core COM interface. This interface is exposed by every COM object and is essential to the object's operation.
An interface pointer allows a COM client to use any of the methods on the interface. However, it does not provide access to the methods on any other interfaces that the object might expose. You must use the IUnknown::QueryInterface method to obtain pointers to the other interfaces.
Data members
COM objects cannot expose public data members, but instead expose data through methods called accessors. Accessors are just methods, but they are usually distinguished from task-oriented methods by a naming convention. Usually, there are separate read and write accessors.
Figure 18-2 shows the relationship between a typical UMDF COM object-a UMDF device callback object-and its contents. The remainder of this section discusses the basic components of a COM object and how they work.
Figure 18-2: A typical COM object
A typical COM object exposes at least two and sometimes many interfaces. An interface is the COM mechanism for exposing public methods on an object. With conventional OOP languages such as C++, clients use raw object pointers that provide access to every public method and data member that the object supports. With COM, objects group methods into interfaces and then expose the interfaces. A client must first obtain a pointer to the appropriate interface before it can use one of the methods. COM objects never directly expose data members.
An interface is effectively a declaration that specifies a group of methods, their syntax, and their general functionality. Any COM object that requires the functionality of an interface can implement the methods and expose the interface. Some interfaces are highly specialized and exposed only by a single object, whereas others are exposed by many different types of object. For example, every COM object must expose IUnknown, which is used to manage the object.
By convention, interface names begin with a capital letter I. All interfaces exposed by UMDF objects have names that begin with "IWDF". The conventional way to refer to a method in documentation is borrowed from C++ declaration syntax. For example, the CreateRequest method on the IWDFDevice interface is referred to as IWDFDevice::CreateRequest. However, the interface name is often omitted if confusion is unlikely.
COM has no standard naming conventions for objects. COM programming focuses on interfaces, not on the underlying objects, so object names are usually of secondary importance.
If an object exposes a standard interface, the object must implement every method in that interface. However, the details of how those methods are implemented can vary from object to object. For example, different objects can use different algorithms to implement a particular method. The only strict requirement is that the method has the correct syntax.
After the publication of a public interface-which includes all of the UMDF interfaces-the declaration should not change. An interface is essentially a contract between an object and any client that might use it. If the interface declaration changes, a client that attempts to use the interface based on its earlier declaration will fail.
IUnknown is the key COM interface. Every COM object must expose this interface, and every interface implementation must inherit from it. IUnknown serves two essential purposes: it provides access to the object's interfaces and it manages the reference count that controls the object's lifetime. These tasks are handled by three methods:
QueryInterface is called to request a pointer to one of the object's interfaces.
AddRef is called to increment the object's reference count when a new interface pointer is created.
Release is called to decrement the object's reference count when an interface pointer is released.
An IUnknown pointer is often used as the functional equivalent of an object pointer because every object exposes IUnknown, which provides access to the object's other interfaces. However, an IUnknown pointer is not necessarily identical to the object pointer because an IUnknown pointer might be at an offset from the object's base address.
Unlike C++ objects, a COM object's lifetime is not directly managed by its clients. Instead, a COM object maintains a reference count:
A newly created object has one active interface and a reference count of one.
Each time a client requests another interface on the object, the reference count is incremented.
When a client is finished with the interface, it releases the interface pointer, which decrements the reference count.
Clients typically increment or decrement the reference count when, for example, they make or destroy a copy of an interface pointer.
When all the interface pointers on the object have been released, the reference count reaches zero and the object destroys itself.
The next section provides some guidelines for using AddRef and Release to properly manage an object's reference count.
Important | Be extremely careful about handling reference counts when you use or implement COM objects. Although clients do not explicitly destroy COM objects, COM has no garbage collection to handle unused or out of scope objects as is the case with managed code. A common mistake is to fail to release an interface. In that case, the reference count never goes to zero and the object remains in memory indefinitely. Conversely, if you release an interface pointer too many times, you destroy the object prematurely, which can cause the driver to crash. Even worse, bugs caused by mismanaged reference counts can be very difficult to locate. |
Follow these general guidelines for using AddRef and Release to correctly manage an object's lifetime.
COM clients usually are not required to explicitly call AddRef. Instead, the method that returns the interface pointer increments the reference count. The main exception to this rule is when you copy an interface pointer. In that case, call AddRef to explicitly increment the object's reference count. When you are finished with the copy of the pointer, call Release.
If an interface pointer is passed as a parameter to a method, the correct approach depends on the type of parameter and whether the UMDF driver is using or implementing the method:
IN parameter
Using: If a caller passes an interface pointer as an IN parameter, the caller is responsible for releasing the pointer.
Implementing: If a method receives an interface pointer as an IN parameter, the method should not release the pointer. The pointer is not released until after the method has returned, so there's no risk of the reference count going to zero while the method is using the interface.
OUT parameter
Using: The caller usually passes an interface pointer that is set to NULL, which points to a valid interface when the method returns. The caller must release that pointer when it is finished.
Implementing: If a method receives an interface pointer as an OUT parameter, it is usually set to NULL. The method must assign a valid interface pointer to the parameter. The reference count for the interface must be incremented by one.
IN/OUT parameter
UMDF does not use IN/OUT parameters for interface pointers.
When a COM client makes a copy of an interface pointer, the client usually calls AddRef and then calls Release when it is finished with the pointer. Do not skip these calls unless you are absolutely certain that the lifetime of the original pointer will exceed that of the copy. For example:
If MethodA uses a local variable to create a copy of an interface pointer that was passed to it, calling AddRef or Release is unnecessary. The copy of the pointer goes out of scope when the function returns, before the caller can possibly release the original pointer.
If MethodA uses a data member or global variable to create a copy of an interface pointer that was passed to it, it must call AddRef and then Release when it is finished with the pointer. In a multithreaded application, it is often impossible to know whether other methods might also access the same global variable. If MethodA does not call AddRef, another method could unexpectedly decrement the reference count to zero, destroying the object and invalidating MethodA's copy of the pointer. However, as long as MethodA calls AddRef when it copies the pointer, the object cannot be destroyed until MethodA calls Release.
When in doubt, call AddRef when you copy an interface pointer and call Release when you are finished with it. Doing so might have a minor impact on performance, but it's safer.
If you discover that the driver has reference counting problems, do not attempt to fix them by simply adding calls to AddRef or Release. Make sure that the driver is acquiring and releasing references according to the rules. Otherwise, you might find, for example, that the Release call that you added to solve a memory leak occasionally deletes the object prematurely and causes a crash.
Globally unique identifiers (GUIDs) are used widely in Windows software. COM uses GUIDs for two primary purposes:
Interface ID (IID)
An IID is a GUID that uniquely identifies a particular COM interface. An interface always has the same IID, regardless of which object exposes it.
Class ID (CLSID)
A CLSID is a GUID that uniquely identifies a particular COM object. CLSIDs are required for COM objects that are created by a class factory, but optional for objects that are created in other ways. With UMDF, only the driver callback object has a class factory or a CLSID.
To simplify using GUIDs, an associated header file usually defines friendly names that conventionally have a prefix of either IID_ or CLSID_ followed by the descriptive name. For example, the friendly name for the GUID that is associated with IDriverEntry is IID_IDriverEntry. For convenience, the UMDF documentation usually refers to interfaces by the name used in their implementation, such as IDriverEntry, rather than the IID.
All access to COM objects is through a virtual function table-commonly called a VTable-that defines the physical memory structure of the interface. The VTable is an array of pointers to the implementation of each of the methods that the interface exposes.
When a client gets a pointer to an interface, the interface is actually a pointer to the VTable pointer, which in turn points to the table of method pointers. For example, Figure 18-3 shows the memory structure of the VTable for IWDFIoRequest.
Figure 18-3: VTable
The VTable is exactly the memory structure that many C++ compilers create for a pure abstract base class. This is one of the main reasons that COM objects are normally implemented in C++, with interfaces declared as pure abstract base classes. You can then use C++ inheritance to implement the interface in your objects, and the compiler automatically creates the VTable for you.
Tip | The relationship between pure abstract base classes and the VTable layout in Figure 18-3 is not intrinsic to C++, rather it's a compiler implementation detail. However, Microsoft C++ compilers always produce the correct VTable layout. |
COM methods often return a 32-bit type called an HRESULT. It's similar to the NTSTATUS type that kernel-mode driver routines use as a return value and is used in much the same way. Figure 18-4 shows the layout of an HRESULT.
Figure 18-4: HRESULT layout
The type has three fields:
Severity, which is essentially a Boolean value that indicates success or failure.
Facility, which can usually be ignored.
Return code, which provides a more detailed description of the results.
As with NTSTATUS values, it's rarely necessary to parse the HRESULT and examine the fields. Standard HRESULT values are defined in header files and described on method reference pages. By convention, success codes are assigned names that begin with "S_" and failure codes with "E_". For example, S_OK is the standard HRESULT value for simple success.
Important | Although NTSTATUS and HRESULT are similar, they are not interchangeable. Occasionally information in the form of an NTSTATUS value must be returned as an HRESULT. In that case, use the HRESULT_FROM_NT macro to convert the NTSTATUS value into an equivalent HRESULT. However, do not use this macro for an NTSTATUS value of STATUS_SUCCESS. Instead, return the S_OK HRESULT value. If you need to return a Windows error value, you can convert it to an HRESULT with the HRESULT_FROM_WIN32 macro. |
It's important not to think of HRESULTs as error values. Methods often have multiple return values for success and for failure. S_OK is the usual return value for success, but methods sometimes return other success codes, such as S_FALSE. The Severity value is all that is needed to determine whether the method simply succeeded or failed.
Rather than parse the HRESULT to get the Severity value, COM provides two macros that work much like the NT_SUCCESS macro that is used to check NTSTATUS values for success or failure. For an HRESULT return value of hr:
FAILED(hr) returns TRUE if the Severity code indicates failure and FALSE if it indicates success.
SUCCEEDED(hr) returns FALSE if the Severity code indicates failure and TRUE if it indicates success.
You can examine the HRESULT's return code to determine whether a failure is actionable. Usually, you just compare the returned HRESULT to the list of possible return values on the method's reference page. However, be aware that those lists are often incomplete. They typically have only those HRESULTs that are specific to the method or standard HRESULTs that have some method-specific meaning. The method might also return other HRESULTs.
Always test for simple success or failure with the SUCCEEDED or FAILED macros, whether or not you test for specific HRESULT values. Otherwise, for example, if you test for success by comparing the HRESULT to S_OK and the method unexpectedly returns S_FALSE, your code will probably fail.
COM objects expose only methods; they do not expose properties or events. However, other mechanisms serve much the same purpose.
Properties Many OOP models use properties to expose an object's data members in a controlled and syntactically simple way. COM objects provide the functional equivalent of properties by exposing data members through accessor methods. When the data is read/write, typically one accessor reads the data value and another accessor writes the data value. Because accessors are just a particular type of method, they are usually distinguished from task-oriented methods by a naming convention.
UMDF uses a Get/Set or Retrieve/Assign prefix for its read and write accessors, respectively. The distinction is that Get/Set accessors never fail and do not return errors. Assign/Retrieve accessors can fail and do return errors. For example, IWDFDevice::GetPnPState is a read accessor that retrieves the state of a specified Plug and Play property.
Events Many OOP models use events to notify objects that something interesting has occurred. Before one COM object can notify another COM object of an event, a communication channel must be explicitly set up between the two objects. This is typically done by having the object that is the event sink pass an interface pointer to the object that is the event source. The event source can then call methods on that interface to notify the event sink that an event has occurred and optionally pass some related data to the event sink.
UMDF makes extensive use of this event mechanism. For example, when a driver creates a queue to handle device I/O control requests, it creates a callback object that exposes the IQueueCallbackDeviceIoControl callback interface. The callback object then passes a pointer to its IQueueCallbackDeviceIoControl interface to the framework device object. When the UMDF runtime receives an device I/O control request, the framework device object calls IQueueCallbackDeviceIoControl::OnDeviceIoControl to notify the driver so that it can handle the request.
ATL is a set of template-based C++ classes that are often used to simplify the creation of COM objects. UMDF developers can use the ATL to simplify some aspects of driver implementation. However, ATL is not required for UMDF drivers and is not used in this book.
Info See the Microsoft Press book Inside ATL and the ATL documentation in the MSDN library for more information about ATL-online at http://go.microsoft.com/fwlink/?LinkId=79772.
Interface definition language (IDL) files are a structured way to define interfaces and objects. IDL files are passed through an IDL compiler-the one from Microsoft is called MIDL-that produces, among other things, the header file that is used to compile the project. MIDL also produces components such as type libraries, which UMDF does not use. IDL files are not strictly necessary-a header file with the appropriate declarations is sufficient-but they provide a structured way to define interfaces and related data types.
The UMDF IDL file, Wudfddi.idl, contains entries for the interfaces that the runtime can expose or that UMDF drivers can implement. Wudfddi.idl contains type definitions for structures and enumerations that UMDF methods use and declarations for all of the UMDF interfaces. UMDF drivers typically do not need their own IDLs because they do not require type libraries and because the callback interfaces that they must implement are already declared in the UMDF IDL files.
Inside Out | Wudfddi.idl is located in %wdk%\BuildNumber\inc\wdf\umdf\VersionNumber on your computer. |
It is often more convenient to get information about an object or interface from the IDL file rather than examine the corresponding header file. For an example, see Listing 18-1.
Listing 18-1: Declaration for IWDFObject from Wudfddi.idl
[ object, uuid(), helpstring("IWDFObject Interface"), local, restricted, pointer_default(unique) ] interface IWDFObject : IUnknown { HRESULT DeleteWdfObject( void ); HRESULT AssignContext( [in, unique, annotation("__in_opt")] IObjectCleanup * pCleanupCallback, [in, unique, annotation("__in_opt")] void * pContext ); HRESULT RetrieveContext( [out, annotation("__out")] void ** ppvContext ); void AcquireLock( void ); void ReleaseLock( void ); };
The interface header at the top of the example contains several attributes, such as:
The [object] attribute, which identifies the interface as a COM interface.
The [uuid] attribute, which designates the interface's IID.
The interface body is similar to the equivalent declaration in a C++ header file and includes the following:
The interfaces from which this interface inherits.
A list of the methods that make up the interface.
The data types of each method's return values and parameters.
An annotation for each parameter that provides various information.
One example is the direction of the parameter: _in, _out, or _inout.
Another example is whether the parameter is required or can instead be set to NULL. In the latter case, _opt is added to the directional annotation, such as _in_opt.
Tip See "The Interface Definition Language (IDL) File" on MSDN for more about how to interpret IDL files-online at http://go.microsoft.com/fwlink/?LinkId=80074.