Using the extended diagnostic macros.
In previous article, we showed you how to use the TRACE()
and WARN() diagnostic macros that ship with the OWL package. You can use
these macros to display text messages and conditional warnings from debug versions of your
application.
Unfortunately, the TRACE() and WARN() macros are all-or-nothing
options. If you define the __TRACE constant when you build the application, the
application will display every TRACE message that appears in the code when you run the
application.
If you're looking for only one or two particular TRACE messages, you may
have difficulty finding the message you want when it's surrounded by less important ones.
This same problem may occur when you use the WARN() macro and define the __WARN
constant.
However, you can use the extended diagnostic macros TRACEX() and WARNX()
to display a specific type of message without displaying other types. In this article,
we'll show you how to create and display different types of diagnostic messages by using
the extended diagnostic macros.
Diagnostic groups and messages
To control the display of a particular type of diagnostic message, you'll need to
define a diagnostic group. A diagnostic group manages a set of diagnostic messages.
After you define a diagnostic group, you can create messages that belong to that group.
When you create a message for a given group, you'll also specify the default message level
for that group. You can think of a given message's level as its priority within that
diagnostic group. When you're using the extended diagnostic macros, messages with a lower
numeric level have a higher priority. To determine which messages from a diagnostic group
the program will display, you can set that group's level to any value between 0 and 127.
Defining diagnostic groups
In your source file, you'll use the macro DIAG_DEFINE_GROUP() ahead of
statements that use any of the extended diagnostic macro functions. You'll use this macro
to name the diagnostic group and set its initial message level.
At runtime, each diagnostic group keeps track of its current level setting as well as
an Enabled flag. The extended diagnostic macros use the Enabled flag to determine whether
they'll display messages that belong to this particular group. If the Enabled flag is set
to 1 (True), the extended diagnostic macros will display messages whose levels are equal
to or lower than their group's current level setting.
For example, if you want to create a diagnostic group named Global to display
messages that relate to global variables, you would add
DIAG_DEFINE_GROUP(Global, 1, 0);
to your source file. In this statement, Global is the name of the group, 1
is the initial setting for the group's Enabled flag, and 0 is the initial value
for the group's message level.
Creating extended messages
Initializing a global variable is an important action, so you'll probably want to
display a message at the initialization point if you're viewing any other messages from
the Global diagnostic group. If you want to display a message whenever you enable
its diagnostic group, set the message's level to 0.
To create a TRACEX message that precedes initialization of the global variable array1,
you'd put a line similar to
TRACEX(Global, 0, "Initializing array1");
before the line that actually initializes array1. In this macro call, Global
specifies the diagnostic group, 0 is the message's level, and "Initializing
array1" is the message that TRACEX() will display.
Modifying a global variable's value is also important, but less so than initializing.
If you want a diagnostic message to precede code that modifies the global variable userCount,
you can create a message similar to the following line:
TRACEX(Global, 1, "Modifying userCount");
To force the compiler to expand the TRACEX() macro, you need to define the
global constant __TRACE. Then, when you're ready to run this code in a debugging
mode, you can set the Global diagnostic group's message level to 0 in the DIAG_DEFINE_GROUP()
macro to make the program display only the initialization message.
If you decide later that you also want to see the message about modifying the global
variable, you'll need to set the default message level higher for the Global
diagnostic group. If you change the level parameter to 1 in the DIAG_DEFINE_GROUP()
macro and then rebuild the program, it will display both messages.
By default, the CHECKS.H header file defines the default diagnostic group Def
with the Enabled flag set to 1 and the message level set to 0. This file defines the
standard TRACE() and WARN() macros as part of the Def
diagnostic group. In fact, the CHECKS.H header file contains a macro that the compiler
uses to expand the macro call
TRACE("HI")
into the extended macro call
TRACEX(Def, 0, "HI")
Since the default group's message level is 0 and its Enabled flag is set to 1, TRACE()
macros will display their text message whenever you use them in a file that defines the
global constant __TRACE. However, the diagnostic group Def is just
another group with a special name. As we'll see later, you can manipulate the Def
diagnostic group's Enabled flag and message level just as you would for groups you define.
Creating extended warnings
The CHECKS.H header file also defines an extended version of the WARN() macro.
The WARNX() macro displays a diagnostic group message only when its second
parameter is 0. To force the compiler to expand the WARNX() macro, you need to
define the global constant __WARN.
The WARNX() macro's first parameter is the message's diagnostic group, as was
the case for the TRACEX() macro. However, since the warning condition is now the
second parameter, the message level and message text parameters move to the third and
fourth positions respectively.
For example, to see a warning message when your program sets the global variable count
to 0, you can add the line
WARNX(Global, count, 1, "count == 0");
This macro call will display its message only when you enable the Global
diagnostic group, you set the Global group's message level to 1 or higher, and
the variable count is equal to 0.
Runtime changes
So far, we've discussed the enabled/disabled state and message level of a diagnostic
group as static settings. We've described changing the message level of a group in the
initial definition macro and then recompiling the program.
However, you can also have your program change the message level or Enabled flag at
runtime. Table A summarizes the extended diagnostic function macros you'll use to
adjust these parameters.
Table A - You can use the extended diagnostic function macros to manipulate a
diagnostic group's parameters.
Extended Diagnostic Function Macros |
DIAG_ENABLE() |
Sets the Enabled flag for a given group |
DIAG_ISENABLED() |
Returns the state of the Enabled flag for a given group |
DIAG_SETLEVEL() |
Sets the value of the Level parameter for a given group |
DIAG_GETLEVEL() |
Returns the value of the Level parameter for a given group |
When you use the DIAG_ENABLE() and DIAG_SETLEVEL() function macros,
you'll pass two parameters: the diagnostic group's name and the new state or message
level. For example, to change the current message level of the Global group to 5,
you would call the DIAG_SETLEVEL() function macro like this:
DIAG_SETLEVEL(Global, 5);
You can use the DIAG_ISENABLED() and DIAG_GETLEVEL() function macros
as if they were normal function calls. To display the current message level for the Global
group, you could use the DIAG_GETLEVEL() function macro in a statement similar to
cout << "Global level is ";
cout << DIAG_GETLEVEL(Global) << endl;
If you use any of the extended macro function calls, be sure to include the line
#if defined(__TRACE) || defined(__WARN)
before code that uses the macro, and
#endif
after the code. Using these conditional compile directives will force the compiler to
skip the debug-related code that appears between the #if and #endif
directives.
Putting the extended diagnostic macros to work
Now, let's create a DOS program that displays messages from the extended diagnostic
macro TRACEX(). To demonstrate the extended diagnostic function macros, we'll use
them to adjust the types of messages the program will display.
To begin, launch the Borland C++ Integrated Development Environment. When the menu bar
and desktop appear, choose New Project... from the Project menu. After the New Project
dialog box appears, enter the following name in the Project Path and Name entry field:
\bc4\ext_diag\ext_diag.ide
In the Platform combo box, choose DOS Standard as the type of application. Then, choose
Small from the Target Model combo box as the memory model size for this project. To allow
the compiler to link the standard C/C++ runtime library file, select the Runtime check box
in the Standard Libraries group. Now you can create the new project by clicking OK.
When the EXT_DIAG.IDE Project window appears, double-click on the EXT_DIAG.CPP file's
icon. When the file's editor window appears, enter the code from Listing A.
Listing A: EXT_DIAG.CPP
#include <owl/pch.h>
DIAG_DEFINE_GROUP(Func, 1, 0);
int foo(int var)
{
TRACEX(Func, 1, "> foo()");
TRACEX(Func, 2, "var = " << var);
int returnValue = var * 5;
TRACEX(Func, 2, "returning "
<< returnValue);
TRACEX(Func, 1, "< foo()");
return returnValue;
}
int main()
{
#if defined(__TRACE) || defined(__WARN)
int dataLevel = 0;
int codeLevel = 0;
#endif
char exitCode = 0;
while((exitCode != 'y') &&
(exitCode != 'Y'))
{
TRACE("Default Trace");
TRACEX(Def, 1, "Level 1 Def");
TRACEX(Def, 3, "Level 3 Def");
foo(10);
#if defined(__TRACE) || defined(__WARN)
cout << endl << endl;
if((int)(DIAG_ISENABLED(Def)))
{
cout << "data diagnostic level ";
cout << (int)DIAG_GETLEVEL(Def) <<
endl;
}
else
cout << "data diagnostics disabled\n";
cout << "set diagnostic level?";
cin >> dataLevel;
if(dataLevel < 0)
DIAG_ENABLE(Def, 0);
else
{
DIAG_ENABLE(Def, 1);
DIAG_SETLEVEL(Def, dataLevel);
}
if((int)DIAG_ISENABLED(Func))
{
cout << "code diagnostic level ";
cout << (int)DIAG_GETLEVEL(Func) <<
endl;
}
else
cout << "code diagnostics disabled\n";
cout << "set diagnostic level?";
cin >> codeLevel;
if(codeLevel < 0)
DIAG_ENABLE(Func, 0);
else
{
DIAG_ENABLE(Func, 1);
DIAG_SETLEVEL(Func, codeLevel);
}
#endif
cout << endl << "exit (Y or N)?";
cin >> exitCode;
}
return 0;
}
When you finish entering the code for EXT_DIAG.CPP, save it by choosing Save from the
File menu. Now, let's run the program to see the extended diagnostic macros in action.
Running DIAG.CPP
To run this project, double-click on the EXT_DIAG.IDE project's icon in the Project
window. This tells the compiler to compile the EXT_DIAG.IDE source file, link the
resulting OBJ file with the appropriate library files, and then run the program from a DOS
prompt.
When the program runs, it will enter the while() loop and display the
following:
c:>Trace EXT_DIAG.CPP 30: [Def] Default Trace
data diagnostic level 0
set diagnostic level?
Enter 5 at the prompt. Next, you'll see
code diagnostic level 0
set diagnostic level?
Enter 5 at this prompt as well. The program now moves to the bottom of the while()
loop and displays
exit (Y or N)?
Enter N. This causes the program to re-enter the while() loop and now
display
Trace EXT_DIAG.CPP 30: [Def] Default Trace
Trace EXT_DIAG.CPP 31: [Def] Level 1 Def
Trace EXT_DIAG.CPP 32: [Def] Level 3 Def
Trace EXT_DIAG.CPP 8: [Func] > foo()
Trace EXT_DIAG.CPP 9: [Func] var = 10
Trace EXT_DIAG.CPP 13: [Func] returning 50
Trace EXT_DIAG.CPP 14: [Func] < foo()
data diagnostic level 5
set diagnostic level?
This time through, enter 1 as the data diagnostic level and 1 as the code
diagnostic level. When the exit prompt reappears, enter N again and you'll
see
Trace EXT_DIAG.CPP 30: [Def] Default Trace
Trace EXT_DIAG.CPP 31: [Def] Level 1 Def
Trace EXT_DIAG.CPP 8: [Func] > foo()
Trace EXT_DIAG.CPP 14: [Func] < foo()
data diagnostic level 1
set diagnostic level?
If you look closely at the output above, you'll notice that the program skipped the
TRACE message from line 32 because the level for the Def diagnostic group was too
low. Likewise, the program skipped the messages from lines 9 and 13 that are level 2
messages for the Func diagnostic group.
Now, enter -1 for the data diagnostic level and 3 for the code diagnostic
level, and then enter N at the exit prompt. During this pass, the program
will display
Trace EXT_DIAG.CPP 8: [Func] > foo()
Trace EXT_DIAG.CPP 9: [Func] var = 10
Trace EXT_DIAG.CPP 13: [Func] returning 50
Trace EXT_DIAG.CPP 14: [Func] < foo()
data diagnostic level 5
set diagnostic level?
This time, the program skipped all the messages from the Def group. This is
because the -1 parameter forces the lines
if(dataLevel < 0)
DIAG_ENABLE(Def, 0);
from our code to disable the Def diagnostic group by setting its Enabled flag
to 0. To exit EXT_DIAG.EXE, enter any value for the diagnostic levels and then enter Y at
the exit prompt.
Other considerations
If you decide to use the extended diagnostic macros in your projects, you may want to
declare an enumeration for the different diagnostic levels. By declaring an enumeration
like
const enum {FINAL, BETA2, BETA1, ALPHA1};
you can use the enumeration value in place of the level parameter in any of the
extended diagnostic macros. This will make it easier for someone to know whether a given
message is really appropriate for a particular debugging session.
You can create a keyboard macro for function bodies that automatically inserts entry
and exit messages at the beginning and end of each function. By doing this, you'll be more
likely to include some form of diagnostic message for each function.
Conclusion
While the TRACE() and WARN() macros are useful, the
extended macros TRACEX() and WARNX() provide more display options for
large projects. In addition, by using the extended function macros, you can control the
diagnostic message output dynamically at runtime.
|