OSE - C++ Library User Guide

Graham Dumpleton
Dumpleton Software Consulting Pty Limited
PO BOX 3150
Parramatta, 2124
N.S.W, Australia
email: grahamd@nms.otc.com.au

Table of Contents

Program Debugging
OSE - C++ Library User GuideProgram Debugging

Program Debugging

1 Tracing Function Execution

When you are debugging a program, you may find it useful to know when specific functions are being entered and exited and the value of any arguments passed to those functions. The tool you would use most often to obtain this information is the debugger. However, due to the debugger being an interactive tool it is not always suitable. The OTC_Tracer class, provides a mechanism for you to compile into your program, code which when run, will display information for you as a function is entered and exited, and the value of any arguments passed to the function. The OTC_Tracer class also allows you to generate debugging information about what a function is doing as it runs.

2 Entry and Exit Points

To use the OTC_Tracer class, create an instance of the class at the start of each function you wish to trace. The instance of the class you create should be on the stack, not on the free store. The class must be created on the stack, as the operation of the class relies on the destructor of the class being called automatically when the function is exited.

As their is no way automatically to obtain the name of the function the compiler is processing, you must provide the name of the function to the constructor of the OTC_Tracer class when you create it. This string will be used in the messages displayed when the function is entered and exited. The class does not make a copy of the string you pass to the constructor. The string should either be a string literal, or otherwise be guaranteed not to be deleted prior to the block being exited.

An example of what your code would look like is given below.

  #include <OTC/debug/tracer.hh>

function()
{
OTC_Tracer tracer("function()");
// ...
}
When your program is run, the above code will have the effect of displaying the following messages each time the function is executed.

  @enter - function()
@exit - function()

3 Displaying Function Arguments

In addition to using the OTC_Tracer class to mark entry and exit to a function, you can use it to display additional information about the arguments passed to the function and what the function is doing. Additional messages are displayed by accessing the underlying stream class which the OTC_Tracer class uses to output its own messages. This stream can be obtained by using the overloaded operator, operator()(). For example:

  main(int argc, char* argv[])
{
OTC_Tracer tracer("main(int argc, char* argv[])");
tracer() << "argc = " << argc << endl;
for (int i=0; i<argc; i++)
{
tracer() << "argv[" << i << "] = "
<< argv[i] << endl;
}
}
Invoking `tracer()' as shown in the example, will result in a reference to an instance of the ostream class being returned. Any operations which you would normally perform on an ostream, such as formatting commands, you can also use here. In addition, any objects you would normally display on an ostream may also be displayed using the OTC_Tracer class.

If you have not created an instance of the OTC_Tracer class, but still need to display information about what is happening in a function, you can use the global function otclib_tracer(). This will return a reference to the same stream as returned when calling `tracer()' in the above example. In fact, the following example is equivalent to the above code.

  main(int argc, char* argv[])
{
OTC_Tracer tracer("main(int argc, char* argv[])");

otclib_tracer() << "argc = " << argc << endl;
for (int i=0; i<argc; i++)
{
otclib_tracer() << "argv[" << i << "] = "
<< argv[i] << endl;
}
}
Note that using the OTC_Tracer class as shown, is not safe within an multi-threaded application.

4 Monitoring Object Lifetimes

Although primarily intended for tracing entry and exit to functions, you can also use the OTC_Tracer class in a number of other useful ways. The first of these is to monitor the lifetimes of objects. It should be highlighted that this technique is not intended as a means of checking for memory leaks in a program, but is intended as a mechanism for ensuring that objects are created and destroyed, when it is expected that they should.

The general strategy employed to monitor the lifetimes of objects, is to use the OTC_Tracer class in the constructor and destructor of the class type you wish to monitor. In order that you can match up the constructor and destructor call for a specific instance of an object, you should output a value in both the constructor and destructor which uniquely identifies that instance of the class. This value could be the address of the object in memory, or may be an identifier which has particular meaning within the context of your application.

A suggested format is illustrated below.

  Foo::Foo()
{
OTC_Tracer tracer("Foo::Foo()");
tracer() << "@create Foo - " << (void*)this << endl;
}

Foo::~Foo()
{
OTC_Tracer tracer("Foo::~Foo()");
tracer() << "@destroy Foo - " << (void*)this << endl;
}
It is recommended that you use easily identifiable tags to denote the creation and destruction of the object, as this will allow you to write automated tools which can scan the program output and determine if everything is working as expected.

5 Coverage Analysis

A second use for the OTC_Tracer class is as a crude mechanism for performing coverage analysis on your code during testing. Although not as good as the results from a tool such as tcov, if your compiler does not support tcov, or an equivalent tool, it is better than nothing.

As with monitoring the lifetimes of objects, the idea is to generate tagged information within the trace output which you can either manually or automatically analyse. A suggested format to use is given in the following example.

  void function()
{
OTC_Tracer tracer("void function()");
tracer() << "@block 1" << endl;

if (...)
{
tracer() << "@block 1.1" << endl;
...
if (...)
{
tracer() << "@block 1.1.1" << endl;
...
}
...
if (...)
{
tracer() << "@block 1.1.2" << endl;
...
}
}

if (...)
{
tracer() << "@block 1.2" << endl;
...
}
}
Generation of the numeric values to identify each block would be tedious if done manually. You may consider writing a tool which would automatically insert and keep the tags up to date.

6 Generating a Stack Trace

When you run a program which uses the OTC_Tracer class, each time an instance of the OTC_Tracer class is created, it is linked up with any instances of the class which already exist. When the function in which the OTC_Tracer class is created exits, it will break its link from this chain. By traversing the list of OTC_Tracer classes you can produce a rudimentary stack trace. The stack trace will not be as complete as one produced using a debugger, but can be a useful debugging aid in working out how program control reached a specific point.

An example of where you might dump out the information held in the chain of OTC_Tracer classes, is in functions which abort the program due to an unexpected error occurring. Code for traversing the chain of OTC_Tracer classes and displaying the name of each function is illustrated below.

  if (OTC_Tracer::level() != 0)
{
OTC_Tracer const* aTracer = OTC_Tracer::last();
if (aTracer != 0)
{
OTC_Logger::notify(
OTCLIB_LOG_DEBUG,"Tracer stack dump:");
while (aTracer != 0)
{
if (aTracer->prototype() != 0)
OTC_Logger::notify(
OTCLIB_LOG_DEBUG,aTracer->prototype());
aTracer = aTracer->prev();
}
}
}

7 Saving Output to a File

By default, all output from the OTC_Tracer class is sent to the standard error output via clog, an instance of the class ostream provided with the standard C++ streams library. If you want to capture the trace output in a file, you should define the environment variable OTCLIB_TRACEFILE to the name of an output file, before you run your program. If the file you specify cannot be opened, output will still be sent to standard error output. If the file can be opened for writing and the file was not empty, it will be truncated before being used.

As the output file will be truncated by the program, you should avoid having multiple programs use the same output file. This means that if you have a process that forks and then executes a subprocess, you should use the system routine putenv() to set the OTCLIB_TRACEFILE environment variable to a different file name, prior to executing the new process.

8 Filtering Trace Output

By default, trace output is indented each time an instance of the OTC_Tracer class is created. Indenting returns to the previous level when an instance of the OTC_Tracer class is destroyed. Indenting makes it easier to track when a block is entered and exited. Filtering of trace output allows you to be more selective about what you see. For example, you may only wish to see trace output while in a specific block of code.

Filtering of trace output can be achieved in a number of ways, the simplest of which is to pipe the output of the program directly into the filter program.

  myprogram 2>&1 | awk -f myscript
When you do this, it is important to remember that the stream clog, which is used to generate messages, displays them on the standard error output. Therefore, you need to merge standard error output with standard output when you run your program, so it can be piped to the filter process.

The problem with this approach is that messages sent to standard output or standard error output which are not part of the trace output, will also be sent to the filter program. These additional messages could confuse the filter program, alternatively you may not wish to have the normal messages formatted in the same way as the trace output.

To split the trace output into its own stream so that it can be processed separately, you can specify a file descriptor to which trace output should be sent instead of that for standard error output. To have this occur, you should set the environment variable OTCLIB_TRACEFD to the file descriptor number to which output should be directed, before you run your program. To make use of this feature directly, you will need to be using a shell which supports the ability to pipe the output of a specific file descriptor to a process. One shell which allows this is `rc', in which it is possible to write:

  OTCLIB_TRACEFD=3 myprogram |[3] awk -f myscript
An alternative way of accessing this feature is to write a program in C, C++ or a scripting language, in which you can invoke a filter program, and then execute your program; passing through the environment variable OTCLIB_TRACEFD, the number of the file descriptor on which the filter program is listening.

9 Redirecting Output to the Logger

A further alternative available, is to direct the trace output to the logger. If this is done, trace messages will be displayed using the loggerat priority OTCLIB_LOG_TRACE. To divert trace output to the logger, the environment variable OTCLIB_LOGTRACE should be defined before your program is run. All features available for diverting of log messages to files or processes, will also apply to tracer messages when they are being diverted to the logger. For example, by setting the OTCLIB_LOGLONGFORMAT environment variable, time stamps can be added to trace messages.

10 Redirecting Output from Within Code

All of the methods for redirecting trace output described so far have relied upon you setting up environment variables before you run the program, or redirecting the standard error output of the program. If you need to ensure that the trace output from a program is always redirected into a particular file or process, it can be coded directly into your program. The first way of achieving this makes use of the fact that the clog stream used by the OTC_Tracer class, is actually of type ostream_withassign. What this means is that it is possible for you assign a different stream to clog. For example, to have the trace output by default always sent to a file, the following could be done within the program's main() routine.

  main()
{
ofstream* outs = new ofstream("LOG");
clog = *outs;

OTC_Tracer tracer("main()");
tracer() << "Hello World" << endl;
return 0;
}
Because of the way streams work, you are not restricted to sending output to a file, you can assign any type of ostream to clog. As an example, you could direct the trace output to another process by using the system routine popen() to create the process, creating an instance of the class ofstream with the file descriptor for the process, and then assigning the instance of ofstream to clog. Alternatively, you could create your own special streams class which directed trace output to a graphics windows, and assign that to clog.

When assigning a new stream to clog, you should ensure that the stream which you have created has been created using operator new(). You should never try to delete the stream once you have assigned it to clog. This is necessary to ensure that clog is still valid if trace output is produced from the destructors of static objects.

It is important to note, that since you would be assigning to clog at the commencement of the main() routine, any output of the OTC_Tracer class which occurred during the initialisation of static objects will not go where you wanted it to go. This method of redirecting trace output therefore may not always be suitable.

An alternative to assigning a new stream to clog, is to call the function OTC_Tracer::setStream(). The same restrictions which apply to assigning to clog, also apply when using this function. An example of using this function is given below.

  main()
{
ofstream* outs = new ofstream("LOG");
OTC_Tracer::setStream(outs);

OTC_Tracer tracer("main()");
tracer() << "Hello World" << endl;
return 0;
}
Once the function OTC_Tracer::setStream() has been called, assigning a stream to clog will not change where trace output is displayed.

11 Optional Inclusion of Trace Code

A major error programmers make when they have finished debugging code is to remove all the extra code they have added in. This can be a considerable waste of time and is even worse if it is found later, that the original problem has manifested itself once again and the debugging code has to be replaced. A much better solution is to enclose the code within a conditional statement, allowing it to be compiled out of your program without having to remove it.

Traditionally this has been done using the preprocessor in the following manner.

  main()
{
#ifdef DEBUG
OTC_Tracer tracer("main()");
tracer() << "Hello World" << endl;
#endif
return 0;
}
This tends to clutter code and make it look unnecessarily complicated. To overcome this problem, macros are provided which you can use instead of using the OTC_Tracer class directly. By using the macros you can conditionally compile in the trace code to your program only when you require it. Using the macros supplied, the code above is rewritten as:

  main()
{
OTCLIB_MARKBLOCK(1,"main()");
OTCLIB_TRACER(1) << "Hello World" << endl;
return 0;
}
Note that versions of OSE prior to release 3.0 provided a OTCLIB_DOTRACE() macro. This is equivalent to the OTCLIB_MARKBLOCK() macro, with a first argument of `1'. The OTCLIB_DOTRACE() macro is kept for backward compatability.

The macros mean the code is similar to what it originally was, but eliminates the `#ifdef' and `#endif'. If you want the trace code to be included into your program, you need to define the preprocessor symbol OTCLIB_TRACE when running the compiler. If you are using makeit, you would include the definition,

  CPPFLAGS += -DOTCLIB_TRACE
in your makefile.

When using the macros, there are two things you will need to be careful about. Firstly, it is not obvious that the code can be left out and so you may accidentally place code in the OTCLIB_TRACER() statement which has a side effect which affects the operation of the program. Obviously you should avoid this as not including the trace code will mean your program will run differently. Secondly, the OTCLIB_MARKBLOCK() macro uses the same name each time it is used, for the instance of the OTC_Tracer class which it creates. This means that you should only use the OTCLIB_MARKBLOCK() macro once within each block of code. For example,

  main()
{
OTCLIB_MARKBLOCK(1,"main()");
OTCLIB_TRACER(1) << "Hello World" << endl;

if (1)
{
OTCLIB_MARKBLOCK(1,"main() - nested block");
}

return 0;
}
is okay, but the following is not.

  main()
{
OTCLIB_MARKBLOCK(1,"main()");
OTCLIB_TRACER(1) << "Hello World" << endl;

OTCLIB_MARKBLOCK(1,"main() - same block");

return 0;
}
Note that it is not necessary for you to have used the OTCLIB_MARKBLOCK() in a block to be able to use the OTCLIB_TRACER() macro. For example, you can write:

  main()
{
OTCLIB_TRACER(1) << "Hello World" << endl;
return 0;
}
The only difference will be that the `@enter' and `@exit' tags for entry and exit of the function will not be displayed.

Although the OTCLIB_MARKBLOCK() and OTCLIB_TRACER() macros eliminate the need for you to use the `#ifdef' and `#endif' statements in the above examples, you will need to resort back to using them in certain circumstances. An example of such a situation is when a conditional statement or loop is required only when trace output is being generated. An example of this is shown below.

  main(int argc, char* argv[])
{
OTCLIB_MARKBLOCK(1,"main(int argc, char* argv[])");

OTCLIB_TRACER(1) << "argc = " << argc << endl;
#ifdef OTCLIB_TRACE
for (int i=0; i<argc; i++)
{
OTCLIB_TRACER(1) << "argv[" << i << "] = ";
OTCLIB_TRACER(1) << argv[i] << endl;
}
#endif
}
If you did not enclose the `for' loop with `#ifdef' and `#endif', redundant statements would be executed by your program. Although not a major problem in this example, it could be in other situations.

12 Controlling Verbosity of Trace Output

When you use the OTCLIB_TRACER() and OTCLIB_MARKBLOCK() macros you must supply an integer argument. This argument is the trace level for that statement. The `@enter' and `@exit' tags will only be displayed, in the case of the OTCLIB_MARKBLOCK() macro, and the code associated with the OTCLIB_TRACER() statement will only be executed, if the trace level threshold is greater than or equal to the trace level for that statement. The trace level allows you to augment information being displayed with a value indicating its importance.

If you use a trace level of `0' the statement will always be executed. The entry and exit tags for a function will be displayed only if the trace level threshold is set to a value of `1' or higher. In general you would use the value `1', however if you have information which need not be displayed always, you should use higher values.

When you run your program, you can set the trace level threshold in two ways. The first method is to set the environment variable OTCLIB_TRACELEVEL to the value desired, before you run your program. If you do not set the OTCLIB_TRACELEVEL environment variable, the default trace level threshold of `0' will be used.

The second way of setting the trace level threshold is by adding code into your program to set it. For example:

  #include <OTC/debug/tracer.hh>

main()
{
OTC_Tracer::setLevel(1);
OTCLIB_MARKBLOCK(1,"main()");
...
}
Since the trace level threshold will only be set inside main(), any trace output generated from constructors for static objects will not be displayed unless the OTCLIB_TRACELEVEL environment variable were also set. Once the set level command has been executed though, the value to which the trace level threshold has been set will take precedence over that defined in the OTCLIB_TRACELEVEL environment variable.

13 Generating Location Tags

When you use the OTCLIB_MARKBLOCK() macro, you can optionally enable the generation of information giving the file and line number where the macro was used. To enable the generation of this information you should set the OTCLIB_TRACEINFO environment variable. This additional information will be generated in the following form.

  @location - "file.cc", line 42
When displayed, this will appear immediately before the `@enter' line generated by the OTCLIB_MARKBLOCK() macro.

14 Enabling all Trace Output

When using the OTCLIB_MARKBLOCK() and OTCLIB_TRACER() macros, it is possible to enable all trace output, regardless of other settings, by setting the environment variable OTCLIB_GLOBALTRACE before running your program. Whether global tracing has been enabled, can be detected in your program by calling the function OTC_Tracer::globalTrace(). You can also enable and disable global tracing from within your program, by calling the OTC_Tracer::enableGlobalTrace() and OTC_Tracer::disableGlobalTrace() functions.