The AddDevice Routine

[Previous] [Next]

In the preceding main section, I showed how you initialize a WDM driver when it's first loaded. In general, though, a driver might be called upon to manage more than one actual device. In the WDM architecture, a driver has a special AddDevice function that the PnP Manager can call for each such device. The function has the following prototype:

 NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo) { } 

The DriverObject argument points to the same driver object that you initialized in your DriverEntry routine. The pdo argument is the address of the physical device object at the bottom of the device stack, even if there are already filter drivers below.

The basic responsibility of AddDevice in a function driver is to create a device object and link it into the stack rooted in this PDO. The steps involved are as follows:

  1. Call IoCreateDevice to create a device object and an instance of your own device extension object.
  2. Register one or more device interfaces so that applications know about the existence of your device. Alternatively, give the device object a name and then create a symbolic link.
  3. Next initialize your device extension and the Flags member of the device object.
  4. Call IoAttachDeviceToDeviceStack to put your new device object into the stack.

Now I'll explain these steps in more detail.

Creating a Device Object

You create a device object by calling IoCreateDevice. For example:

 PDEVICE_OBJECT fdo; NTSTATUS status = IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &fdo); 

The first argument (DriverObject) is the same value supplied to AddDevice as the first argument. This argument establishes the connection between your driver and the new device object, thereby allowing the I/O Manager to send you IRPs intended for the device. The second argument is the size of your device extension structure. As I discussed earlier in this chapter, the I/O Manager allocates this much additional memory and sets the DeviceExtension pointer in the device object to point to it.

The third argument, which is NULL in this example, can be the address of a UNICODE_STRING providing a name for the device object. Deciding whether to name your device object and which name to give it requires some thought, and I'll describe these surprisingly complex considerations a bit further on in the section, "Should I Name My Device Object?"

The fourth argument (FILE_DEVICE_UNKNOWN) is one of the device types listed in Table 2-4. Whatever value you specify here can be overridden by an entry in the device's hardware key or class key. If both keys have an override, the hardware key has precedence. For devices that fit into one of the established categories, specify the right value in one of these places because some details about the interaction between your driver and the surrounding system depend on it. In addition, the default security settings for your device object depend on this device type.

The fifth argument (0) provides the Characteristics flag for the device object. (See Table 2-3.) These flags are relevant mostly for mass storage devices. The undocumented flag bit FILE_AUTOGENERATED_DEVICE_NAME is for internal use only—the DDK documenters didn't simply forget to mention it. Whatever value you specify here can be overridden by an entry in the device's hardware key or class key. If both keys have an override, the hardware key has precedence.

The sixth argument to IoCreateDevice (FALSE in my example) indicates whether the device is exclusive. The I/O Manager allows only one handle to be opened by normal means to an exclusive device. Whatever value you specify here can be overridden by an entry in the device's hardware key or class key. If both keys have an override, the hardware key has precedence.

NOTE
The exclusivity attribute matters only for whatever named device object is the target of an open request. If you follow Microsoft's recommended guidelines for WDM drivers, you won't give your device object a name. Open requests will then target the PDO, but the PDO will not usually be marked exclusive because the bus driver usually has no way of knowing whether you need your device to be exclusive. The only time the PDO will be marked exclusive is when there's an Exclusive override in the device's hardware key or class key's Properties subkey. You're best advised, therefore, to avoid relying on the exclusive attribute altogether. Instead, make your IRP_MJ_CREATE handler reject open requests that would violate whatever restriction you require.

The last argument (&fdo) points to a location where IoCreateDevice will store the address of the device object it creates.

If IoCreateDevice fails for some reason, it returns a status code and does not alter the PDEVICE_OBJECT described by the last argument. If it succeeds, it returns a successful status code and sets the PDEVICE_OBJECT pointer. You can then proceed to initialize your device extension and do the other work associated with creating a new device object. Should you discover an error after this point, you should release the device object and return a status code. The code to accomplish these tasks would be something like this:

 NTSTATUS status = IoCreateDevice(...); if (!NT_SUCCESS(status)) return status; ... if (<some other error discovered>) { IoDeleteDevice(fdo); return status; } 

I'll explain the NTSTATUS status codes and the NT_SUCCESS macro in the next chapter.

Naming Devices

Windows NT uses a centralized Object Manager to manage many of its internal data structures, including the driver and device objects I've been talking about. David Solomon presents a fairly complete explanation of the Windows NT Object Manager and namespace in Chapter 3, "System Mechanisms," of Inside Windows NT, Second Edition (Microsoft Press, 1998). Objects have names, which the Object Manager maintains in a hierarchical namespace. Figure 2-16 is a screen shot of my DEVVIEW application showing the top level of the name hierarchy. The objects displayed as folders in this screen shot are directory objects, which can contain subdirectories and "regular" objects. The objects displayed with other icons are examples of these regular objects. (In this respect, DEVVIEW is similar to the WINOBJ utility that you'll find in the BIN\WINNT directory of the Platform SDK. WINOBJ can't give you information about device objects and drivers, though, which is why I wrote DEVVIEW in the first place.)

click to view at full size.

Figure 2-16. Using DEVVIEW to view the namespace.

Device objects can have names that conventionally live in the \Device directory. Names for devices serve two purposes in Windows 2000. Giving your device object a name allows other kernel-mode components to find it by calling service functions like IoGetDeviceObjectPointer. Having found your device object, they can send you IRPs.

The other purpose of naming a device object is to allow applications to open handles to the device so they can send you IRPs. An application uses the standard CreateFile API to open a handle, whereupon it can use ReadFile, WriteFile, and DeviceIoControl to talk to you. The pathname an application uses to open a device handle begins with the prefix \\.\ rather than with a standard Universal Naming Convention (UNC) name such as C:\MYFILE.CPP or \\FRED\C-Drive\HISFILE.CPP. Internally, the I/O Manager converts this prefix into \??\ before commencing a name search. To provide a mechanism for connecting names in the \?? directory to objects whose names are elsewhere (such as in the \Device directory), the Object Manager implements an object called a symbolic link.

Symbolic Links

A symbolic link is a little bit like a desktop shortcut in that it points to some other entity that's the real object of attention. Symbolic links are mainly used in Windows NT to connect the leading portion of DOS-style names to devices. Figure 2-17 shows a portion of the \?? directory, which includes a number of symbolic links. Notice, for example, that C: and other drive letters in the DOS file-naming scheme are actually links to objects whose names are in the \Device directory. These links allow the Object Manager to "jump" somewhere else in the namespace as it parses through a name. So, if I call CreateFile with the name C:\MYFILE.CPP, the Object Manager will take this path to open the file:

  1. Kernel-mode code initially sees the name \??\C:\MYFILE.CPP. The Object Manager looks up "??" in the root directory and finds a directory object with that name.
  2. The Object Manager now looks up "C:" in the \?? directory. It finds a symbolic link by that name, so it forms the new kernel-mode pathname \Device\HarddiskVolume1\MYFILE.CPP and parses that.
  3. Working with the new pathname, the Object Manager looks up "Device" in the root directory and finds a directory object.
  4. The Object Manager looks up "HarddiskVolume1" in the \Device directory. It finds a device object by that name.

click to view at full size.

Figure 2-17. The \?? directory with several symbolic links.

At this point in the process, the Object Manager will create an IRP that it will send to the driver(s) for HarddiskVolume1. The IRP will eventually cause some file system driver or another to locate and open a disk file. Describing how a file system driver works is beyond the scope of this book. If we were dealing with a device name like COM1, the driver that ends up receiving the IRP would be the driver for \Device\Serial0. How a device driver handles an open request is definitely within the scope of this book, and I'll be discussing it in this chapter (in the section "Should I Name My Device Object?") and in Chapter 5 when I'll talk about IRP processing in general.

A user-mode program can create a symbolic link by calling DefineDosDevice, as in this example:

 BOOL okay = DefineDosDevice(DDD_RAW_TARGET_PATH, "barf", "\\Device\\SECTEST_0"); 

You can see the aftermath of a call like this one in Figure 2-17, by the way.

You can create a symbolic link in a WDM driver by calling IoCreateSymbolicLink,

 IoCreateSymbolicLink(linkname, targname); 

where linkname is the name of the symbolic link you want to create and targname is the name to which you're linking. Incidentally, the Object Manager doesn't care whether targname is the name of any existing object: someone who tries to access an object by using a link that points to an undefined name simply receives an error. If you want to allow user-mode programs to override your link and point it somewhere else, you should call IoCreateUnprotectedSymbolicLink instead.

ARC Names

In the Advanced RISC Computing (ARC) architecture, there is a concept known as ARC naming that Windows 2000 relies on. You can see ARC names at work in the BOOT.INI file in the root directory of your boot drive. Here's what my copy of that file looked like at one point in the development of this book:

 [boot loader] timeout=30 default=c:\ [operating systems] C:\="Microsoft Windows 98" scsi(0)disk(1)rdisk(0)partition(1)\BETA2F="Win2k Beta-2 (Free Build)" /fastdetect /noguiboot scsi(0)disk(1)rdisk(0)partition(1)\WINNT="Win2K Beta-3 (Free Build)" /fastdetect /noguiboot 

On an Intel platform, ARC names like scsi(0)disk(1)rdisk(0)partition(1) are symbolic links within the kernel's \ArcName directory that point—eventually, that is, if you resolve all the links in the way—to regular device objects. DEVVIEW will show you these links on your own system.

Drivers for mass-storage devices other than hard disks should call IoAssignArcName during initialization to set up one of these links. The I/O Manager automatically creates the ARC names for hard disk devices, since these are needed to boot the system in the first place.

Should I Name My Device Object?

Deciding whether to give your device object a name requires, as I said earlier, a little thought. If you give your object a name, it will be possible for any kernel-mode program to try to open a handle to your device. Furthermore, it will be possible for any kernel-mode or user-mode program to create a symbolic link to your device object and to use the symbolic link to try to open a handle. You might or might not want to allow these actions.

The primary consideration in deciding whether to name your device object is security. When someone opens a handle to a named object, the Object Manager verifies that they have permission to do so. When IoCreateDevice creates a device object for you, it assigns a default security descriptor based on the device type you specify as the fourth argument. There are three basic categories that the I/O Manager uses to select a security descriptor. (Refer to the second column in Table 2-4.)

  • Most file system device objects (that is, disk, CD-ROM, file, and tape) receive the "public default unrestricted" access control list (ACL). This list gives just SYNCHRONIZE, READ_CONTROL, FILE_READ_ATTRIBUTES, and FILE_TRAVERSE access to everyone except the System account and all administrators. File system device objects, by the way, exist only so that there can be a target for a CreateFile call that will open a handle to a file managed by the file system.
  • Disk devices and network file system objects receive the same ACL as the file system objects with some modifications. For example, everyone gets full access to a named floppy disk device object, and administrators get sufficient rights to run ScanDisk. (User-mode network provider DLLs need greater access to the device object for their corresponding file system driver, which is why network file systems are treated differently than other file systems.)
  • All other device objects receive the "public open unrestricted" ACL, which allows anyone with a handle to the device to do pretty much anything.

You can see that anyone will be able to access a nondisk device for both reading and writing if the driver gives the device object a name at the time when it calls IoCreateDevice. This is because the default security allows nearly full access and because there is no security check at all associated with creating a symbolic link—the security checks happen at open time, based on the named object's security descriptor. This is true even if other device objects in the same stack have more restrictive security.

DEVVIEW will show you the security attributes of the device objects it displays. You can see the operation of the default rules I just described by examining a file system, a disk device, and any other random device.

The PDO also receives a default security descriptor, but it's possible to override it with a security descriptor stored in the hardware key or in the Properties subkey of the class key. (The hardware key has precedence if both keys specify a descriptor.) Even lacking a specific security override, if either the hardware key or the class key's Properties subkey overrides the device type or characteristics specification, the I/O Manager constructs a new default security descriptor based on the new type. The I/O Manager does not, however, override the security setting for any of the other device objects above the PDO. Consequently, for the overrides (and the administrative actions that set them up) to have any effect, you should not name your device object. Don't despair though—applications can still access your device by means of a registered interface, which I'll discuss very shortly.

You need to know about one last security concern. As the Object Manager parses its way through an object name, it needs only FILE_TRAVERSE access to the intermediate components of the name. It only performs a full security check on the object named by the final component. So, suppose you had a device object reachable under the name \Device\SECTEST_0 or by the symbolic link \??\SecurityTest_0. A user-mode application that tries to open \\.\SecurityTest_0 for writing will be blocked if the object security has been set up to deny write access. But if the application tries to open a name like \\.\SecurityTest_0\ExtraStuff that has additional name qualifications, the open request will make it all the way to the device driver (in the form of an IRP_MJ_CREATE I/O request) if the user merely has FILE_TRAVERSE permission, which is routinely granted. The I/O Manager expects the device driver to deal with the additional name components and to perform any required security checks with regard to them.

To avoid the security concern I just described, you can supply the flag FILE_DEVICE_SECURE_OPEN in the device characteristics argument to IoCreateDevice. This flag causes Windows 2000 to verify that someone has the right to open a handle to a device even if additional name components are present.

The Device Name

If you decide to name the device object, you would normally put the name in the \Device branch of the namespace. To give it a name, you have to create a UNICODE_STRING structure to hold the name, and you have to specify that string as an argument to IoCreateDevice:

 UNICODE_STRING devname; RtlInitUnicodeString(&devname, L"\\Device\\Simple0"); IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &devname, ...); 

I'll discuss the usage of RtlInitUnicodeString in the next chapter.

Conventionally, drivers assign their device objects a name by concatenating a string naming their device type ("Simple" in this fragment) with a zero-based integer denoting an instance of that type. In general, you don't want to hard-code a name like I just did—you want to compose it dynamically using string-manipulation functions like the following:

 UNICODE_STRING devname; static LONG lastindex = -1; LONG devindex = InterlockedIncrement(&lastindex); WCHAR name[32]; _snwprintf(name, arraysize(name), L"\\Device\\SIMPLE%2.2d", devindex); RtlInitUnicodeString(&devname, name); IoCreateDevice(...); 

I'll explain the various service functions used in this fragment in the next couple of chapters. The instance number you derive for private device types might as well be a static variable, as shown in the previous fragment.

Notes on Device Naming

If all you wanted to do was to provide a quick-and-dirty way for an application to open a handle to your device during development, you could perfectly well assign the device object a name in the \?? branch. For a production driver, however, you're better advised to do what the text suggests and name the device object with a \Device directory name.

The \?? directory used to be named \DosDevices. In fact, \DosDevices will still work, but it itself is a symbolic link to \??. The change was made to move the often-searched directory of user-mode names to the front of the alphabetical list of directories. See the "Windows98 Compatibility Notes" section at the end of this chapter for an important caution about using \?? in your names.

In previous versions of Windows NT, drivers for certain classes of devices (notably disks, tapes, serial ports, and parallel ports) called IoGetConfigurationInformation to obtain a pointer to a global table containing counts of devices in each of these special classes. A driver would use the current value of the counter to compose a name like Harddisk0, Tape1, and so on, and would also increment the counter. WDM drivers don't need to use this service function or the table it returns, however. Constructing names for the devices in these classes is now the responsibility of a Microsoft type-specific class driver (such as DISK.SYS).

Device Interfaces

The older method of naming I just discussed—naming your device object and creating a symbolic link name that applications can use—has two major problems. We've already discussed the security implications of giving your device object a name. In addition, the author of an application that wants to access your device has to know the scheme you adopted to name your devices. If you're the only one writing the applications that will be accessing your hardware, that's not much of a problem. But if many different companies will be writing applications for your hardware, and especially if many hardware companies are making similar devices, devising a suitable naming scheme is difficult. Finally, many naming schemes rely on the language spoken by the programmer, which isn't necessarily a good choice in an increasingly global economy. (My favorite example involves an American chef who tells a German diner he's eating a "gift" [poison], whereupon the diner, only incompletely realizing the linguistic difficulty, calls the chef a "schmuck" [jewelry].)

To solve these problems, WDM introduces a new naming scheme for devices that is language-neutral, easily extensible, usable in an environment with many hardware and software vendors, and easily documented. The scheme relies on the concept of a device interface, which is basically a specification for how software can access hardware. A device interface is uniquely identified by a 128-bit GUID. You can generate GUIDs by running the Platform SDK utilities UUIDGEN or GUIDGEN—both utilities generate the same kind of number, but they output the result in different formats. The idea is that some industry group gets together to define a standard way of accessing a certain kind of hardware. As part of the standard-making process, someone runs GUIDGEN and publishes the resulting GUID as the identifier that will be forever after associated with that interface standard.

More About GUIDs

The GUIDs used to identify software interfaces are the same kind of unique identifier that's used in the Component Object Model (COM) to identify COM interfaces and in the Open Software Foundation (OSF) Distributed Computing Environment (DCE) to identify the target of a remote procedure call (RPC). For an explanation of how GUIDs are generated so as to be statistically unique, see page 66 of Kraig Brockschmidt's Inside OLE, Second Edition (Microsoft Press, 1995), which contains a further reference to the original algorithm specification by the OSF. I found the relevant portion of the OSF specification online at http://www.opengroup.org/onlinepubs/9629399/apdxa.htm.

The mechanics of creating a GUID for use in a device driver involve running either UUIDGEN or GUIDGEN and then capturing the resulting identifier in a header file. GUIDGEN is easier to use because it allows you to choose to format the GUID for use with the DEFINE_GUID macro and to copy the resulting string onto the clipboard. Figure 2-18 shows the GUIDGEN window. You can paste its output into a header file to end up with this:

 // {CAF53C68-A94C-11d2-BB4A-00C04FA330A6} DEFINE_GUID(<<name>>, 0xcaf53c68, 0xa94c, 0x11d2, 0xbb, 0x4a, 0x0, 0xc0, 0x4f, 0xa3, 0x30, 0xa6); 

You then replace the <<name>> with something more mnemonic like GUID_SIMPLE and include the definition in your driver and applications.

Figure 2-18. Using GUIDGEN to generate a GUID.

I think of an interface as being analogous to the protein markers that populate the surface of living cells. An application desiring to access a particular kind of device has its own protein markers that fit like a key into the markers exhibited by conforming device drivers. See Figure 2-19.

click to view at full size.

Figure 2-19. Using device interfaces to match up applications and devices.

Registering a Device Interface A function driver's AddDevice function should register one or more device interfaces by calling IoRegisterDeviceInterface, as shown here:

 1  2  3  
 #include <initguid.h> #include "guids.h" ... NTSTATUS AddDevice(...) { ... IoRegisterDeviceInterface(pdo, &GUID_SIMPLE, NULL, &pdx->ifname); ... } 

  1. We're about to include a header (GUIDS.H) that contains one or more DEFINE_GUID macros. DEFINE_GUID normally declares an external variable. Somewhere in the driver, though, we have to actually reserve initialized storage for every GUID we're going to reference. The system header file INITGUID.H works some preprocessor magic to make DEFINE_GUID reserve the storage even if the definition of the DEFINE_GUID macro happens to be in one of the precompiled header files.
  2. I'm assuming here that I put the GUID definitions I want to reference into a separate header file. This would be a good idea, inasmuch as user-mode code will also need to include these definitions and will not want to include a bunch of extraneous kernel-mode declarations relevant only to our driver.
  3. The first argument to IoRegisterDeviceInterface must be the address of the PDO for your device. The second argument identifies the GUID associated with your interface, and the third argument specifies additional qualified names that further subdivide your interface. Only Microsoft code uses this name subdivision scheme. The last argument is the address of a UNICODE_STRING structure that will receive the name of a symbolic link that resolves to this device object.

The return value from IoRegisterDeviceInterface is a Unicode string that applications will be able to determine without knowing anything special about how you coded your driver and will then be able to use in opening a handle to the device. The name is pretty ugly, by the way; here's an example that I generated for one of my sample devices in Windows 98: \DosDevices\0000000000000007#{CAF53C68-A94C-11d2-BB4A-00C04FA330A6}. (You can call it 007 once you get to know it better.)

All that registration actually does is create the symbolic link name and save it in the registry. Later on, in response to the IRP_MN_START_DEVICE Plug and Play request we'll discuss in Chapter 6, you'll make the following call to IoSetDeviceInterfaceState to "enable" the interface:

 IoSetDeviceInterfaceState(&pdx->ifname, TRUE); 

In response to this call, the I/O Manager creates an actual symbolic link object pointing to the PDO for your device. You'll make a matching call to disable the interface at a still later time (just call IoSetDeviceInterfaceState with a FALSE argument), whereupon the I/O Manager will delete the symbolic link object while preserving the registry entry that contains the name. In other words, the name persists and will always be associated with this particular instance of your device; the symbolic link object comes and goes with the hardware.

Since the interface name ends up pointing to the PDO, the PDO's security descriptor ends up controlling whether people can access your device. That's good, because it's the PDO's security that an administrator can control through the Management Console.

Enumerating Device Interfaces Both kernel-mode and user-mode code can locate all the devices that happen to support an interface in which they're interested. I'm going to explain how to enumerate all the devices for a particular interface in user mode. The enumeration code is so tedious to write that I eventually wrote a C++ class to make my own life simpler. You'll find this code in the DEVICELIST.CPP and DEVICELIST.H files that are part of the WDMIDLE sample in Chapter 8, "Power Management." These files declare and implement a CDeviceList class, which contains an array of CDeviceListEntry objects. These two classes have the following declaration:

 class CDeviceListEntry { public: CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname); CDeviceListEntry(){} CString m_linkname; CString m_friendlyname; }; class CDeviceList { public: CDeviceList(const GUID& guid); ~CDeviceList(); GUID m_guid; CArray<CDeviceListEntry, CDeviceListEntry&> m_list; int Initialize(); }; 

The classes rely on the CString class and CArray template class that are part of the Microsoft Foundation Classes (MFC) framework. The constructors for these two classes simply copy their arguments into the obvious data members:

 CDeviceList::CDeviceList(const GUID& guid) { m_guid = guid; } CDeviceListEntry::CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname) { m_linkname = linkname; m_friendlyname = friendlyname; } 

All the interesting work occurs in the CDeviceList::Initialize function. The executive overview of what it does is this: it will enumerate all of the devices that expose the interface whose GUID was supplied to the constructor. For each such device, it will determine a "friendly" name that we're willing to show to an unsuspecting end user. Finally, it will return the number of devices it found. Here's the code for this function:

 1  2  3  4  5  
 int CDeviceList::Initialize() { HDEVINFO info = SetupDiGetClassDevs(&m_guid, NULL, NULL, DIGCF_PRESENT | DIGCF_INTERFACEDEVICE); if (info == INVALID_HANDLE_VALUE) return 0; SP_INTERFACE_DEVICE_DATA ifdata; ifdata.cbSize = sizeof(ifdata); DWORD devindex; for (devindex = 0; SetupDiEnumDeviceInterfaces(info, NULL, &m_guid, devindex, &ifdata); ++devindex) { DWORD needed; SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &needed, NULL); PSP_INTERFACE_DEVICE_DETAIL_DATA detail = (PSP_INTERFACE_DEVICE_DETAIL_DATA) malloc(needed); detail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA); SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)}; SetupDiGetDeviceInterfaceDetail(info, &ifdata, detail, needed, NULL, &did)); TCHAR fname[256]; if (!SetupDiGetDeviceRegistryProperty(info, &did, SPDRP_FRIENDLYNAME, NULL, (PBYTE) fname, sizeof(fname), NULL) && !SetupDiGetDeviceRegistryProperty(info, &did, SPDRP_DEVICEDESC, NULL, (PBYTE) fname, sizeof(fname), NULL)) _tcsncpy(fname, detail->DevicePath, 256); CDeviceListEntry e(detail->DevicePath, fname); free((PVOID) detail); m_list.Add(e); } SetupDiDestroyDeviceInfoList(info); return m_list.GetSize(); }

  1. This statement opens an enumeration handle that we can use to find all devices that have registered an interface that uses the same GUID.
  2. Here we call SetupDiEnumDeviceInterfaces in a loop to find each device.
  3. The only two items of information we need are the "detail" information about the interface and information about the device instance. The detail is just the symbolic name for the device. Since it's variable in length, we make two calls to SetupDiGetDeviceInterfaceDetail. The first call determines the length. The second call retrieves the name.
  4. We obtain a "friendly" name for the device from the registry by asking for either the FriendlyName or the DeviceDesc.
  5. We create a temporary instance named e of the CDeviceListEntry class, using the device's symbolic name as both the link name and the friendly name.

Friendly Names

You might be wondering how the registry comes to have a FriendlyName for a device. The INF file you use to install your device driver—see Chapter 12—can have an HW section that specifies registry parameters for the device. You should normally provide a FriendlyName as one of these parameters.

Other Global Device Initialization

You need to take some other steps during AddDevice to initialize your device object. I'm going to describe these steps in the order you should do them, which isn't exactly the same order as their respective logical importance. I want to emphasize that the code snippets in this section are even more fragmented than usual—I'm going to show only enough of the entire AddDevice routine to establish the surrounding context for the small pieces I'm trying to illustrate.

Initializing the Device Extension

The content and management of the device extension are entirely up to you. The data members you place in this structure will obviously depend on the details of your hardware and on how you go about programming the device. Most drivers would need a few items placed there, however, as illustrated in the following fragment of a declaration:

 1  2  3  4  5  6  7  8  
 typedef struct _DEVICE_EXTENSION { PDEVICE_OBJECT DeviceObject; PDEVICE_OBJECT LowerDeviceObject; PDEVICE_OBJECT Pdo; UNICODE_STRING ifname; IO_REMOVE_LOCK RemoveLock; DEVSTATE devstate; DEVSTATE prevstate; POWERSTATE powerstate; DEVICE_POWER_STATE devpower; SYSTEM_POWER_STATE syspower; DEVICE_CAPABILITIES devcaps; ... } DEVICE_EXTENSION, *PDEVICE_EXTENSION; 

  1. I find it easiest to mimic the pattern of structure declaration used in the official DDK, so I declared this device extension as a structure with a tag as well as a type and pointer-to-type name.
  2. You already know that you locate your device extension by following the DeviceExtension pointer from the device object. It's also useful in several situations to be able to go the other way—to find the device object given a pointer to the extension. The reason is that the logical argument to certain functions is the device extension itself (since that's where all of the per-instance information about your device resides). Hence, I find it useful to have this DeviceObject pointer.
  3. I'll mention in a few paragraphs that you need to record the address of the device object immediately below yours when you call IoAttachDeviceToDeviceStack, and LowerDeviceObject is the place to do that.
  4. A few service routines require the address of the PDO instead of some higher device object in the same stack. It's very difficult to locate the PDO, so the easiest way to satisfy the requirement of those functions is to record the PDO address in a member of the device extension that you initialize during AddDevice.
  5. Whichever method (symbolic link or device interface) you use to name your device, you'll want an easy way to remember the name you assign. In this fragment, I've declared a Unicode string member named ifname to record a device interface name. If you were going to use a symbolic link name instead of a device interface, it would make sense to give this member a more mnemonic name, such as "linkname."
  6. I'll discuss in Chapter 6 a synchronization problem affecting how you decide when it's safe to remove this device object by calling IoDeleteDevice. The solution to that problem involves using an IO_REMOVE_LOCK object that needs to be allocated in your device extension as shown here. AddDevice needs to initialize that object.
  7. You'll probably need a device extension variable to keep track of the current Plug and Play state and current power states of your device. DEVSTATE and POWERSTATE are enumerations that I'm assuming you've declared elsewhere in your own header file. I'll discuss the use of all these state variables in later chapters.
  8. Another part of power management involves remembering some capability settings that the system initializes by means of an IRP. The devcaps structure in the device extension is where I save those settings in my sample drivers.

The initialization statements in AddDevice (with emphasis on the parts involving the device extension) would be as follows:

 NTSTATUS AddDevice(...) { PDEVICE_OBJECT fdo; IoCreateDevice(..., sizeof(DEVICE_EXTENSION), ..., &fdo); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; pdx->DeviceObject = fdo; pdx->Pdo = pdo; IoInitializeRemoveLock(&pdx->RemoveLock, ...); pdx->devstate = STOPPED; pdx->powerstate = POWERON; pdx->devpower = PowerDeviceD0; pdx->syspower = PowerSystemWorking; IoRegisterDeviceInterface(..., &pdx->ifname); pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(...); } 

Initializing the Default DPC Object

Many devices signal completion of operations by means of an interrupt. As you'll learn when I discuss interrupt handling in Chapter 7, "Reading and Writing Data," there are strict limits on what your interrupt service routine (ISR) can do. In particular, an ISR isn't allowed to call the routine (IoCompleteRequest) that signals completion of an IRP, but that's exactly one of the steps you're likely to want to take. You utilize a deferred procedure call (DPC) to get around the limitations. Your device object contains a subsidiary DPC object that can be used for scheduling your particular DPC routine, and you need to initialize it shortly after creating the device object:

 NTSTATUS AddDevice(...) { IoCreateDevice(...); IoInitializeDpcRequest(fdo, DpcForIsr); } 

Setting the Buffer Alignment Mask

Devices which perform direct memory access (DMA) transfers work directly with data buffers in memory. The HAL might require that buffers used for DMA be aligned to some particular boundary, and your device might require still more stringent alignment. The AlignmentRequirement field of the device object expresses the restriction—it is a bit mask equal to one less that the required address boundary. You can round an arbitrary address down to this boundary with this statement:

 PVOID address = ...; SIZE_T ar = fdo->AlignmentRequirement; address = (PVOID) ((SIZE_T) address & ~ar); 

You round an arbitrary address up to the next alignment boundary like this:

 PVOID address = ...; SIZE_T ar = fdo->AlignmentRequirement; address = (PVOID) (((SIZE_T) address + ar) & ~ar); 

In these two code fragments, I used SIZE_T casts to transform the pointer (which may be 32 bits or 64 bits wide, depending on the platform for which you're compiling) into an integer wide enough to span the same range as the pointer.

IoCreateDevice sets the AlignmentRequirement field of the new device object equal to whatever the HAL requires. For example, the HAL for Intel x86 chips has no alignment requirement, so AlignmentRequirement is 0 initially. If your device requires a more stringent alignment for the data buffers it works with (say, because you have bus-mastering DMA capability with a special alignment requirement), you want to override the default setting. For example:

 if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement) fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1; 

I've assumed here that elsewhere in your driver is a manifest constant named MYDEVICE_ALIGNMENT that equals a power of two and represents the required alignment of your device's data buffers.

Miscellaneous Objects

Your device might well use other objects that need to be initialized during AddDevice. Such objects might include a controller object, various synchronization objects, various queue anchors, scatter/gather list buffers, and so on. I'll discuss these objects, and the fact that initialization during AddDevice would be appropriate, in various other parts of the book.

Initializing the Device Flags

Two of the flag bits in your device object need to be initialized during AddDevice and never changed thereafter: the DO_BUFFERED_IO and DO_DIRECT_IO flags. You can set one (but only one) of these bits to declare once and for all how you want to handle memory buffers coming from user mode as part of read and write requests. (I'll explain in Chapter 7 what the difference between these two buffering methods is and why you'd want to pick one or the other.) The reason you have to make this important choice during AddDevice is that any upper filter drivers that load after you will be copying your flag settings and it's the setting of the bits in the topmost device object that's actually important. Were you to change your mind after the filter drivers load, they probably wouldn't know about the change.

Three of the flag bits in the device object pertain to power management. In contrast to the two buffering flags, these three can be changed at any time. I'll discuss them in greater detail in Chapter 8, but here's a preview. DO_POWER_PAGABLE means that the Power Manager must send you IRP_MJ_POWER requests at interrupt request level (IRQL) DISPATCH_LEVEL. (If you don't understand all of the concepts in the preceding sentence, don't worry—I'll completely explain all of them in later chapters.) DO_POWER_INRUSH means that your device draws a large amount of current when powering on, such that the Power Manager should make sure that no other in-rush device is powering up simultaneously. DO_POWER_NOOP means that you don't participate in power management in the first place and is only an appropriate setting for WDM drivers that don't manage any hardware.

Setting the Initial Power State

Most devices start life in the fully powered state. If you know the initial state of your device, you should tell the Power Manager:

 POWER_STATE state; state.DeviceState = PowerDeviceD0; PoSetPowerState(fdo, DevicePowerState, state); 

See Chapter 8 for much more detail about power management.

Building the Device Stack

Each filter and function driver has the responsibility of building up the stack of device objects, starting from the PDO and working upward. You accomplish your part of this work with a call to IoAttachDeviceToDeviceStack:

 NTSTATUS AddDevice(..., PDEVICE_OBJECT pdo) { PDEVICE_OBJECT fdo; IoCreateDevice(..., &fdo); pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo); } 

The first argument to IoAttachDeviceToDeviceStack (fdo) is the address of your own newly created device object. The second argument is the address of the PDO. The second argument to AddDevice is this address. The return value is the address of whatever device object is immediately underneath yours, which can be the PDO or the address of some lower filter device object.

Clear DO_DEVICE_INITIALIZING

Pretty much the last thing you do in AddDevice should be to clear the DO_DEVICE_INITIALIZING flag in your driver object:

 fdo->Flags &= ~DO_DEVICE_INITIALIZING; 

While this flag is set, the I/O Manager will refuse to attach other device objects to yours or to open a handle to your device. You have to clear the flag because your device object initially arrives in the world with the flag set. In previous releases of Windows NT, most drivers created all of their device objects during DriverEntry. When DriverEntry returns, the I/O Manager automatically traverses the list of device objects linked from the driver object and clears this flag. Since you're creating your device object long after DriverEntry returns, however, this automatic flag clearing won't occur, and you must do it yourself.



Programming the Microsoft Windows Driver Model
Programming the Microsoft Windows Driver Model
ISBN: 0735618038
EAN: 2147483647
Year: 1999
Pages: 93
Authors: Walter Oney

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net