Developing Drivers with the Windows Driver Foundation (Pro Developer)

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.

The Contents of a COM Object

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

Objects and Interfaces

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

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:

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.

Reference Counting

Unlike C++ objects, a COM object's lifetime is not directly managed by its clients. Instead, a COM object maintains a reference count:

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.

Guidelines for AddRef and Release

Follow these general guidelines for using AddRef and Release to correctly manage an object's lifetime.

Reference Count

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.

Interface Pointers as Parameters

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:

Release Calls

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:

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.

Fixing Reference Count Bugs

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.

GUIDs

Globally unique identifiers (GUIDs) are used widely in Windows software. COM uses GUIDs for two primary purposes:

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.

VTables

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.

HRESULT

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:

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:

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.

Properties and Events

COM objects expose only methods; they do not expose properties or events. However, other mechanisms serve much the same purpose.

Active Template Library

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 Files

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 interface body is similar to the equivalent declaration in a C++ header file and includes the following:

 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.

Категории