Feedback Search Top Backward
EDM/2

Debugging Classes in Borland C++

Written by Timm Steinbeck

 

Introduction

When using Borland C++ for OS/2 I quickly found out that there are certain problems involved when debugging programs. Several of these were related to my programs, several were of a nature general to debugging and some were and are very specific problems due to Borland's debugger. The second type of problems manifested themself in a context of Murphy's law: Everything goes wrong when you expect it the least, meaning your program has a fault especially when you are not debugging it. The third category meant that when debugging rather often, not only my program would crash but also the debugger.

The Base Protocol Class

To avoid these types of problems I decided to write a class that would enable me easily to put statements in the code to write some sort of comment in a file for later analysis. I decided to write it so that there would be one globally accessible instance of this class in every program that uses it. That way I would not have to worry about initialization and deinitialization in the programs. I could put this code in the constructor and destructor and forget about it. I also made it it so that the constructor and destructor also wrote the time and date of the beginning and ending of the session.

After some time I realized that it would be quite useful having routines for writing a complete PM message or a number without having to manually convert them to strings. So together with the function for writing text to the file I then had a class declaration that looked like this:

class OProtocol {
protected:
  char* PathName;              // Pointer for the name of the file
                               // were comments are to be stored
  int IO;                      // Indicator whether file can be
                               // opened or not
public:
  OProtocol(char* Name);      // Filename is passed to class
  ~OProtocol();
  virtual void Text(char* Text);
  virtual void Msg(HWND hWnd, UINT Msg, MPARAM P1, MPARAM P2);
  virtual void Long(char *c, long l);
};

The Procedure Protocol Class

After some time I even became too lazy to put statements at the beginning and end of each procedure. While nothing could be done about the beginning I decided to write another class for procedure-comments, using the OProtocol class for writing the comments to the file. For this class I only needed to write a constructor and a destructor. When an instance of the class is declared at the beginning of a procedure the constructor writes a comment about the procedure being started. The destructor is then called automatically when the procedure ends, so this is a good place for writing a comment about the end of the procedure. This left me with the following class declaration:

class OProcedure {
protected:
  char *ProcName;
public:
  OProcedure(char * Name);
  ~OProcedure();
};

I wrote the constructor so, that it wrote something like "Function <name> started" in the protocol file. The destructor then writes "Function <name> ended". So when creating an instance of this class in a function passing the name of the function I had a list of when this function was called and when it was ended.

The Method Protocol Class

But even this capability did not satisfy me, since whenever working with several instances of a class I only knew that a member function was called, but not to which instance it did belong. Of course I could call OProtocol::Long(this) but being lazy, I sought a better - or rather easier - way to do this. The solution was to write a new class that was similar to the OProcedure class but whose constructor accepted a PVOID as an argument and could then write this pointer to the protocol file as well.

class OMethod {
protected:
  char *ProcName;
  void *that;
public:
  OMethod(void *That, char *Name);
  ~OMethod();
};

This class is of course not much different from OProcedure, but it has the additional advantage of providing a standardized format in the file for method calls. I did not derive the OMethod class from the OProcedure class on purpose because I wanted to have the whole comment in one line, so that I had no use for the OProcedure constructor.

Some Helping Hands

What I described so far are just the classes. But until now when I used them during a debugging cycle and then finished the program I would have to delete everything by hand. As I am sure you will have guessed by now, this did not satisfy me and so I went ahead and wrote a couple of macros for calling the functions. The first macro actually is for creating and initializing an instance of the OProtocol class.

#define PROT_NEW(ClasS, FilE) ClasS Protocol(FilE);ProtP=3D&Protocol

ProtP is a global pointer to OProtocol. So for using a protocol file in a program I would then write

PROT_NEW(OProtocol, "Foo.Prt");
This approach also enables other users to derive classes from OProtocol and then use these for writing the messages. The other macros I defined just went about and called ProtP methods or created instances of OProcedure or OMethod respectively. And not to forget the reason for this operation I used preprocessor directives for empty versions of all these macros.

The actual implementation of all this is rather straightforward and will therefore not be elaborated further; there are, of course, comments in the source file.

Usage

For the classes to be of some use, you must know how to use them. For this purpose I will put some exemplary statements here.

PROT_NEW(OProtocol, "C:\\FOO\\BAR.PRT");
This will initialize the protocol session in a file C:\FOO\BAR.PTR. A statement like that has to occur before any other protocoling attempts take place. And there should only be one of them as well.
PROT_TEXT("Sample debugging text");
This writes
Sample debugging text
in the previously initialized file, in this case C:\FOO\BAR.PRT. If you want a line feed here you have to pass it yourself, but it is not much work rewriting the code, so that it is done automatically.
PROT_LONG("A sample long", ASampleLong);
The Result of this will be
A sample long: 0x1234
or with whatever value ASampleLong happens to have.
PROT_MSG(AnHWND, WM_CREATE, 0, 0);
After this call you will be able to read
Message: 0x1234        0x1      0x0       0x0
in your protocol file.
PROT_METHOD("SampleMethod");
This statement in a method will result in two lines:
Object 0x1234 Method --> SampleMethod
Object 0x1234 Method <-- SampleMethod
The this pointer of the object need not be given specifically PROT_PROCEDURE can be used similarly to PROT_METHOD. But whereas you can use PROT_PROCEDURE in methods as well, you can't use PROT_METHOD in a simple function, since it it assumes that this is properly defined.

To determine what you want to include and what you don't want when compiling there are several #defines. These are:

  • DBG_PROT_TEXT
  • DBG_PROT_MSG
  • DBG_PROT_LONG
  • DBG_PROT_PROC
  • DBG_PROT_ALL

All of those defines will result in the including of the OProtocol class and DBG_PROT_PROC will also include OProcedure and OMethod. They will also control the expansation of the PROT_* macros to something useful or just empty macros.

  • DBG_PROT_TEXT This will expand the PROT_TEXT macro for text into a call to OProtocol::Text().
  • DBG_PROT_MSG This will expand the PROT_MSG macro into a call to OProtocol::Msg().
  • DBG_PROT_LONG This will enable the use of OProtocol::Long() via PROT_LONG.
  • DBG_PROT_PROC This results in the expansion of the PROT_PROCEDURE and PROT_METHOD for access to the OProcedure and OMethod classes.

Finally, to use it you need to link PROT.OBJ to your executable when you are debugging it. When you haven't defined any of the DBG_PROT_* defines there is no need to do so.

I will also add one word or warning: be careful not to put too many protocoling statements in a program, especially be wary with protocolin messages. You can quickly end up having a protocol file of more than half a megabyte. Try to reduce the usage to the finished parts.