This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


September 1996

Microsoft Systems Journal Homepage

Our Exclusive Class Library Speeds Building Windows NT Kernel-Mode Device Drivers

John Elliot and Jeff Huckins

Jeff Huckins is a senior software engineer for Intel Corporation in the Intel Communication Group. He can be reached at Jeffrey_Huckins@ccm.jf.intel.com.

John Elliott is a senior software engineer for Intel Corporation in the Intel Communication Group. He can be reached at John_Darren_Elliott@ccm.jf.intel.com.

Microsoft¨ Windows¨ NT is a modular operating system that defines a number of user-mode objects and kernel-mode objects. These objects lend themselves to having classes written around them because, typically, each object has a set of routines. A pointer to the object is passed to the routine along with other parameters. The MFC class library from Microsoft encapsulates most of the user-mode objects. Likewise, a class can encapsulate a kernel-mode object and its methods into one C++ object. That's what we will do in this article. In some cases you have to supply a variety of routines as callbacks from the operating system. These callback routines can also be encapsulated in the C++ object, as can any additional data and routines associated with a particular kernel-mode object (such as a device extension).

A device driver is composed of three sets of methods and data. The first set encompasses the device driver object itself (the Windows NT DDK DRIVER_OBJECT). The second set encompasses the actual device objects running under the device driver (the DDK's DEVICE_OBJECT). The third set of methods and data is device driver specific. The framework we develop here defines a virtual interface for the third set of methods by encapsulating the first two sets in C++ classes, giving you the basis for writing a kernel-mode device driver for Windows NT 3.51 or 4.0. This framework is a set of simple classes that wraps up the functionality of kernel-mode objects (drivers, devices, interrupts, and so on) and contains much of the boilerplate code needed for most drivers. Although it's not a complete framework, it's a good starting place. You will still need the Windows NT DDK and a C++ compiler (we used Microsoft Visual C++¨ 4.1).

Driver and Device Objects

A Windows NT kernel-mode device driver is represented by a software element, the driver object (DRIVER_OBJECT), and a hardware element, the device object (DEVICE_OBJECT). The driver object provides all of the entry points for the operating system to carry out user and system requests. There can be more than one device object, but there's only one driver object for a given device driver. The device objects store all of the needed state information for a particular piece of real or virtual hardware. The basis of our framework is two classes that encapsulate these objects: CDriverObject and CDeviceObject. Their relationship to each other as well as to the DRIVER_OBJECT and DEVICE_OBJECTs is shown in Figure 1.

Figure 1 CDriverObject and CDeviceObject Classes

You create the CDriverObject statically so there can be only one per driver. The DRIVER_OBJECT is created for you by the Windows NT I/O Manager and passed to the driver's entry point, DriverEntry (discussed later). At this point, the instance of CDriverObject is bound to the DRIVER_OBJECT. Static instantiation is enforced in our class by providing CDriverObject with an operator new member, which returns NULL.

When CDriverObject is instantiated, it fills in the necessary driver entry points for handling the common I/O Request Packets (IRPs), overlapped I/O, and termination. CDriverObject performs all the work required to get a driver loaded, assign the callbacks, and create device objects. CDriverObject is an abstract base class, so you must define the member CreateDevices in your actual driver code to instantiate a derived class. In addition to resolving this pure virtual method, there are a handful of virtual methods in the CDriverObject class that you can override to implement a driver (for example, OnInitialize, OnUnload, OnStartIo, and OnIrpMjXxx).

The DriverEntry routine is called when a driver is loaded. This routine does some initialization of the runtime environment and then calls CDriverObject::DriverEntry, which fills in the bare minimum DRIVER_OBJECT entry point, DriverUnload. This is required by our framework to clean up when the driver is unloaded. That accomplished, CDriverObject::DriverEntry calls CDriverObject::Initialize.Thisiswhere CDeviceObjects are created (via CreateDevices) and initialized (via InitDevices). If successful, the CDriverObject::OnInitialize method is called. At this point you can optionally add support for DEVICE_OBJECTs that are not part of this framework and provide other driver specific initialization. The base class CDriverObject::OnInitalize sets up the most common dispatch table routines and DriverStartIo.

The DriverUnload routine of the DRIVER_OBJECT is called when a driver is unloaded. This invokes CDriverObject::Unload, which lets you clean up by calling CDriverObject::OnUnload, then calls CDriverObject::UnloadDevices. Within CDriverObject::OnUnload, you must clean up any DEVICE_OBJECTs that are not part of this framework. CDriverObject::UnloadDevices iterates through all remaining CDeviceObjects and unloads them before freeing them from memory.

All driver I/O dispatch entry points contained in the DRIVER_OBJECT data structure are by default routed to device objects through the CDriverObject::IrpMjXxx method. The default CDriverObject::OnIrpMjXxx method calls the associated CDeviceObject::OnIrpMjYyy methods, where Yyy is based on the actual IRP's major function code-IRP_MJ_CREATE would invoke CDeviceObject::OnIrpMjCreate. With this model, a CDriverObject object can filter all entry-point calls for its associated devices. Most driver objects will simply set these calls to pass through to the device objects.

The DriverStartIo entry point is routed through CDriverObject::StartIo if it is set up in CDriverObject::OnInitialize. CDriverObject::StartIo then invokes CDriverObject::OnStartIo and, by default, passes the call on to one of three methods: CDeviceObject::OnStartReadIo for a read request, CDeviceObject::OnStartWriteIo for a write request, or CDeviceObject::OnStartIo for any other case.

Using this model, it is possible for the CDriverObject object to act as an entry point call filter for all of the DEVICE_OBJECTS that it creates whether or not they are part of our framework (CDeviceObject objects). Figure 2 shows the implementation of CDriverObject.

The CDeviceObject class encapsulates the common functionality and behavior associated with a DEVICE_OBJECT. As described previously, all device-oriented dispatch entry points contained in DRIVER_OBJECT are by default routed to CDeviceObject::OnIrpMjYyy methods.

There are two ways to associate a CDeviceObject with a DEVICE_OBJECT. The first approach is to call the DDK's IoCreateDevice (requesting no extra space for the DeviceExtension), and then allocate the CDeviceObject from the non-paged pool using the DDK's ExAllocatePool API. Bind the two objects together by setting the DeviceExtension field of the DEVICE_OBJECT to point to the CDeviceObject and setting the m_DeviceObject member of the CDeviceObject to point to the DEVICE_OBJECT.

The second approach is to define an alternate form of CDeviceObject::operator new that makes the call to IoCreateDevice. This allows the I/O Manager to allocate the memory for CDeviceObject via the DeviceExtension field in DEVICE_OBJECT, then bind CDeviceObject to DEVICE_OBJECT. A CDeviceObject::operator delete performs the call to the DDK's IoDeleteDevice. This frees up system resources associated with the DEVICE_OBJECT. We used this approach in our framework.

The CDriverObject class declares a pure virtual method, CreateDevices, which is called from within the Initialize method of CDriverObject. This is the context by which a CDriverObject can instantiate one or more CDeviceObject objects using the CDeviceObject::operator new. The standard operator new for CDeviceObject simply returns NULL to enforce the use of the alternate operator. In the same manner, when a CDriverObject is being unloaded, it frees the resources associated with its CDeviceObject objects by using CDeviceObject::operator delete.

To provide handlers for the various dispatch requests, you need to override the OnIrpMjYyy virtual methods defined in CDeviceObject. The default methods implemented in CDeviceObject, with the exception of OnIrpMjWrite and OnIrpMjRead, simply complete the IRP by calling the DDK's IoCompleteRequest and returning STATUS_SUCCESS. CDeviceObject's implementations of OnIrpMjWrite and OnIrpMjRead mark the IRP pending with a call to the DDK's IoMarkIrpPending, call the CDeviceObject::StartPacket method, and then return STATUS_PENDING. If the device is not busy, either CDeviceObject::OnStartReadIo or CDeviceObject::OnStartWriteIo will be called in CDeviceObject to complete the IRP by calling IoCompleteRequest and returning STATUS_SUCCESS. CDeviceObject also provides the basic support required to cancel IRPs.

CDeviceObject provides default handlers for various callbacks from two more classes in our framework, CInterruptObject and CDpcObject. CInterruptObject will generate, by default, a callback for every interrupt to the CDeviceObject::OnInterrupt method. If a CDeviceObject is connected to some hardware that generates interrupts, it should override this method and provide the appropriate implementation. It could use the default deferred procedure call (DPC) facility provided with the DEVICE_OBJECT; the CDpcObject generates callbacks to the CDeviceObject::OnDpc method for every DPC that it receives. You should override this method and provide the appropriate implementation. The other two possible callbacks come from the CInterruptObject. These callbacks, for synchronizing execution for read or write I/O, are CDeviceObject::OnSychronizeExecutionForRead and CDeviceObject::OnSychronizeExecutionForWrite. They are called when a driver calls the methods CInterruptObject::SychronizeExecutionForRead or CInterruptObject::SychronizeExecutionForWrite and provide exclusion from the interrupt handler for accessing shared data structures or hardware.

The CDeviceObject supports finding and claiming hardware resources via the PciFindDevice, AssignSlotResources, IoAssignResources, and ReportResourcesUsage member functions (see Figure 3). Presently, it finds hardware on a PCI bus only, but it would not be difficult to add support for other busses.

A Typical CDeviceObject

Although only CDriverObject and CDeviceObject are required in this framework, typically several other objects are used in association with a CDeviceObject. These objects include CInterruptObject, CDpcObject, CSpinLock, CCustomDpcObject, CDeviceQueue, CAdapterObject, CRegistryKey, and CUnicodeString (see Figure 4). If a device handles hardware interrupts, it could use CInterruptObject, CDpcObject, and CSpinLock. If a device handles overlapped I/O (simultaneous reads and writes), it could use CCustomDpcObject and CDeviceQueue as well. If a device performs DMA operations, it could use CAdapterObject. Many devices will need to access the registry or perform system calls that require Unicode strings through CRegistryKey and CUnicodeString.

Let's look at CInterruptObject first. CInterruptObject encapsulates and simplifies the handling of a hardware interrupt object (KINTERRUPT). A CInterruptObject is normally contained within a CDeviceObject. As such, its constructor can bind it to a specific CDeviceObject. CInterruptObject assigns the static CInterruptObject::Isr routine as the ISR handler in a call to the DDK's IoConnectInterrupt. When it receives an interrupt callback in its Isr method, CInterruptObject calls the CDeviceObect::OnInterrupt method to handle the interrupt, but you can override the CInterruptObject::OnInterrupt method to do something else.

CInterruptObject's SynchronizeExecutionForRead and SynchronizeExecutionForWrite methods provide synchronized read and write operations to access data or hardware referenced in an interrupt handler. These methods are often called from within CDeviceObject::StartIO. CInterruptObject::SynchronizeExecutionForRead and CInterruptObject::SynchronizeExecutionForWrite simply call the KeSynchronizeExecution routine, passing pointers to CInterruptObject's static methods, SynchronizeRoutineForRead and SynchronizeRoutineForWrite, as the synchronization routines. The SynchronizeRoutineForRead and SynchronizeRoutineForWrite methods then invoke CDeviceObject's SychronizeExecutionForRead and SynchronizeExecutionForWrite, which in turn invoke CDeviceObject::OnSynchronizeExecutionForRead and CDeviceObject::OnSynchronizeExecutionForWrite. CInterruptObject also provides a generic SynchronizeExecution method so you can create your own synchronization callback routine for I/O operations other than read and write or in a context other than CDeviceObject.

CInterruptObject::Initialize must be called at an IRQL less than or equal to PASSIVE_LEVEL. Our class library obtains the interrupt vector, IRQL, and processor affinity for the requested interrupt via a call to the DDK's HalGetInterruptVector. After successfully obtaining the interrupt information, the method will call CInterruptObject::OnInitialize to give you a chance to perform some additional initialization.

CInterruptObject::Connect calls the DDK's IoConnectInterrupt to hook the interrupt and assign the member m_InterruptObject to the KINTERRUPT returned by the call. CInterruptObject::Disconnect must be called no later than during CDeviceObject::OnUnload, resulting in a call to the DDK's IoDisconnectInterrupt routine to unhook the interrupt. It also resets the m_InterruptObject member to NULL. Both of these methods must be called at an IRQL less than or equal to PASSIVE_LEVEL.

CDpcObject encapsulates the functionality of the DpcForIsr that is a part of a DEVICE_OBJECT. A DPC is used to complete the work of an interrupt handler. To use CDpcObject for this purpose, you need to initialize it by calling CDpcObject::InitializeDpcRequest with a pointer to the CDeviceObject to call back from the DPC routine. CDpcObject::InitalizeDpcRequest calls the IoInitializeDpcRequest routine, passing CDpcObject::Dpc as the DPC routine. When the CDeviceObject object that set up the CDpcObject calls CDeviceObject::RequestDpc, eventually the CDpcObject::Dpc method is called at an IRQL of DISPATCH_LEVEL. CDpcObject::Dpc calls the owning CDeviceObject's OnDpc method, passing the IRP and Context arguments passed in to it.

CCustomDpcObject encapsulates the functionality and behavior of a custom DPC. Since a DEVICE_OBJECT can be associated with only one DpcForIsr, it is frequently necessary to create custom DPC objects instead of or in addition to the DpcForIsr. A CDeviceObject need not instantiate a CDpcObject at all. Instead, it may instantiate one or more CCustomDpcObject objects, one for each I/O operation that can overlap another I/O operation. A driver may, for example, support overlapping reads and writes. To initialize a CCustomDpcObject, call CCustomDpcObject::InitializeDpcRequest, passing in a DPC routine and a DeferredContext. The DPC routine conforms to a certain calling convention, so it must be either a C-style function or a static member of a class. The DeferredContext is passed to the DPC routine when it is executed to provide the necessary state to carry out the DPC. You can queue a custom DPC request by calling CCustomDpcObject::InsertQueue with up to two arguments. Again, these arguments are passed to the DPC routine.

There are at least two strategies you can employ in using the CCustomDpcObject. You can create static methods in a class (usually a CDeviceObject-derived class) that serve as callback methods, and pass the class "this" pointer as the DeferredContext. You can also derive a class based on CCustomDpcObject that contains a callback method, and register another object with this class to carry out the actual callbacks (such as a CDeviceObject). Either method provides similar capabilities.

CAdapterObject encapsulates the functionality of an ADAPTER_OBJECT used for allocating common DMA buffers. The kernel-mode drivers that we initially intended this framework for did not use the system DMA controller, so our CAdapterObject is simplified. You could derive a CDmaAdapterObject class from CAdapterObject or enhance CAdapterObject to support use of the system DMA controller.

One of two overloaded CAdapterObject::Initialize methods should be called from CDeviceObject::OnInitialize at initialization time. Which one depends on whether the driver wants to allocate a common buffer at startup or later. In either case, the caller needs to supply a PDEVICE_DESRIPTION parameter. This can be obtained, assuming it has been overridden, by calling CDeviceObject::GetDeviceDescription. The driver will need to allocate and initialize a separate CAdapterObject for each common buffer it needs. The actual allocation of the common buffer supports an arbitrary alignment (at the expense of memory).

CDeviceQueue encapsulates and enhances the functionality provided by a KDEVICE_QUEUE object. CDeviceQueueEntry is a helper class derived from the DDK's _KDEVICE_QUEUE_ENTRY structure. The two objects together give you the ability to add additional device queues beyond the one provided with a DEVICE_OBJECT. This is useful when a driver can support overlapped I/O. CDeviceQueue provides one set of methods that takes CDeviceQueueEntry pointers as a parameter and another set that takes void pointers as a parameter.

To insert a piece of data into a CDeviceQueue, wrap the data up into a CDeviceQueueEntry and then call CDeviceQueue::Insert. You can also call CDeviceQueue::InsertData, which does the wrapping automatically. To remove a piece of data from a CDeviceQueue, call CDeviceQueue::Remove and then pull the data out of the returned CDeviceQueueEntry. CDeviceQueue::RemoveData does the unwrapping automatically. The Insert, InsertData, Remove, and RemoveData methods are overloaded to take a SortKey as well. The one exception is removing a specific entry on the queue; you must call CDeviceQueue::RemoveEntry and pass the CDeviceQueueEntry that should be removed.

CSpinLock encapsulates a KSPIN_LOCK. It protects resources that are shared between DPCs and other portions of a driver (excluding interrupt handlers). A CSpinLock can be acquired by calling either CSpinLock::Acquire or CSpinLock::AcquireAtDpcLevel. The latter is used when acquiring a CSpinLock from within a DPC routine. To release a CSpinLock, call either CSpinLock::Release or CSpinLock::ReleaseFromDpcLevel. Again, the latter is used when releasing a CSpinLock from within a DPC routine. The destructor for a CSpinLock will release it if possible.

CRegistryKey is used to access the Windows NT registry. You can open a registry key by calling CRegistryKey::Create or get information about a registry key by calling CRegistryKey::Query. To read a registry key's value, call the CRegistryKey::QueryValue method or one of the simpler methods, CRegistryKey::QueryValueULONG or CRegistryKey::QueryValueString. Set key values by calling CRegistryKey::SetValue or one of the simpler methods, CRegistryKey::SetValueULONG or CRegistryKey::SetValueString.

CUnicodeString encapsulates the UNICODE_STRING object. Windows NT uses Unicode strings to represent all text within the operating system, so you'll have to use Unicode strings on occasion. The CUnicodeString object lets you either own the memory for the buffer or simply point at a buffer allocated from a pool. To initialize a CUnicodeString from a wide character string, call CUnicodeString::Init. To copy a wide character string or a UNICODE_STRING, call CUnicodeString::Copy or use the CUnicodeString::operator =. Append to a CUnicodeString by calling CUnicodeString::Append or using the CUnicodeString::operator +=. To change the pool a string buffer is allocated from, call CUnicodeString::Convert.

Miscellaneous Objects

There are several additional objects defined in the framework that you can use or that are used internally in the framework itself (see Figure 5). For example, CFile encapsulates a file object and is used to access a file from within a driver. It is simply a wrapper around the ZwYyy file routines. It exports the following methods: Create, Read, Write, Close, QueryInformation, and SetInformation.

The CTimeOut class stores a timeout value passed to it in the constructor. It stores the current system time by calling its member function StartTimer and makes a comparison between the time period that it represents and the elapsed time since calling StartTimer. It does this comparison in response to calling its IsExpired member function.

CObject encapsulates Win32¨ objects that are accessible from a device driver (such as files or registry keys). It provides support to get an object pointer from an object handle by calling its GetObjectPtr member function. It also supports proper cleanup via a Close method and a destructor that dereferences the object pointer (if necessary). A CNamedObject can be referred to by Unicode string, an example of which is CRegistryKey. It adds the additional methods to set the name, SetName, and get the name, GetName. CFileObject is a named object that represents a file. It adds an additional method to create the file: Create.

CLargeInteger encapsulates the LARGE_INTEGER object. Large integers are used to represent 64-bit values and are occasionally required by drivers (PHYSICAL_ADDRESS is a 64-bit value, for example). The CLargeInteger class defines a new type based on LARGE_INTEGER. It defines all of the basic addition and relational operators, and two members are references to the fields within the contained LARGE_INTEGER. This allows you to treat a CLargeInteger exactly as you would a LARGE_INTEGER. You might use this when passing a CLargeInteger to macros that expect a LARGE_INTEGER.

CThread encapsulates a system thread. This is used to perform non-time-critical operations. It can be useful to perform file I/O in kernel mode. To start the thread execution, call Create. When the thread wishes to terminate, it can call the Terminate member function or simply return. The latter is preferable. You can adjust thread's priority by calling the SetBasePriority or SetPriority member functions. To use the CThread class, derive a new class and override the OnStart member function and, optionally, the OnTerminate method. The OnStart method is the thread's entry point. Upon returning from OnStart, OnTerminate is called and the thread is terminated.

CUpperDeviceObject is a CDeviceObject that provides support for device layering (see Figure 6). It allows the device to attach to a lower-level device either by name or by pointer. By attaching to a lower-level device, a device makes itself a filter of I/O requests. Alternately, an upper-level device can create a lower-level device and then issue I/O requests to the lower-level driver by calling the CallDriver member function.

CDlist provides support for manipulating doubly linked lists.

Startup Code

In general, C++ doesn't require any special treatment to operate correctly in the context of a kernel-mode driver. However, C++ global or static objects need to be initialized at startup. Normally, this is done by a variant of the startup code that is shipped with libc.lib, but libc.lib cannot be used in kernel-mode drivers. The C++ compiler generates code to the constructors of these objects, and the startup code calls this code. This startup code is shipped with the Microsoft Visual C++ compilers, so it seems fairly simple to take this code and simply add it to the framework. Well, it almost is. The startup code makes explicit use of some symbols found only in libc.lib. The good new is, these symbols can be found in a single object file, crt0init.obj. To use these symbols, we extracted this module from libc.lib. Global destructors work in a similar manner. The C++ compiler generates atexit calls to register exit callbacks that invoke the destructors, so we had to provide an atexit routine.

You can override the global new operator to use the DDK's ExAllocatePool. Our framework does this. As a global new cannot be defined with a default parameter, all such allocations must specify the pool from which you will allocate. Of course, since we defined a global new, we also had to define a global delete-it simply calls the DDK's ExFreePool.

To facilitate good debugging support we added ASSERT and TRACE macro support. We also added a set of ASSERT_IRQL_XX macros that ensure the current IRQL is within an expected range.

Figure 7 lists the header files used from within the framework. One of these is the master external include file idrivrpp.h. To build the framework library, use the Build utility that ships with the Microsoft Windows NT DDK.

A Sample Driver

Our sample driver demonstrates the implementation of the framework (see Figure 8). Full source code is available via the services listed on page 5. We will describe just the key concepts for successfully implementing a device driver. These key concepts include driver initialization, CDeviceObject object creation, synchronized device access, and driver deinitialization. You can review the remaining code for implementation details such as dispatch routine handling.

The sample driver can not be used as is. It will build with the makefiles provided, but the PCI device ID and vendor ID for device configuration are dummies. The sample project is intended to provide you with a starting point. We wrote the framework. You need to supply your driver-specific code (and perhaps a piece of hardware).

Driver initialization is performed during calls to component OnInitialize members. The first component to get a chance at initialization is the driver's CDriverObject object, CSampleDriver. The sampdrv.cpp file shows the implementation of CSampleDriver. CSampleDriver::OnInitialize is a typical implementation and just calls the base class CDriverObject::OnInitialize method. The real initialization work is performed by purevirtualmember CSampleDriver::CreateDevices. CreateDevices is called from the nonvirtual CDriverObject::Initialize member before calling the virtual OnInitialize member.

The purpose of a CreateDevices implementation is to instantiate all CDeviceObject-type objects required by the driver implementation. The sample driver instantiates and uses a single CDeviceObject, CSampleDevice. However, multifunction adapters (such as a sound card with a game port) would, in most cases, instantiate a CDeviceObject object for each function supported by the adapter. Remember that the memory for the CDeviceObject object is actually the DeviceExtension space of the DEVICE_OBJECT object. Therefore, the pointer returned from using object new on a CDeviceObject object does not need to be cached because the DRIVER_OBJECT object contains a linked list of DEVICE_OBJECT pointers as the DeviceObject field. In most implementations, the DEVICE_OBJECT pointers are accessed only by base class CDriverObject for device deinitialization.

After a successful call to CreateDevices, CDriverObject::Initialize iterates through the DRIVER_OBJECT list of device objects and initializes each via a call to the virtual CDeviceObject:: OnInitialize member. This is where each CDeviceObject object performs all device-specific driver initialization, such as PCI configuration.

In our sample driver, class CPciDevice performs all PCI bus-specific initialization in its OnInitialize member, which is called by CSampleDevice::OnInitialize prior to performing its own initialization. In our example, the bus-specific initialization includes obtaining the device base memory address as well as the interrupt level and vector. The base memory address is mapped to system address space, which you can use to access device registers. The interrupt vector and level are used to initialize the CInterruptObject object and to connect or disconnect the interrupt.

CSampleDevice initializes CAdapterObject objects and their associated common buffers-one CAdapterObject object for reading from the device and one CAdapterObject object for writing to the device. In addition, CSampleDevice initializes two separate custom DPC objects: one to be associated with write I/O interrupts and one to be associated with read I/O interrupts. CSampleDevice provides static methods DpcForWriteIsr and DpcForReadIsr as handlers for the write and read DPCs. Separate DPCs are provided because CSampleDevice supports overlapping read and write I/O operations. If a single DPC was implemented for concurrent read and write operations, we could not guarantee that both would be queued since a second DPC will not be queued if a DPC is already in the queue. Therefore, a request to queue the DPC as a result of a write interrupt would fail if the same DPC was already in the queue as a result of a read operation.

Synchronizing Device Access

To enforce exclusive device access for operations such as writing and reading device registers and sharing data between the device's ISR and other routines, a device must implement a DDK SynchCritSection callback routine. The framework provides this functionality via the CInterruptObject class. CSampleDevice makes use of CInterruptObject SynchronizeExecutionForRead and SynchronizeExecutionForWrite methods. It also provides virtual handlers OnSynchronizeExecutionForRead and OnSynchronizeExecutionForWrite for the associated SynchCritSection routines. Driver dispatch routines and other routines running below the device's interrupt IRQL-that must access the data and state shared with the device's ISR-must synchronize access in this manner. The DDK's KeSynchronizeExecution is invoked when CInterruptObject::SynchronizeExecutionForYyy is called, which results in the current IRQL being raised to the device's interrupt IRQL. This prevents the ISR from being entered until after the IRQL is lowered upon returning from the SynchCritSection routine.

In the source for CSampleDevice::OnIrpMjRead and CSampleDevice::OnIrpMjWrite, device setup for the associated I/O operation is started by a call to the CInterruptObject object's SynchronizeExecutionForRead and SynchronizeExecutionForWrite members. As a result, CSampleDevice members OnSynchronizeExecutionForRead and OnSynchronizeExecutionForWrite are called at the raised IRQL. The shared device state can be accessed safely in these SynchCritSection routines.

Driver Deinitialization

Driver deinitialization is performed essentially in reverse order of driver initialization via the DriverUnload entry point call. When the DriverUnload entry point is called, the static CDriverObject::Unload member begins undoing the initialization performed in the CDriverObject::Initialize member.

The first deinitialization performed is to call the virtual member CDriverObject::OnUnload, which invokes the base class CDriverObject::OnUnload member. The base class version simply returns. Next, CDriverObject::Unload calls CDriverObject::UnloadDevices. It iterates through the DRIVER_OBJECT DeviceObject list, gets the CDriverObject object pointer from each DEVICE_OBJECT object's DeviceExtension, and invokes the CDeviceObject object's nonvirtual Unload member.

CDeviceObject::Unload calls its own virtual OnUnload member, then frees the memory associated with its registry path Unicode string. When CSampleDevice::OnUnload is entered, it deinitializes its CAdapterObject objects with a call to their CAdapterObject::Unload members, disconnects its CInterruptObject object by a call to CPciDevice::DisconnectInterrupt, and then calls the base class CPciDevice::OnUnload. CPciDevice::OnUnload unmaps the device memory address, which was mapped during device initialization, and frees all resources reported during the initialization process.

Conclusion

It is sometimes easy to look at an API that lends itself nicely to object-oriented programming and design. In these cases, the benefits of OOP languages such as C++ don't always manifest themselves. In many cases you end up with a thin veneer of C++ over an object-oriented API. Code reuse is minimal and the value of the time spent developing the thin veneer is questionable.

We've attempted to take a good object-oriented C API (the C-based Windows NT DDK) and connect across objects to obtain a significant amount of reusable code. Our goal was to provide a framework that allows you to worry only about specific driver requirements, not the implementation requirements for every other driver that is ever to be written.

We understand that every implementation we've provided may not be optimal or entirely complete. We do hope that this is sufficient to provide a sound foundation to launch C++ into the Windows NT DDK realm. We found this framework to be a huge timesaver. Reusable, tested code is always a benefit.

From the September 1996 issue of Microsoft Systems Journal.