Last reviewed and updated: 10 August 2020
In a recent article, I described three of the most common WDF errors that we, here at OSR, see in released drivers. One type of error that I described is the use, or rather misuse, of I/O Targets. The root cause of the problems in this category is that WDF allows you to use architecturally invalid combinations of activities involving I/O Targets without reporting an error. As a result, you can code-up your driver, test it, and even ship it… only to have your driver break later on when it encounters a slight change in its runtime environment.
In this article, I’ll provide some guidelines for using WDF I/O Targets that – if you follow them – will ensure that you stay out of trouble with your use of WdfRequestSend and I/O Targets.
Background
In case you’re not familiar with I/O Targets and WdfRequestSend, I’ll give you a brief introduction. An I/O Target is a location to which you can send WDF Requests. There are two types of I/O Targets that are interesting in terms of our discussion: Local I/O Targets and Remote I/O Targets. The third type of I/O Target, named “special” I/O Targets, are related to USB (only) and are not particularly relevant to this article.
If your driver wants to send a Request to “the next device down” in its Device Stack, it retrieves the handle to its Local I/O Target by calling WdfDeviceGetIoTarget. If your driver wants to send a Request to a device in the system other than the device that’s immediately below it in the Device Stack, it needs the handle to a Remote I/O Target that represents that device. To get this handle, the driver creates an empty I/O Target object using the function WdfIoTargetCreate and then opens that newly created I/O Target object using the function WdfIoTargetOpen. The target device can be described to the WdfIoTargetOpen function either by name or by providing a pointer to that device’s existing native WDM Device Object.
When you send a Request to an I/O Target, you must supply the I/O parameters that will be sent with the Request to that I/O Target. These parameters include the I/O function code (Read, Write, DeviceControl, InternalDeviceControl), a description of the associated data buffer(s), and the offset on the device at which the operation should start (like, the offset from which to start reading or writing). The way you supply these parameters is by “formatting” the Request prior to sending it with WdfRequestSend. This format step is almost always an explicit step in setting up the Request to be sent, but in some very specific cases the formatting can be done implicitly. We’ll talk about these cases when we discuss The Rules below.
That’s a pretty quick description. If you need to know more, you should take our WDF seminar. Or search the web.
The Key to Understanding: Realizing Local and Remote Targets Are Very Different
The key thing to realize about using I/O Targets is that the operations you can perform, and how you handle the Request that you’re going to send, is dependent on the type of I/O Target you’re using. Once you understand the steps and allowed options for sending to a Local I/O Target and those for sending to a Remote I/O Target are different, you’re on your way to writing safe, stable, correct code that will continue working even outside your test environment and into WDF versions in the future.
In the following section I’ll describe some of the basic rules. In this article, I’m going to stick to the most common design patterns and the major rules. As a result, I’m going to consciously ignore some of the less-used design patterns and some of the things that are possible but are rarely done. So, if you’re an experienced WDF developer or you’re a member of the WDF development team, don’t get all upset that I haven’t described your favorite special case. The rules I describe here err on the side of being conservative. As a result, I can confidently say that if you stick to these rules below, you’ll never go wrong when using I/O Targets.
The Rules for Sending to a Remote I/O Target
Recall that you can open a Remote I/O Target by name or by providing a pointer to an existing native WDM Device Object.
The primary rule is: If you’re sending a Request to a Remote I/O Target, you must format the Request for that specific I/O Target before sending it. That means you must call one of the formatting functions that starts with WdfIoTarget, such as WdfIoTargetFormatRequestForXxxx (where Xxxx is Read, Write, Ioctl, and InternalIoctl). There is no other way that’s legal. Note, specifically, that it is not correct (or even supported) to call WdfRequestFormatRequestUsingCurrentType before you send a Request to a Remote I/O Target. Judging by a lot of the code I’ve read, this will come as a surprise to a lot of people. You have to use a method that’s specific to the I/O Target to which you’ll be sending the Request. Hence, the method you use must start with WdfIoTarget.
When you send the Request, you may send the Request to the Remote I/O Target either Synchronously or Asynchronously with a callback. There are some very narrow, very limited cases where it’s possible to send a Request to a Remote I/O Target using _SEND_AND_FORGET but these cases are so limited that they’re not worth discussing. So, forget _SEND_AND_FORGET for Remote I/O Targets. Just remember that if you’re sending a Request to a Remote I/O Target, you must send it either Synchronously or Asynchronously with a callback.
Pretty simple, right? Right: Format the Request using WdfIoTargetFormatRequestForXxxx, specify a WDF_REQUEST_SEND_OPTIONS structure specifying either synchronous processing or asynchronous processing with a callback, call WdfRequestSend, and you’re done. You can see this in code in Figure 1.
status = WdfRequestRetrieveOutputMemory(Request, &outputMemory); if (!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); return; } // // Step 1: Format with reference to the specific I/O Target to which // we'll be sending the Request. // status = WdfIoTargetFormatRequestForIoctl(devContext->Target, Request, IoControlCode, NULL, NULL, outputMemory, NULL); if (!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); return; } // // Set a completion callback... we'll be sending the Request async. // WdfRequestSetCompletionRoutine(Request, MyDriverRequestCompletionRoutine, NULL); // // Step 2: Send the Request to the Remote I/O Target // ret = WdfRequestSend(Request, devContext->Target, WDF_NO_SEND_OPTIONS); if (ret == FALSE) { status = WdfRequestGetStatus (Request); WdfRequestComplete(Request, status); }
Note that you can use this pattern for any WDF Request that you’re sending to a Remote I/O Target. In other words, you can use it to send Requests that you receive from a Queue (so called Queue Presented Requests) or Requests that you create in your driver by calling WdfRequestCreate.
Oh, one more thing: If you specify synchronous processing, be absolutely sure you have specified an ExecutionLevel constraint on your WDFDEVICE or WDFQUEUE as WdfExecutionLevelPassive. This is the only time you can use synchronous processing.
The Rules for Sending to a Local I/O Target
The first thing to understand about sending Requests to Local I/O Targets is that you can always use the same pattern that you use for sending to Remote I/O Targets. That is, you can format the Request using WdfIoTargetFormatRequestForXxxx and then send the Request either synchronously or asynchronously with a callback using WdfRequestSend. So, if you want to learn one pattern for formatting and sending Requests, always call WdfIoTargetFormatRequestForXxx and WdfRequestSend for synchronous or asynchronous processing, and you’ll always be right.
While you could stick with always using the same pattern, there are a few potentially useful optimizations that we should talk about that apply only to sending Requests to a Local I/O Target. The first of these is using _SEND_AND_FORGET. When you send a Request using _SEND_AND_FORGET, you’re effectively telling the Framework to (a) send the Request to the Local I/O Target exactly as you received it and then (b) forget about that Request in terms of any further processing. The send operation is asynchronous. As soon as you call WdfRequestSend, you relinquish ownership of the Request, you do not get a callback when the Request is complete, and you’re relieved of having to complete the Request in your driver.
_SEND_AND_FORGET is primarily useful for filter drivers that want to send along Requests that they receive from a Queue but do not need to process. For example, let’s say you have a filter driver that’s filtering IOCTLs. Your driver is interested in one specific IOCTL Control Code. If you get an IOCTL that doesn’t have the Control Code you’re interested in, you just want to pass it down the Device Stack to your Local I/O Target. You don’t care if the driver below you completes the Request successfully. You don’t ever want to see the Request again. You just want to pass the Request along, just as you received it.
Because of the way it’s typically used, _SEND_AND_FORGET allows the further optimization that you do not have to format the Request before sending it. It “just knows” to pass along the same Request parameters to your Local I/O Target that were passed into your driver. In fact, you cannot call WdfIoTargetFormatRequestForXxxx if you use _SEND_AND_FORGET. You can see an example of using the _SEND_AND_FORGET option in Figure 2.
// // This control code is not interesting to us. Just send the Request // down to the next driver in the device stack. No formatting required! // WDF_REQUEST_SEND_OPTIONS_INIT(&sendOptions, WDF_REQUEST_SEND_OPTION_SEND_AND_FORGET); status = WdfRequestSend(Request, WdfDeviceGetIoTarget(myDevice), &sendOptions); if (status == FALSE) { status = WdfRequestGetStatus (Request); WdfRequestComplete(Request, status); }
_SEND_AND_FORGET is the lowest overhead way of using WdfRequestSend to send along a WDFREQUEST that you’ve received from a Queue to an underlying device and driver (your Local I/O Target) without modifying any of the Request parameters. Oh, by the way, you can only use _SEND_AND_FORGET with Requests that you got from a Queue. It won’t work with Requests that you create in your driver using WdfRequestCreate.
A second optimization that you may wish to consider is the use of WdfRequestFormatRequestUsingCurrentType. You can use this function in many of the same cases where you might otherwise use _SEND_AND_FORGET but you want to specify either synchronous or asynchronous processing with a callback for your call to WdfRequestSend. You can see an example of the use of this function in Figure 3.
// // Pass along the same parameters that we received. // WdfRequestFormatRequestUsingCurrentType(Request); // // Completion callback… We need to know that the Request // succeeded. // WdfRequestSetCompletionRoutine(Request, FilterRequestCompletionRoutine, WDF_NO_CONTEXT); // // Send the Request and don't wait for it to be completed // ret = WdfRequestSend(Request, Target, WDF_NO_SEND_OPTIONS); if (ret == FALSE) { status = WdfRequestGetStatus (Request); WdfRequestComplete(Request, status); }
Note that it’s also possible to call WdfRequestFormatRequestUsingCurrentType prior to sending a Request with WdfRequestSend specifying _SEND_AND_FORGET. While it may be architecturally valid as far as the Framework is concerned, doing this doesn’t make a great deal of sense. Remember, _SEND_AND_FORGET automagically does the formatting, passing along the parameters sent to it. And it does it more efficiently than separately calling WdfRequestFormatRequestUsingCurrentType, too.
In Summary
I hope you agree that if you view the rules in this way, it’s quite easy to be sure you’re using I/O Targets and WdfRequestSend the right way. In summary:
Remote I/O Targets
- Always format the Request being sent for the specific I/O Target using a function that starts with the characters WdfIoTarget, such as WdfIoTargetFormatRequestForXxxx. You may not use WdfRequestFormatRequestUsingCurrentType.
- Always call WdfRequestSend to send the Request using either synchronous or asynchronous processing. You may not use _SEND_AND_FORGET.
- If you use synchronous processing, be sure you’ve established a WdfExecutionLevelPassive constraint for your Device or Queue.
Local I/O Targets
- You may use any of the methods listed under Remote I/O Targets with Local I/O Targets. Methods for Remote I/O Targets will always work.
- As an optimization, if you want to send a Request you received from one of your WDF Queues to your Local I/O Target, and you do not want to change any of the parameters in the Request, and you do not need to see the Request after it is completed, you may choose to call WdfRequestSend using the _SEND_AND_FORGET option. This will send the Request to your Local I/O Target and effectively complete it from the viewpoint of your driver. If you do this, do not format the Request.
- Another possible optimization, if you want to pass along a Request that you received from one of your WDF Queues to your Local I/O Target and you do not want to change any of the parameters in the Request, but you do want to see the Request after it is completed, you may choose to format the Request using WdfRequestFormatRequestUsingCurrentType, and then call WdfRequestSend specifying either synchronous or asynchronous processing. Once again, if you use synchronous processing, be sure you’ve established a WdfExecutionLevelPassive constraint for your Device or Queue.