Debugging C Programs

This article will give you some hints on how to produce useful diagnostic messages by enhancing OS error messages, and using your own messages to good effect. Symbolic debuggers such as !DDT have their place, but outputting messages at strategic points can produce more useful information. Also, debuggers can have side-effects which completely mask bugs, and Acorn's DDT can be rather awkward and unreliable.

Where to output messages

It's usually best to output all messages to a file, then you've got a permanent record if the computer crashes. If you're working on a Wimp task, or something capable of running in a task window, and you do want the messages to appear in a window as they are generated instead, simply use a filename of "Pipe:SomeLeafname", and in another TaskWindow type "*Type Pipe:SomeLeafname", where SomeLeafname should be some identifying name - it's a good idea to name it after your application.

Rather than go to the extra trouble of opening and closing a log file from my programs, I nearly always use the stderr file stream from the standard library. Using a standard stream makes it very easy to add messages to the file using calls such as printf.

By default, messages sent to stderr appear on the screen, but it can be redirected by adding 2>LogFileName to the end of the command line which calls the C program (usually !RunImage in an application), where LogFileName is the name of the log file you want to create. Using a single '>' means any existing file with that name will be overwritten each time the program starts. If you want to preserve messages from previous runs, use a pair of symbols, '>>' instead. Note that this sort of redirection is handled by the Shared C library, not RISC OS's shell, which requires '{}' braces around the redirection and works on a lower level.

So with what we know so far, we can produce messages from anywhere in our program with lines such as:

fprintf(stderr, "The value of x" " is %d\n", x);

If you have a very serious error which crashes the system before the messages are cleared out of RAM buffers onto disc, you can add:

fflush(stderr);
to flush the buffers after each message.

Enhancing OS error messages with context

If you use DeskLib, you should be familiar with the Error_Check function. If you use Toolbox, you will probably have a very similar function, and RISC_OSLib has one called wimpt_complain, if my memory serves me correctly. For those in the dark, Error_Check's prototype is:

BOOL Error_Check(os_error *);

and its purpose is to check whether SWIs (or calls to veneers such as Wimp_) were successful, so where you might write:

Wimp_CreateWindow(&winblock, &handle);

instead you write:

Error_Check(Wimp_CreateWindow(&winblock, &handle));

Error_Check tests whether the value of its parameter is zero or points to an error. If zero, it returns FALSE, otherwise it calls Wimp_ReportError then returns TRUE. There is also an Error_CheckFatal, which is similar except that it has a void return type and exits instead of returning if there is an error.

Typing out Error_Check (or equivalent) every time I call a SWI or low-level function is quite inconvenient, so instead I use a macro simply called E. Using a macro also means we can perform some very useful tricks before forwarding on to Error_Check. The first trick is simply that we probably want to log the message as well as report it - that's easy.

The second trick is to add context. How often have you seen an error message which simply says something like "Illegal window handle", but you have little idea which window handle is causing the problem and where? We can do something about this, taking advantage of two macros built into the compiler, __FILE__ and __LINE__. The __FILE__ expands to a string containing the name of the source file, and __LINE__ is a numerical value of the line number the macro appears in. So we can change the definition of E to:

#define E(e) my_Error_Check((e) \, __LINE__, __FILE__)

where my_Error_Check would be defined as something like:

BOOL my_Error_Check(os_error *e, int line, const char *file)
{
    os_error lerr;
    if (!e)
        return FALSE;
    lerr.errnum = e->errnum;
    sprintf(lerr.errmess,
            "Error \"%s\" (code 0x%x) at line %d of %s",
            e->errmess, e->errnum, line, file);
    fputs(stderr, lerr.errmess);
    putc('\n', stderr);
    return Error_Check(&lerr);
}

Going back to the macro definition, note the extra brackets around the occurrence of e in the body. This is important because it allows e to be treated as a single expression even if you pass the macro a more complex one; if the expression contained a comma operator, the macro would fail to compile without those brackets. As a rule of thumb, always bracket the arguments in macro bodies unless you're sure that the macro makes it impossible for this confusion to arise (and even then, extra brackets won't hurt), or you're performing some trick - more of which below.

The third trick is that macros can easily be redefined, so during developing you'd use the above technique, then for a release, you could change it back to simply forwarding to Error_Check to avoid cluttering up the code with the extra strings. As well as E and EF (F for Fatal) you might like an additional macro which does the same as E in development builds, but ignores the error for releases:

#define EI(e) e

Similarly, when you've finished debugging, you can simplify E to:

#define E(e) Error_Check(e)

Doing the same for Desk

DeskLib's more advanced younger sibling, Desk, uses a different technique for trapping errors which does away with having to wrap Error_Check around each SWI call. However, you might still end up with error messages with no context; you can get it to tell you which function the error occurred in, but this might not be specific enough, and the information tends to get buried in masses of other technical output.

Desk veneers can return any type of value rather than being constrained to returning an os_error pointer with a consistent use, so an E macro for Desk will have to wrap whole statements, not just the veneer/SWI expression.

The example line we used above, converted to Desk, would become:

E(handle =
  Desk_Wimp_CreateWindow(
       &winblock));

The macro definition for E is considerably more complicated:

#define E(e) Desk_Error2_Try { \
           e; \
       } Desk_Error2_Catch { \
           my_Error_Check(__FILE__,  __LINE__); \
       } Desk_Error2_EndCatch

and my_Error_Check:

void my_Error_Check(const char *file, int line)
{
    Desk_os_error nerr, *oerr;
    oerr = Desk_Error2_ConvertToOS2();
    nerr.errnum = oerr->errnum;
    sprintf(nerr.errmess, "%s [at line %d of %s]",
            oerr->errmess, line, file);
    Desk_Error2_Check(&nerr);
}

This converts the error to an OS error (a sort of highest common denominator in terms of information made available), generates a new error message with added context information, and throws it. I have verified with a test program that you can throw a new exception from within a Desk catch block, but you might feel that it's more appropriate to modify Desk_error2_globalblock and call Desk_Error2_ReThrow. If you do, bear in mind that the result of Desk_Error2_ConvertToOS2 might point to Desk_error2_globalblock's existing data, so you should make a temporary copy of the string when modifying it.

void log_printf(const char *format, ...)
{
    va_list ap;
    va_start(ap, format);
    vfprintf(stderr, format, ap);
    /* fflush(stderr); */
    /* Uncomment the above line if
       crashes are severe enough to
       prevent automatic flushing. */
    va_end(ap);
}

An advanced macro trick

Now we want to replace our calls to fprintf(stderr, ...) with some macro, so we can easily remove the logging from release builds and to save typing. This would be quite straightforward if we were using fputs, but we want to use fprintf so we can easily display numbers etc in the logs. The trouble is that fprintf takes as many arguments as we like (the very feature we want to take advantage of, in fact) and macros can only take a fixed number. There is a way round this, using brackets around multiple arguments to make them appear as one argument as far as the macro is concerned, then taking advantage of the features available in macro expansion to turn them back into multiple arguments for a function call.

If we provide a function like the one in the box below, we can do this with the macro:

#define LOG(msg) log_printf ## msg

The important thing is that when we call LOG, we put double brackets around the message and arguments, like this:

LOG(("Value of x is %d\n", x));

Then the LOG macro will be passed a single argument:

("Value of x is %d\n", x)

and the ## operator will convert the line to:

log_printf ("Value of x is %d\n", x);

If you don't want logging enabled, simply change the macro definition to:

#define LOG(a)

and no code will be generated for calls to LOG; therefore it's important to make sure such calls have no side-effects, e.g. changing values of important variables.

Conclusion

Now you should have a grasp of some useful techniques and tricks to make it easy to add switchable logging to your programs. I'll leave it up to you to work out how to tailor them to your own needs. One thing you might like to do is make use of __FILE__ and __LINE__ in the above logging macro, although I tend to use messages which have enough context in them anyway. That particular exercise may not be as easy as you think!


Source: Archive Magazine 12.12
Publication: Archive Magazine
Contributor: Tony Houghton