Using a device object namespace instead of a bus driver
Suppose you're writing a driver for a device that contains several different kinds of sensors. You want to allow clients to open individual sensors and you want to give more than one client simultaneous access to a given sensor, but you want to avoid the complexity of writing a bus driver that exposes each sensor through Plug and Play.
Your first thought might be to create a device object for each sensor, but there's a simpler solution: Use a file-system-style notation to define a "sensor namespace" for your device object. Your driver can determine which sensor is being opened by parsing the file name in the create request. You can even expose the different sensors through separate Plug and Play device interfaces, something you can't do with one device object for each sensor. You can use the namespace technique in drivers for Microsoft Windows NT 4.0 and later versions of Windows.
In a WDM driver, every device object has an associated namespace. Names in the device's namespace are paths that begin with the device's name. If a driver supports open requests into its device namespace, it treats an open request as a request to open a file. However, the "file" can be any item that is suitable for the driver. For the sensor example, these items are the sensors in the device.
Here's how you might implement a namespace for the sensor example. Suppose your device object is \device\sensors. This is the root of your namespace. Clients can always open \device\sensors, either directly or through a symbolic link (\\.\Sensors) that refers to \device\sensors. Clients can also open individual sensors by using the same notation that is used for files in a file system: \device\sensors\1, \device\sensors\2, or even \device\sensors\temperature\outside. The "trailing name" after \device\sensors\ specifies which sensor the client wants to open. This works in the same way for symbolic links. For example, if you set up a symbolic link to \device\sensors, a client can open \\.\sensors\1, \\.\sensors\2, \\.\sensors\temperature\outside, and so on.
All create requests (IRP_MJ_CREATE) are sent to your driver's DispatchCreate routine. The driver can determine which sensor a client is trying to open by checking FileObject >FileName in the I/O stack location of the create request. (Remember that FileName is allocated from paged pool, so it must be accessed at PASSIVE_LEVEL.) FileName is a Unicode string that is either the name of the sensor being opened on the device (for example, temperature\outside) or NULL, which means that the caller is opening the device object itself (that is, \device\sensors\). If FileName contains a string, the string is the "trailing name" that your driver can parse to identify which sensor is being opened. So if a client opens \device\sensors\temperature\outside, FileName contains "\temperature\outside".
Now what? Parse the string in FileName and store any information that you want to keep where you can access it for future read or write requests, so you can identify the sensor being targeted by I/O requests that pass this file object. You must store or parse at least the FileName string because this string is valid only during the DispatchCreate call and cannot be accessed in future read or write requests to see which sensor is being used.
The device extension will not work for this purpose because it is shared by all of the open handles to your device object. Instead, you should allocate storage and point to it from the FsContext of the file object, which is associated with this particular handle (and therefore with this client and this sensor). Every I/O request that your driver receives has the corresponding file object in its I/O stack location, so you can check the per-client context at FsContext to learn which sensor the client wants to access. If the driver stack to which your driver is attached already uses FsContext and FsContext2 for another purpose, create a lookup table that maps the PFILE_OBJECT to your context pointer and store the table in the device extension.
If your driver allows more than one client at a time to access a given sensor, you'll also want to maintain a reference count for each sensor. Increment the reference count in the driver's DispatchCreate and decrement it in DispatchClose.
When a client finishes using a sensor, it calls CloseHandle, which results in one or more cleanup requests (IRP_MJ_CLEANUP) with the file object that corresponds to that handle being sent to your driver. Your driver should complete any outstanding I/O for this file object with STATUS_CANCELLED (which might require canceling requests to lower drivers). After all of the outstanding I/O is finished, your driver receives a close request (IRP_MJ_CLOSE). In response, your driver should free the FsContext structure, decrement the reference count on per-sensor data and, if no other clients are still using the sensor, perform any other tasks involved in shutting down the sensor, such as reducing power.
If you're writing a PnP driver, you can use this technique with device interfaces. IoRegisterDeviceInterface takes an optional ReferenceString parameter that is usually NULL. If you supply a string for this parameter, the I/O manager appends it to the symbolic link that it creates for the interface. Then, when a client enumerates the interface and uses it to open the corresponding device path, the reference string (and any additional string the client might supply) are appended to the file object name in the create request. For example, if ReferenceString is "temperature", the symbolic link returned by IoRegisterDeviceInterface would be \\.\<symbolic link>\temperature.
Using the reference string for a device interface creates a unique name that you can use to distinguish among individual items. For example, if you have two different sensors, you might implement a device interface for each sensor. A client could use the device installation functions (SetupDiXxx) to discover and open all sensors, whereas your driver could use the reference string to distinguish among individual sensors.
Remember that namespaces were designed for file systems, which manage security, exclusive access, and so forth, so your driver must manage these in its device namespace. You should always specify FILE_DEVICE_SECURE_OPEN in the characteristics of your device object, so that the I/O manager applies the security descriptor of the device object to all open requests, including file-open requests.
You should also ensure that your device object is opened for nonexclusive access. The DO_EXCLUSIVE flag in the device object's flags provides exclusive access at the device level. This flag is clear by default, thus providing nonexclusive access. Unlike some other device object flags, a driver does not set or clear DO_EXCLUSIVE directly. Instead, exclusive access is enabled by the class or device INF for a WDM driver or by setting Exclusive to TRUE when calling IoCreateDeviceSecure for a non-WDM driver or for a device that operates in raw mode. If DO_EXCLUSIVE is set, then only one client at a time can open any item within the device's namespace. If you want to provide exclusive access to a given item, keep a per-item reference count as described earlier and fail create requests if that reference count is non-zero.
The USB bulk transfer client driver sample (bulk usb.sys) in the Windows Server 2003 with Service Pack 1 (SP1) DDK implements a namespace to allow clients to open individual bulk transfer pipes by name. For details, see the sample source code at %winddk%\src\wdm\usb\bulkusb\.
What should you do?
If you are writing a single driver that provides access to multiple items, implement a namespace rather than multiple device objects or a bus driver:
- Use a file-system-style notation to identify each item that your device supports.
- Check FileObject >FileName in the I/O stack location of the create request to determine which item is being opened.
- Allocate storage for per-client information, including the filename string, and point to it from the FsContext of the file object, or create a lookup table that maps the PFILE_OBJECT to your context pointer and store the table in the device extension.
- If you're writing a PnP driver, use device interfaces to distinguish among kinds of items and the reference string to identify an individual item.
- Ensure that the device object is opened for nonexclusive access.
- Maintain a reference count of the number of times each item has been opened. If you want an item to be opened for exclusive use by one client, fail any create requests for the item if it is already in use.
- Specify FILE_DEVICE_SECURE_OPEN in the characteristics of your device object so the I/O manager applies the security descriptor of the device object to open requests for items as well as for the device itself.