Wizards Simplify Windows NT Kernel-Mode Driver Design

Ruediger R. Asche
Microsoft Developer Network Technology Group

October 17, 1995

Click to open or copy the files in the NTDDWZD sample application for this technical article.

Abstract

A custom AppWizard is a new feature in the Microsoft® Visual C++™ version 4.0 development environment that helps build application skeletons. Although AppWizard extensions were mainly designed for Microsoft Foundation Class Library (MFC) applications, they can be used to build skeletons for any code project, in any language. The first part of this article discusses the custom AppWizard architecture in general, and the second part describes a custom AppWizard that generates skeletons for Windows NT™ kernel-mode device drivers. The companion article "Using the Windows NT Custom Driver Wizard" (also in the Microsoft Development Library) describes how to put the wizard to use.

Disclaimer: This article is part of a series of articles about writing and debugging Windows NT kernel-mode device drivers using Visual C++. Please be aware that the material covered in this article series is rather experimental. If you have any questions about the material in this article, or in any other article in this series, do not contact Microsoft Product Support Services. Please e-mail me directly (ruediger@microsoft.com) or contact the Microsoft Developer Network.

Introduction

In my book, there are very roughly two steps in coding software. The first step is boring, mechanical, and annoying: You build the basic application into which you will later embed functionality. In this step, you generally cut and paste a lot of code from existing applications, customize that code, run the whole thing through the compiler, fix the syntax and declaration errors that you made, and repeat these steps until your project compiles without errors. There is usually nothing exciting about this step, and if you are lucky, you can find a big chunk of off-the-shelf code that you can recycle more or less painlessly to do this work.

The second part—adding the code that implements what your software is supposed to do—is more challenging, because here you begin to build something new: You actually design the new functionality into the application. (Sounds cool? Well, in reality, you end up fixing run-time errors instead of syntax errors now. Oh well...) In an ideal world, the second step is where 98% of the work you put into the project should go.

I find the discrepancy between these two steps particularly annoying when it comes to device drivers, where the tricky work usually occupies only a very small percentage of the code, and most of the code is pure bookkeeping. Take kernel-mode drivers in the Microsoft® Windows NT™ operating system: Typically, most of the work that goes into the DriverEntry routine is registry interaction, setting up interrupt service routines (ISRs), creating device objects, allocating and deallocating memory, and so on. At this stage, a single I/O request has yet to take place, and what’s worse, there is very little in the bookkeeping code that requires intelligence. It’s all mechanical, repetitive stuff such as "initialize a Unicode™ string to a certain length, copy something into it, append something you found in the registry, pass the string to a system call, and then nuke the string, but make sure that nothing fails anywhere along the way."

One way to simplify this procedure is to encapsulate some of the data structures into C++ objects, and provide a simple interface to the driver designer. I explored this approach in the article "Writing Windows NT Kernel-Mode Drivers in C++." However, using an object-oriented language for device drivers can be problematic; in particular, memory usage may be less than optimal (as opposed to C, where you have a greater degree of control over system resources).

In this article, I will introduce a different approach to circumnavigating the annoying elements in driver design: I will explain how you can use an AppWizard extension to interactively decide which features the driver is supposed to have, and produce a skeleton driver according to the specifications. This way, you can ask Visual C++™ to create the mechanical pieces of code for you, and then you can plunge right into the interesting pieces of driver design.

What Is a Custom AppWizard?

A custom AppWizard is a new concept introduced in Visual C++ version 4.0. In simple terms, it is a code skeleton generator that is written using the Microsoft Foundation Class Library (MFC). As input, the custom AppWizard takes user specifications for your project. (These specifications consist of an MFC-based set of dialog boxes that you, as the custom wizard writer, design using the Dialog Editor and derivatives of the CDialog class.) Based on these specifications and a set of template files, the custom AppWizard generates the code skeleton, a makefile, and possibly a complete Help file.

The application wizard extension engine was designed primarily to generate application skeletons based on MFC applications. To write custom application wizards for something that is not built upon MFC or that requires special treatment (such as Windows NT kernel-mode drivers), you must sometimes bypass the engine because of shortcomings in the implementation. We will see how this affects us as driver nerds later on. For now, let's see how custom wizards work conceptually.

Input, Output, and All that Jazz

Custom AppWizards are straightforward: Each custom AppWizard consists of a single dynamic-link library (DLL) with an .AWX extension. The custom AppWizard is copied into the TEMPLATE subdirectory of your MSDEV directory. Visual C++ automatically loads all custom wizards when the user selects New/Project Workspace from the File menu, and offers the user a choice of the different workspaces offered by the custom AppWizards.

Generating a skeleton for your custom AppWizard is a piece of cake, because Visual C++ version 4.0 includes—you guessed it right—a custom AppWizard that generates a custom AppWizard for you. From the File menu, select New/Project Workspace, which will display the New Workspace dialog box. From the Type list, select Custom AppWizard. You will be guided through a set of dialog boxes that ask you for specifics on the custom AppWizard you'd like to create. The project files that the custom AppWizard generates are based on template files that you supply; the templates typically contain meta symbols that are used to transform the template into a project file.

The heart of the custom AppWizard engine is a macro processor. In response to the user input in the dialog boxes, your wizard will typically enter variables into a macro dictionary. After the user has completed all the dialog boxes and all variables have been assigned values, the template files are parsed, and the macro processor copies the template files to the new project files, performing modifications based on the variables and on a small set of rules: The dictionary entries are used either to replace variables in the template files with strings, or to conditionally copy sections of text from a template file to a project file. Thus, the dictionary is the "interface" between the macro processor and the custom AppWizard code.

Here's the bad news: The macro processor is not very powerful, which makes writing your extension a little bit awkward. The only statements that the macro processor currently understands are the following:

Literal substitution: If the expression $$variablename$$ is encountered in a template file, it will be replaced with the value that variablename has in the dictionary. A few refinements to variable usage are described under (3) below.

Conditional statements: The sequence $$IF(variablename) text $$ELSE other text $$ENDIF means that either text or other text is copied to the project file, depending on whether variablename has a value assigned in the dictionary (the $$ELSE clause is optional). Nested $$IF/$$ELSE/$$ENDIF statements can be simplified using the $$ELSEIF statement.

Note that the conditional expression in the $$IF clause can only be a variable name. It is not possible, for example, to test whether a variable has a specific value. Thus, to implement mutually exclusive options, you must set aside a single variable entry in the dictionary for each option.

Iterations: The sequence $$BEGINLOOP(variablename) body $$ENDLOOP in a template file requires that variablename be assigned a string that represents a numeric value, for example, "2". The loop is traversed as many times as the number represents. Any occurrence of a variable name as outlined in (1) above within a $$BEGINLOOP/$$ENDLOOP body may reference an index that is implicitly derived from the current loop. Let's look at an example:

$$BEGINLOOP(COUNT)
...
$$THE_VARIABLE$$
...
$$ENDLOOP

In this sequence, when the macro processor encounters $$THE_VARIABLE$$, it will first look for an entry in the dictionary called THE_VARIABLE_xxx, where xxx is a number that matches the current iteration. For example, the first time the body is executed, the processor looks for THE_VARIABLE_0, the second time, it looks for THE_VARIABLE_1, and so on. If the indexed version of the variable is not found, the macro processer queries the dictionary for THE_VARIABLE. If neither is found, the macro processor will abort parsing of the template with a run-time error. The only way for you to make sure that this does not happen is to amend the above fragment as follows:

$$BEGINLOOP(COUNT)
...
$$IF(THE_VARIABLE)
...
$$THE_VARIABLE$$
...
$$ENDIF
...
$$ENDLOOP.

The macro processor is a nice attempt at a programming language, but it has a lot of shortcomings. For example, while you're in a loop, you cannot determine what iteration the loop is currently performing; thus, you must either process all iterations exactly the same way, or define separate variables for each special case. Another problem that arises from the implementation of an implicit loop variable is that nested loops are not possible.

Yet another problem with the macro processor is that it makes no syntax checks in the template files before run time. Thus, if one of your project files contains a number of nested $$IF/$$ELSE/$$ENDIF statements and you fail to test all possible control flows, you might miss a missing $$ENDIF somewhere, and the processor may gracelessly stop processing a source file in a real-life scenario.

Template Files

Where does the macro processor obtain its template files, and how does it know which files to process? Because (as I mentioned before) an AppWizard extension is a DLL, all the template files must be stored somewhere in the DLL, right?

Correct. Each source file that the macro processor can parse and process is represented as a resource in the DLL executable. A new resource type called TEMPLATE has been introduced into the resource format for this purpose. You create the template files and store them anywhere on disk (normally, they go into the TEMPLATE subdirectory in the custom AppWizard project), then you create a new resource of type TEMPLATE and link the resource to your template file with the Property dialog box. The exact steps are outlined in the "Creating Custom AppWizards" section in the Visual C++ User's Guide in the Microsoft Development Library (Product Documentation, Languages, Visual C++ 4.0 [32-bit]). When the resource compiler processes the .RC file, the template file (including the variables and macro processor directives) is copied to the .RES file and eventually incorporated into the DLL.

Note that storing the templates in the DLL has a number of advantages, especially in that you need only one file to implement a custom AppWizard. One disadvantage of this setup is that each time your template files change, you must rebuild the entire DLL. Normally this should not be a big problem, because the macro-based architecture generally requires you to change at least one of the custom wizard source files whenever you modify one of the template files.

One more question remains about the process: How does the AppWizard extension know which template file to process? A user option could be added to simply make one or more template files obsolete for the project, but how do we decide which template to process and, even more importantly, which processed source files will make it into the project?

Here is the way this works: After the user finishes traversing the dialog boxes, one of the very first files that the custom AppWizard processes is a file called NEWPROJ.INF. This file is processed—that is, scanned by the macro processor—exactly like the other files, but NEWPROJ.INF also contains specific directives that tell the AppWizard which template files to process and how. The AppWizard then spits out a makefile based on these directives and processes only the files that NEWPROJ.INF specified.

No Template for Makefiles

This is quite a bummer. We cannot tweak or customize a makefile in a custom AppWizard. All we can do is specify which source files go into the project; everything else in the project is built according to what the AppWizard thinks is right. This lack of control is especially painful for non-MFC projects, because options for these projects frequently differ from MFC options in significant ways. For example, as I described in the article "Writing Windows NT Kernel-Mode Drivers in C++" in the Microsoft Development Library, both the compile and link options for device drivers differ quite a bit from the options for MFC-based projects. For this reason, the makefile that AppWizard generates from the source files, say, for a driver project is totally useless.

To work around this problem, one of the templates I include in the driver project is a "hand-made" makefile that I generated from an existing driver project. I built a device driver project with the options outlined in the article "Writing Windows NT Kernel-Mode Drivers in C++," edited the file manually to replace all references to the source file with a variable symbol that AppWizard understands, and included the makefile as a template in the custom AppWizard.

This strategy works, but it has a number of disadvantages. First, you need to edit a makefile generated by Visual C++ as a text file, which is somewhat dangerous. Second, when your device driver project obtains more source files than are recognized by the generic makefile I provide, you must use Visual C++ to add the new source files to the project. Third, the makefile format might change in the future, so you might have to build another makefile when you upgrade to a new version of Visual C++. And fourth, whenever you generate a skeleton for a new driver, you need to remember to discard (that is, close) the project file generated by Visual C++ and load the one I provide.

Using Wizard Technology for Non-MFC Projects

In this section, I will demonstrate how AppWizard technology can be used to generate skeletons for non-MFC projects. The custom wizard I introduce generates skeletons for Windows NT kernel-mode device drivers. You will probably benefit more from the upcoming discussion if you are familiar with Windows NT device driver technology, but most of the points I raise are applicable to any custom wizard that serves non-MFC projects. If you are interested in device driver technology, you might want to read the article "Using the Windows NT Custom Driver Wizard" before continuing, as some of the following material references the device driver wizard.

To begin with, you need to provide a number of C++ classes for a custom AppWizard. For each dialog box, there will be an object of a class that is derived from CAppWizStepDlg. Along with the corresponding dialog box templates, these classes implement the set of steps a user traverses to define the skeleton of the driver. You will also need to provide one class derived from CCustomAppWiz. All of these classes and a few auxiliary classes are generated for you when you build a custom AppWizard using the Visual C++ custom AppWizard.

Unless your custom AppWizard does something funky (which mine does), the only classes you need to write code for are the dialog box functions. The CCustomAppWiz derived class and the auxiliary classes do "the right thing" for an off-the-shelf custom AppWizard.

As I mentioned earlier, custom AppWizards follow a two-step process: In the first step, the user's input is mapped into a set of macros, which the custom AppWizard uses (in the second step) to transform template files into project files. The assignment of macros in the dictionary takes place in the OnDismiss member function of the CAppWizStepDlg derivatives that you implement for each dialog box. After the user completes the last dialog box, the ProcessTemplate member of your CCustomAppWiz derived class is called for CONFIRM.INF (a text file that confirms the choices made to the user), for NEWPROJ.INF (which determines what files are to go into the project), and for each project file.

Closely related to CCustomAppWiz is the OutputStream class. The parameters to ProcessTemplate include a string buffer (which contains the entire unexpanded contents of the template file to be processed) and an object of the OutputStream class. CCustomAppWiz::ProcessTemplate expands each line of the template file and calls the WriteLine member of the OutputStream object passed to it to write the output to the corresponding template file. By providing a derived OutputStream class, your custom AppWizard can reprocess the output from the macro expander.

Step 1: Gathering Input

As I mentioned before, the OnDismiss member function of each dialog box that you implement is where macros are generally assigned. Let's look at a typical implementation (the AssignValues member function is called from OnDismiss):

BOOL CCustom2Dlg::AssignValues(BOOL bComesFromDialog)
{
   DefineBoolMacro(_T("MACBASYNC"),m_bHasAsynch);
   DefineIntMacro(_T("NUMDEVICES"),m_iNumDevices);
    CButton *cwndControl;
   DefineBoolMacro(_T("MACBHASIOCTL"),FALSE);
   for (int i=0; i<MAXOPERATIONS-2; i++)   // RAc
   {
    if (bComesFromDialog)
    {
     cwndControl = (CButton *)GetDlgItem(g_Operations[i].iId);
      if (cwndControl->GetCheck()==1)
     {
      DefineStringMacro(_T("OPERATION"),i,g_Operations[i].Name);
      DefineStringMacro(_T("IRPCODE"),i,g_Operations[i].FullName);
      };
     if (g_Operations[i].iId == IDC_OPERATIONIOCTL)
     {
      DefineBoolMacro(_T("MACBHASIOCTL"),i,TRUE);
      DefineBoolMacro(_T("MACBHASIOCTL_ALL"),TRUE);
     };
    }; // if (bComesFromDialog)
    DefineIntMacro(_T("OPINDEX"),i,i);
   }; // for loop
    DefineIntMacro(_T("NUM_OPS"),MAXOPERATIONS-2);
   DefineIntMacro(_T("NUM_OPSNOIOCTL"),MAXOPERATIONS-3);
    if (bComesFromDialog)
   {
    cwndControl = (CButton *)GetDlgItem(IDC_OPERATIONUNLOAD);
    if (cwndControl->GetCheck()==1)
     DefineBoolMacro(_T("MACBHASUNLOADROUTINE"),TRUE);
    cwndControl = (CButton *)GetDlgItem(IDC_OPERATIONSTARTIO);
    if (cwndControl->GetCheck()==1)
     DefineBoolMacro(_T("MACBHASSTARTIO"),TRUE);
   };
// Now register the number of extra steps...
    if (bComesFromDialog)
     NtDDWzdaw.RegisterNewSteps(m_iNumDevices-1);    
    return TRUE;   // Return FALSE if the dialog box shouldn't be dismissed.
}

Without really understanding the ins and outs of the code fragment above, you can see that the most important task in this routine is to set a number of variables in the dictionary. A macro is conveniently defined with one of the following functions: DefineBoolMacro, DefineStringMacro, or DefineIntMacro. The DefineBoolMacro/DefineIntMacro/DefineStringMacro function family is not built into the custom AppWizard engine; rather it is a rather small but extremely useful wrapper around the dictionary. I copied and pasted these functions from the custom AppWizard source code provided as sample code with Visual C++. Note that each function comes in several overloaded variations; in particular, you can define every macro with an optional integer as the second parameter. A macro defined in this way will be an indexed variable, as we discussed earlier. For example:

DefineStringMacro(_T("THE_VARIABLE"),1,"value")

will assign the string "value" to the variable THE_VARIABLE_1.

Note also that OnDismiss is called every time the dialog box is dismissed; that is, it is called whenever Back, Next, or Finish is selected from the control set for the dialog box. So, if the user changes her mind about some of the options and skips between dialog boxes to correct previous selections, OnDismiss is called again each time she dismisses a dialog box. When implementing OnDismiss, you need to undefine macros for selections that may have changed, as demonstrated in the call to DefineBoolMacro(_T("MACBHASIOCTL"),FALSE) above. If this call didn't exist, the user could select IOCtl support in a first pass through the dialog box, but if she cleared the selection in a second pass, the macro would still be defined.

There is one nasty little problem with defining the macros in the OnDismiss member function: The user can always skip dialog boxes by clicking Finish instead of Next. If that happens, the OnDismiss member functions of subsequent dialog boxes are not called, and consequently, some macros do not get defined. So where would be a good point to define default values for those macros? We could set default values for all of the dialog boxes "globally" before displaying the first dialog box, but then any changes in the set of macros for a particular dialog box would also require changes to the default setting routine. It is good practice to keep all the code that associates macros with options in the same place, namely, in the dialog box routine in which the options are processed.

It would be nice if CAppWizStepDlg had a virtual SetDefaults member function that the dialog boxes could use to assign default values to macros. Because the custom AppWizard engine calls the CAppWizStepDlg derivatives directly, we cannot derive our dialog boxes from an intermediate class that derives from CAppWizStepDlg and that implements a SetDefaults member function. 

My solution to the problem is to provide an AssignValues member function in each dialog box. The responsibility of this function is to define the macros depending on the current values of the member variables. AssignValues is called both from within OnDismiss and from the constructors.The constructors of the dialog boxes will have defaults assigned to their member variables according to the user's specifications, so AssignValues will pick up the values and translate them into the appropriate macros as soon as the object is created.

Dynamically Changing the Number of Steps

When you generate a custom AppWizard using the custom AppWizard wizard, the number of steps (dialog boxes) will be fixed. For my device driver wizard, I wanted to have a dynamically adjustable number of steps, because the user can specify the number of devices to be created, and each device should have its own copy of the device configuration dialog box. To dynamically adjust the number of dialog boxes, you manipulate the CDialogChooser class that the custom AppWizard wizard generates for you. CDialogChooser is responsible for switching back and forth between dialog boxes: By default, the class constructor builds an internal array of dialog boxes, and the Back and Next member functions walk the array back and forth. Having CDialogChooser generate new dialog objects dynamically and changing the Back and Next functions to select an appropriate dialog box from the dynamically created list allows us to change, at run time, the number of dialog boxes that the user traverses before the project files are generated.

Keep in mind that the custom AppWizard will disable the Next button on the dialog box it considers to be the last, and disable the Back button on the dialog box it considers to be the first. You can use the global SetNumberOfSteps function to let the wizard know how many dialog boxes there are altogether.

In my project, I have the wizard create multiple instances of the same dialog box at run time. To distinguish between the instances of the dialog box, I overload the constructor of my dynamic dialog class to accept a variable of type int as a parameter. The value passed at construction time is unique for each dialog box. Later on, this value will be used in the OnDismiss member function of the corresponding dialog class to define macros with unique names.

Step 2: Producing Output

As I mentioned earlier, the macro processor lacks some of the power of programming languages such as C, which makes a few things awkward to implement. One of the most difficult problems to overcome involves the missing iteration count: Within a $$BEGINLOOP/$$ENDLOOP statement, the current iteration count cannot be retrieved, so it is difficult to expand the same body of template code more than once and yet distinguish between the different iterations. Let me demonstrate this with an example: In the device driver wizard, the DeviceIOControl function needs to be implemented the same way as the other major dispatch functions; thus, the code for DeviceIOControl is embedded in a loop that is traversed for Read, Write, Open, Close, and so on (the following code is from ROOTDEP.C):

$$BEGINLOOP(NUM_OPS)
$$IF(OPERATION)
NTSTATUS                
$$SAFE_ROOT$$$$OPERATION$$(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp
    )
{  
 NTSTATUS status;
$$IF(MACBHASSTARTIO)
    Irp->IoStatus.Status = STATUS_PENDING;
// After this point, you can't touch the IRP anymore.
// All hell breaks loose if you dare to
// return a different value than the one in the IRP...
    IoMarkIrpPending(Irp);
    IoStartPacket(
            DeviceObject,
            Irp,
            (PULONG)NULL,
            NULL
            );

    return STATUS_PENDING;
$$ELSE
// This prefix sets up a pointer to the device extension for you.
// C-specific code - Note that DEVICE_EXTENSION must be defined in your .H file.
    PDEVICE_EXTENSION deviceExtension;
    deviceExtension = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;
// TO DO: Add the code that implements the functionality here.
    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = 0;

// TO DO: Depending on what happens here, either mark IRP pending or complete.
// IoMarkIrpPending(Irp);
// IoCompleteRequest(Irp, IO_NO_INCREMENT);
 return(status);
$$ENDIF
}
$$ENDIF
$$ENDLOOP

However, the DeviceIOControl function is unique in that it is the only function in which an additional piece of information (the I/O control code specified by a user-mode application) needs to be extracted from the parameters passed to the function. Thus, the code for DeviceIOControl looks almost, but not exactly, like the code for the other major functions. How can we use DeviceIOControl in the loop but still treat it differently when it is not possible to determine where we are in the loop at expansion time?

The answer is not obvious, but it is simple: Define a variable such as MACBHASIOCTL_x, where x is the index that corresponds to the IOCTL function. The ROOTDEP.C template file contains the following code fragment:

$$BEGINLOOP (NUM_OPS)   // This generates the function bodies for 
                        // the major function.
<common stuff>
$$IF(MACBHASIOCTL) ...
<more common stuff>
$$ENDLOOP

Because MACHASIOCTL is defined only for the index that corresponds to the IOCTL function body, the code in the $$IF (MACBHASIOCTL) branch will be expanded only in that case.

Summary

Custom AppWizards provide a convenient way to build customized project skeletons. Although custom wizards are best suited for MFC-based applications, you can exploit their abstraction mechanisms to cover non-MFC-based applications, and even use them for completely different project types such as device drivers. My article "Using the Windows NT Custom Driver Wizard" in the Development Library demonstrates how a device driver wizard can be used to simplify Windows NT device driver design.