Programming the Microsoft Windows Driver Model Second Edition
Updates & Errata
Revised December 8, 2004

 

Note: Technical corrections or additions are in BLACK text, with corrected contents shown in RED. Merely stylistic or typographical corrections are in GREEN text.

p. xxi Where to get sample files [12/26/02]:

The sample files are not available from www.microsoft.com as stated here. You must install them from the CD. If your CD is damaged or missing, follow the support link from http://www.microsoft.com/mspress/books/6262.asp.

p. xxv DDK requirements for samples [12/26/02]:

SP-2 to the sample drivers incorporates a solution to the USBD.LIB compatibility problem based on a special import library in the WIN98 directory. It is once again true that the Windows 98 DDK is only needed for the PNPMON sample driver. Thanks to Alexander Grigoriev.

p. 7 Contents of NTDLL.DLL [05/18/03]:

The third sentence of the second paragraph in the sidebar should read: “Most of the entries in this DLL are thin wrappers around calls to kernel-mode functions that actually perform native API functionality.” Thanks to Maxim Shatskih.

p. 22 Missing character [12/26/02]:

The third line of the code sample at the bottom of the page should read:

printf(“Hello, world!\n”);

p. 34 Driver image mapping [05/18/03]:

Don’t read too much into my use, in the fifth paragraph on this page, of the phrase “file mapping” to describe how drivers are mapped into virtual memory. The memory manager closes the .SYS file, which has the important side effect of allowing you to replace the .SYS file while the driver is running. Thanks to Maxim Shatskih.

p. 38 Name that key [12/26/02]:

In the third line of the paragraph following the heading “Order of Driver Loading”, replace “device key” with “hardware key”.

p. 40 Figure labeling [12/26/02]:

Box number 3 in Figure 2-7 should contain “Function driver” (singular, not plural).

p. 49 Other common data types [05/18/03]:

Table 2-1 could usefully mention the LONGLONG and ULONGLONG types, which are typedef names for the native data types __int64 and unsigned __int64, respectively. Thanks to Maxim Shatskih.

p. 53 Meaning of DO_EXCLUSIVE [12/26/02]:

The DO_EXCLUSIVE flag means that only one handle can be open at a time. The statement in the text implies that a single thread might be able to open multiple handles to a DO_EXCLUSIVE device, which is not the case. See also the discussion of the corresponding parameter to IoCreateDevice on p. 62.

p. 57 DriverUnload is required [12/26/02]:

The text doesn’t emphasize strongly enough that you must have a DriverUnload routine in a WDM driver because the system can’t unload a driver that doesn’t have one.

p. 57 Wrong font in list [12/26/02]:

In the fourth bullet, MajorFunction should be in bold face.

p. 58 IRP_MJ_SYSTEM_CONTROL support is required [12/26/02]:

A WDM driver must support IRP_MJ_SYSTEM_CONTROL requests, at least to the extent of passing them down the PnP stack. The driver verifier checks for this.

p. 85 Where's that flag again? [12/08/04]:

The DO_DEVICE_INITIALIZING flag is in the device object, not the driver object (see the second line after the heading "Clear DO_DEVICE_INITIALIZING"). Thanks to Chris Rhodes..

p. 98-99 Performance impact of try/finally [12/26/02]:

It’s expensive to exit from a __try block with a return statement as shown in the example that spans these two pages. Microsoft’s Pre-FAST tool will flag such usages.

p. 102 NULL pointers in DOS virtual machines [05/18/03]:

Low virtual addresses are valid in an NTVDM (Virtual DOS machine) process. Thanks to Maxim Shatskih.

p. 118 Where’s the tag? [02/04/03]:

When you allocate less than a page of memory with ExAllocatePoolWithTag, and when pool tagging is enabled in the kernel, the tag appears in the four bytes immediately preceding the memory block addressed by the returned pointer. When you allocate a page or more, the pool manager keeps track of the tag internally; it does not appear in memory. Thanks to Gennady Mayko.

p. 119 Wrong quotation style [12/26/02]:

The first line on this page should read:

#define DRIVERTAG 'KNUJ'

p. 129 Typo in sample [06/20/03]:

The code at note 3 should read:

PSINGLE_LIST_ENTRY psLink = PopEntryList(&SingleHead);

Thanks to Matthew Giedt.

p. 134 Definitely rude [12/14/02]:

The text should read as follows (missing characters):

In latin script—tlhonchaj chIljaj motlhmoHwI’pu’ — “May the standard makers lose their nostrils.” Thanks to Marc Reinig. Font available at http://www.kli.org.

p. 137 Sample needs wide-character constant [12/08/04]:

The code fragment at the bottom of the page should read as follows:

if (bArriving)
  RtlInitUnicodeString(&foo,
L"Hello, world!");

Thanks to Rusty Lai.

p. 145 Deleting a registry value [05/18/03]:

The first code fragment on the page should use ZwDeleteValueKey, the function referred to in the text, instead of RtlDeleteValueKey, which doesn’t even exist. Thanks to Maxim Shatskih.

p. 150 Accessibility of SystemRoot [05/18/03]:

The SystemRoot directory is not accessible to boot devices. Thanks to Maxim Shatskih.

p. 153 Additional DbgPrint formatting code [12/14/02]:

You can use the additional formatting code Z in DbgPrint (or KdPrint) calls to display UNICODE_STRING values. E.g.:

UNICODE_STRING foo;
RtlInitUnicodeString(&foo, L”Hello, world!”);

KdPrint((DRIVERNAME “ - %wZ\n”, &foo));

Don’t forget that you must be running at an IRQL less than DISPATCH_LEVEL to display UNICODE strings.

pp. 167-68 Wrong font [12/26/02]:

All of the timestamps in the examples on these pages and on p. 173 should be of the form “t” followed by a subscript number. E.g., t1, t2, etc.

p. 176 How to initialize an in-stack queued spinlock [04/12/03]:

Sixth line of first paragraph: You use KeInitializeSpinLock, just like with regular spin locks. Thanks to David Lavo.

pp. 181, 184 Confusing word choice [12/26/02]:

The DDK uses the phrase “side effect” to describe operations that the system performs coincident with placing a dispatcher object into the signalled state. My editors substituted the wishy-washy phrase “operations”, even though I pleaded with them to leave the DDK terminology in place.

p. 193 Missing word [12/26/02]:

In the 5th prose paragraph, insert “the” to make phrase read “the purpose of the extra parameters”.

p. 203 Missing word [12/26/02]:

In the last sentence on the page, insert the word “arise” to make the line begin “can arise if you use”.

p. 204 Typo in sidebar [05/18/03]:

The sidebar should refer to ExAcquireFastMutex. There is no such function as KeAcquireFastMutex. Thanks to Maxim Shatskih.

p. 208 Wrong comment [12/26/02]:

In the last code sample, the “don’t do this” comment should begin with a less-than: “<== don’t do this”.

p. 229 Name that DDI [12/26/02]:

The code snippet after the note should read:

ObDereferenceObject(FileObject);

p. 232 Misplaced code flag [12/26/02]:

Code flag 3 should be next to the line reading “return STATUS_Xxx;”

p. 232 Undefined variable [05/18/03]:

In the same code fragment as the previous note, device should instead read fdo. Thanks to Maxim Shatskih.

p. 233 ff. Awkward word choice [12/26/02]:

Driver programmers use the verbs “fail” and “succeed” transitively to describe completing an IRP with a failure or a success status, respectively. My editors nonetheless insisted on substituting the awkward phrases “cause to fail” and “cause to succeed” throughout the book, starting here.

p. 236 When do you get an MDL? [02/21/03]:

In the fourth line of the second bullet point at the top of this page, replace DO_BUFFERED_IO with DO_DIRECT_IO. Thanks to Jonathan Taylor.

p. 238 Misdrawn figure[12/26/02]:

In Figure 5-6, the box labeled “Filter driver completion routine” in the right-hand part of the drawing should connect to the “CompetionRoutine” box in the second IO_STACK_LOCATION. The third stack location is fallow, which is the whole point of using IoSkipCurrentIrpStackLocation.

p. 240 Hum a few bars and I’ll fake it [12/26/02]:

A reader who wanted (but failed) to stay anonymous savaged the entire first edition of the book in an online review because there was no explanation of what it means to “fake” an interrupt. I mean calling the interrupt service routine (ISR) as a subroutine in such a way that the ISR performs the same processing it would do for an equivalent hardware interrupt. The PIOFAKE sample on the disc does this from the TransferFirst subroutine.

p. 253 Awkward word choice [12/26/02]:

In the fourth line of the last full paragraph on the page, my editors insisted on using the ambiguous verb “post” instead of the verb “pend”. “Post” is file-system-driver slang for what happens to an IRP when you return STATUS_PENDING from a dispatch routine. I’ve never seen “post” used in this sense in any device driver, and I don’t like it because it gives no sense of what the IRP is “posted” on, or to, or whatever. I wrote “pend”, which I think better describes what’s going on.

p. 264 Extraneous label in figure [12/26/02]:

Pretend that the label “Queue IRPs” and the arrow that connects it to the I/O thread was never there.

p. 264 Wrirting is not a word [12/27/02]:

The fifth line of the note should read “modified while I was writing thsi buk….”

p. 269 Alternative to CSQ for IRP parking [12/26/02]:

Chapter 9 discusses an IRP caching scheme that I find preferable to the parking technique described here. GenericCacheControlRequest and GenericUncacheControlRequest deal with the caveats mentioned here, and they are also closely (and automatically) tied to PNP, POWER, and CLEANUP code.

p. 275 Missing return [12/08/04]:

There should be a "return;" statement at the end of the page. Without it, the code attempts to release the spin lock twice. Thanks to Dave Probert.

p. 277 Concerning anthropomorphism [12/26/02]:

Most programmers, including me, personify their programs. In the fourth line of the fifth bullet, I’m pretty sure I wrote “nobody” to describe the entity that hasn’t changed the cancel routine address for the IRP. A gremlin changed it to “nothing”.

p. 279 Missing word [05/18/03]:

The first sentence of the first bullet point in the list should refer to the “global cancel spin lock.” Thanks to Maxim Shatskih.

p. 296 Redundant statement [09/04/03]:

The first statement in the ForwardAndForget subroutine (pdx = etc.) should be deleted. Thanks to David Lavo.

p. 302 Wrong function call in code sample [05/18/03]:

The code sample should be calling IoBuildSynchronousFsdRequest, which is the whole point. Thanks to Maxim Shatskih.

p. 304 Wrong function call in code sample [05/18/03]:

The code sample should be calling KeInitializeEvent. Thanks to Maxim Shatskih and Brian Kenn.

p. 291 Misspelled major function code [12/08/04]:

In the first line, IRP_MJ_DISPATCH_CLEANUP should be IRP_MJ_CLEANUP. Thanks to Takin Nili-Esfahani.

p. 316-17 Concerning the intelligence of readers [12/26/02]:

I know you realize what PCI and PCMCIA mean! My editors appear to have been concerned that you might have forgotten, so they inserted definitions of these and many other acronyms the first time they appear in a chapter. Next time, I’ll suggest we put a glossary at the end of the book so as to avoid talking down to you readers. And don’t get me started about the Microsoft® Windows® XP brand of operating system for personal computers….

p. 321 DEVQUEUE states [12/26/02]:

Figure 6-3 should show an extra state, “Stalled and Rejecting”, between the Stalled and Rejecting states. The point is that the stall and abort operations are independent and establish different conditions, either of which affects the flow of IRPs into and out of the queue.

p. 330 Wrong function argument [12/08/04]:

In the fifth line from the bottom, replace pdx->LowerDeviceObject with fdo, so that the line reads:

DefaultPnpHandler(fdo, Irp);

Thanks to Niels Jensen.

p. 336 More about IRP_MJ_SHUTDOWN [06/12/03]:

In the third paragraph of the sidebar, replace the last sentence, which begins “The DDK doesn’t say so . . .” with this:

(While we’re on the subject, note these additional details about IRP_MJ_SHUTDOWN. Like every other IRP, this one will be sent first to the topmost FiDO in the PnP stack if any driver in the stack has called IoRegisterShutdownNotification. Furthermore, as many IRPs will be sent as there are drivers in the stack with active notification requests. Thus, drivers should take care to do their shutdown processing only once and should pass this IRP down the stack after doing their own shutdown processing.)

p. 352-53 DBT_What? [12/08/04]:

On both these pages, DBT_QUERYREMOVEDEVICE should read DBT_DEVICEQUERYREMOVE instead. Thanks to Kyonho Park.

p. 365 Handling unexpected resources [12/26/02]:

Some bus drivers attach private resource descriptors to the list. It would have more clear to put a default label into the switch on resource->type to emphasize that you shouldn’t do anything in particular with these private resources. You should detect unexpected port, memory, interrupt, or DMA resources, because their presence may indicate that you’ve been loaded for the wrong hardware somehow.

p. 366 Order of resources [12/26/02]:

I’ve now been authorized to tell you that your I/O resources will appear in BAR order for a PCI device, on all platforms. Thus, if you have multiple resources of the same type and size, you can safely rely on their descriptors appearing in a predictable order.

pp. 373-78 Handling of port resources [06/12/03]:

Microsoft has changed the rules for handling CmResourceTypePort I/O resources on non-x86 platforms. You must now program your driver to work when you get either a CmResourceTypePort or a CmResourceTypeMemory resource for what you thought was a “port” type resource. The CM_RESOURCE_PORT_IO flag referred to on p. 376 is now obsolete. Refer to the article on this subject in the July 15, 2003, issue of WD-3 for more information and for suggestions about coding techniques.

You can avoid difficulty by always using memory-type registers in your hardware, in which case you will always get a CmResourceTypeMemory resource.

p. 406 When AllocateAdapterChannel returns [12/26/02]:

When the resources needed for a DMA operation are immediately available, AllocateAdapterChannel allocates them and calls your adapter control routine before returning. When the resources are not immediately available, AllocateAdapterChannel queues your device object and returns. The call to your adapter control routine happens later.

p. 407, 409 Fifth parameter to MapTransfer is a PULONG [12/08/04]:

The fifth parameter to MapTransfer is a PULONG. Accordingly, the fragments on pages 407 and 409 should be calling the function with &pdx->xfer as the argument. Thanks to Phil Elwell.

p. 414 When GetScatterGatherList returns [12/26/02]:

When the resources needed for a DMA operation are immediately available, GetScatterGatherList allocates them and calls your execution routine before returning When the resources are not immediately available, GetScatterGatherList queues your device object and returns. The call to your execution routine happens later.

p. 437 What other event codes? [12/26/02]:

In the ninth line from the bottom of the page, delete the word “other” so the one complete sentence on this line reads, “There are only three event codes.”

p. 485 New function [12/26/02]:

Beginning in the Server 2003 version of Windows, you can call IoValidateDeviceIoControlAccess to help enforce a more stringent access policy than is defined by the FILE_XXX_ACCESS setting in an I/O control code.

p. 489 Memory access rights [12/26/02]:

The driver needs write access with METHOD_OUT_DIRECT. I suspect that most NT-capable computers also confer read access to any page that can be written, but such is not required for this buffering method.

p. 495 When to wait [12/26/02]:

The text (lines 7-8 of the 2d paragraph) should tell you to wait on the event when the target driver returns STATUS_PENDING. The Caution that appears further down the page elaborates when you should and should not wait.

p. 503 Correction to code sample [12/26/02]:

The sixth-from-last line in the code sample on this page should read:

PIRP* pIrp = (PIRP*) Irp->Tail.Overlay.DriverContext[0];

That is, the pIrp variable is the address of a variable in which we earlier saved a pointer to the IRP we’re canceling. (The code inside GENERIC.SYS reads this way, as it should.)

p. 512 When to call IoWMIRegistrationControl [06/12/03]:

In Windows 2000, boot drivers should defer calling IoWMIRegistrationControl to register with the WMI subsystem until after they process IRP_MN_START_DEVICE for the first time. Running WMI initialization code before the I/O subsystem is completely initialized will lead to system crashes. Beginning with Windows XP releases, the operating system internally queues WMI registration calls until they can safely be performed.

Generally speaking, it does not hurt to move the registration call to the StartDevice routine in any driver for any platform. You mustn’t, however, register more than once because the checked build will ASSERT if you do. Thus, any registration call you place into StartDevice should be conditional so that you don’t mistakenly register a second time coming out of a PnP STOPPED state. Consult the chapter 10 samples, as modified by SP-3, for examples of how to do this.

p. 521 Correction to code sample [02/04/03]:

The tenth line of the SetDataBlock routine should read:

if (instindex != 0)

That is, the expression involving the undefined variable instcount should not be there. Thanks to Dave Matheny.

p. 523 Misplaced paragraph [12/26/02]:

The 2d paragraph on this page was actually supposed to be a note on p. 516.

p. 534 WDMSTUB isn’t a VxD [12/26/02]:

WDMSTUB.SYS is a WDM lower filter driver, rather than a VxD. In addition, versions beginning with 5.0.0.5 (which was part of SP-1 to this 2d edition) stub WmiSystemControl and WmiCompleteRequest in Windows 98 Gold only. This allows a driver to load which references these two WMILIB routines, since WMILIB is not available for that old platform. It remains true that WMI functionality would not work on Windows 98 Gold, of course.

p. 553 Reference the what? [12/26/02]:

In the last sentence before the subheading, replace “interface” with “device object”. That is, you should call ObReferenceObject to take an extra reference to your own device object in order to pin your driver in memory while your caller has an outstanding reference to the interface you’ve exported.

p. 558 Another IRP to support [12/26/02]:

A controller or multifunction driver must also handle IRP_MJ_SYSTEM_CONTROL requests. Wearing the PDO hat, complete these requests with whatever status is already in the IRP. Wearing the FDO hat, at least pass them down the PnP stack. The Verifier makes sure you do these things by sending you an IRP with a bogus minor function code during your startup processing.

p. 567 Upper limits on endpoint packet sizes [12/26/02]:

All the less-than signs in Table 12-2 should be less-than-or-equal signs. (In fact, they were in the manuscript.) That is, the upper bound for a high-speed bulk endpoint is 512 bytes and not 511 as implied by the printed table.

p. 571 Strictly speaking… [12/26/02]:

The second sentence in the second full paragraph should say, “Strictly speaking, endpoints belong to interfaces….”

p. 578 Maximum polling interval for high-speed devices [02/04/03]:

The largest polling frequency for a high-speed endpoint is 32 microframes, or every 8 milliseconds. Thanks to Michael Luloh.

p. 584 Example of multifunction device [12/26/02]:

A better example of a multifunction device would be a keyboard that has a physically integral SmartCard reader. (If I were designing a keyboard/mouse device, I’d use a single HID interface with multiple reports.)

p. 588 Upper limits on data [12/26/02]:

The less-than signs in Table 12-7 should all be less-than-or-equal signs. (In fact, they were in the manuscript.)

p. 591 Use of UsbBuildVendorRequest [12/26/02]:

In Table 12-8, note that we use UsbBuildVendorRequest for both vendor and class control requests.

p. 602 MaximumTransferSize [02/15/03]:

Windows XP and later systems will ignore the MaximumTransferSize member of the USBD_PIPE_INFORMATION structure. Thus, if your driver never has to run on an earlier system, you can dispense with the multisegment logic discussed on pp. 605 ff. for LOOPBACK.

p. 607 What memory to release [05/18/03]:

The eighth line of the first full paragraph should read “releases the memory occupied by the URB . . ..” Thanks to Maxim Shatskih.

p. 643 Wrong global item [01/03/03]:

The text in the third line from the bottom of the page should refer to USAGE_MAXIMUM instead of LOGICAL_MAXIMUM.

pp. 646-47 More about DevicesArePolled [01/06/03]:

There is a bug in all current versions of Windows that will cause a system lockup during startup of a HID minidriver that (a) has two or more input collections in its report descriptor, and (b) sets DevicesArePolled to TRUE.

p. 662 Completing IOCTL_HID_READ_REPORT synchronously [12/27/02]:

If you have set the DevicesArePolled flag to FALSE (as is usually recommended) and complete an IOCTL_HID_READ_REPORT synchronously (i.e., in the dispatch routine), versions of HIDCLASS prior to XP will send you another READ_REPORT from the completion routine. If you keep completing IRPs, you’ll eventually overflow the stack. This can be a particular problem for a device of the second type listed in the bullet list here, in that you might have a large number of reports saved up from device interrupts. In a similar situation, I coded my READ_REPORT handler not to complete the IRP in a recursive-call situation.

p. 663 IOCTL_HID_WRITE_REPORT does not use METHOD_BUFFERED [12/08/04]:

The text doesn't correctly describe IOCTL_HID_WRITE_REPORT. First of all, HIDCLASS does not send this IOCTL when it gets an IOCTL_HID_SET_OUTPUT_REPORT--it sends an internal IOCTL with the code IOCTL_HID_SET_OUTPUT_REPORT instead. The buffering method for IOCTL_HID_WRITE_REPORT is METHOD_NEITHER, but the parameters are presented in a strange way. Namely, Irp->UserBuffer is the address of a HID_XFER_PACKET structure, and stack->Parameters.DeviceIoControl.InputBufferLength is the length of that structure. (Normally, a METHOD_NEITHER IOCTL uses stack->Parameters.DeviceIoControl.Type3InputBuffer to point to the input buffer).

p. 667 Missing ampersand [12/08/04]:

The fourth line of the code fragment should read:

&stack->etc.

p. 684 Too many arguments [12/27/02]:

The last sentence on the page should begin “The next two arguments….”

p. 688 Extraneous remove-lock release [02/04/03]:

The call to IoReleaseRemoveLock that appears in the third line of the code sample on this page should not be there. Thanks to Martin Jansen.

p. 691 Mistake in code sample [09/26/03]:

The Callback routine shown at the top of the page should read:

VOID Callback(PDEVICE_OBJECT fdo, PRANDOM_JUNK stuff)
  {
  PAGED_CODE();
  . . .
  IoFreeWorkItem(stuff->item);
  ExFreePool(stuff);
  }

Thanks in part to James Gatt.

p. 692 Missing parenthesis [12/08/04]:

The call to InterlockedIncrement in the last code fragment on the page is missing a right-parenthesis. Thanks to Aviv Blattner..

p. 738 What do you mean by “is”? [12/27/02]:

The last sentence of note 3 should begin “I included the installer so I could….” (no “is”).

p. 745, 752 Note caption [12/27/02]:

The caption of the note boxes on these pages should be just “Note” instead of “On the CD”.

p. 771 Another INF incompatibility [07/03/03]:

Windows 98 (and probably Windows Me as well) will not strip a leading backslash from the pathname in a DestinationDirs specification. Thus, the following construction:

DefaultDestDir=10,\System32\Drivers ; ç don’t do this

Causes an error in 98/Me (but not in 2K or later systems) because the setup program tries to use, e.g., C:\Windows\\System32\Drivers as the directory name. So leave out the leading backslash. Thanks to Mark Giese.

p. 780-81 What device type to use in a filter driver [06/12/03]:

Except in Windows 2000, filter drivers should always specify FILE_DEVICE_UNKNOWN as the device type in the call to IoCreateDevice. The file system bug referred to in the text was fixed in XP. Using a disk device type causes IoCreateDevice to create a VPB, and unneeded VPBs will leak because no one will ever destroy them.

SP-3 for the sample drivers contains a revised FILTER sample incorporating this idea.

p. 782 Revised explanation of DO_POWER_PAGABLE [06/12/03]:

The true reason for the DO_POWER_PAGABLE flag is this: the power manager needs to power-down devices with pageable power handlers first, before powering-down dvices with nonpageable power handlers. For this to be possible, all the drivers in a particular PnP stack need to have the same setting for the DO_POWER_PAGABLE flag.

p. 786 There is no USAGE_NOTIFICATION race [06/12/03]:

The race condition alluded to in the sidebar should never happen because no other driver should be attaching to our device stack once the stack is built. Accordingly, the sidebar should just be stricken.

p. 789 Wrong registry key [12/08/04]:

The CLASSKEY string should be defined as follows:

CLASSKEY=System\CurrentControlSet\Control\Class\{<class-GUID>}

Thanks to Werner Frischauf.

p. 794 Undeclared variable [12/27/02]:

A line about two thirds of the way down in the AddDevice sample should read:

RtlInitUnicodeString(&edoname, namebuf);

p. 794 Incomplete example [04/29/03]:

The example illustrating how to create an Extra Device Object is a little too terse, leading people to generate code that doesn’t entirely work. Just before the ellipsis at the very end, I suggest the following additional code:

edo->Flags &= ~DO_DEVICE_INITIALIZING;

In other words, you still have to clear DO_DEVICE_INITIALIZING, as with other device objects, to allow applications to open handles.

p. 800 Wrong character [12/08/04]:

In the last line of the value for UpperFilters, replace the seventh parameter (63) with 73. Thanks to David Lavo.

p. 804 Wrong library name [05/18/03]:

In the third line of the third paragraph, SCIS.SYS should read SCSIPORT.SYS. A service pack will correct the actual code sample, too. Thanks to Maxim Shatskih.