![]() |
|
Published 1999-05-25 Printer-friendly version
In some of my recent classes and a few newsgroup articles, I was asked to explain or show the debugger. Those in my class may testify that they could have seen a wicked looking smile. Those asking on the newsgroups let me assure you that the smile was indeed there.
I really don't know why the debugger has fallen out of use so quickly. I am big fan of using debuggers.
In CPD 2.x, Clarion users had a debugger, sort of. Remember all those CTRL-S and CTRL-F keystrokes to see one line of code? Not too many things to inspect, but it did work, slow as it was.
In version 3.x, Clarion Software (Topspeed's former name) delivered a really robust, state of the art debugger. I loved it! At the time I was also working in a COBOL shop. Micro Focus made one of the best debuggers I have ever seen. Reading pages of COBOL code is quite a task, and to navigate through so much very quickly is quite a task. The CFD debugger had about the same power and ease of use. No wonder this was one of my most favorite versions of Clarion. (OK, I am a minority on this point, but that is my story and I am sticking to it).
When Clarion for Windows first came out, it wasn't long
before I hit the debugger. What is an event anyway? Why did I have
to go through the ACCEPT statement so many times and
why did I have to step through all those lines of code before I got
to the interesting bits?
The debugger seemed OK, but why did I have step through all
those routines? I was not interested in those, just my code. I gave
up just like everyone else and started using the
MESSAGE() function. But that takes some typing to set
it up right. So I began using the STOP()
statement.
When I couldn't figure out why some code was behaving the
way it was, or it started working with all the STOP()
statements in place, I was even more mystified! One of my
co-workers suggested that I use the debugger. Oh NO! Not the
dreaded debugger! PLEASE! I was in a hurry, deadlines
approaching. I felt like I was fighting for my life. I fired up the
debugger and stepped through every line of code in my procedure. I
did eventually find the problem and fixed it. But what a chore that
was!
A couple of years ago I had the fortune of talking with designer of the debugger. I started asking some questions. He answered them. I still remember the last question I asked him: "How long has that been in there?!" After we finished our drinks, I had to go back to my room and try a few things. Son of gun! Will ya look at that! I was getting excited about debuggers again! There were the breakpoints, the fast stepping, and even watch windows! He told me how these features worked.
However I wasn't completely convinced. At the time I was
having problems with a conversion program, and I had
STOP() statements everywhere. The problem was all the
values shown by my STOP() statements were correct, but
the application still wouldn't work properly. I was sure there
was nothing wrong with my code.
Out of desperation, I fired up the debugger, and I found the bug
in 20 seconds. This after four hours of reading the code,
STOP() statements, etc. The problem wasn't that
my code had bugs. In fact, the debugger proved that my code was
good, but it did not work! The code I wrote did not have bugs in
it; the code I did not write was the problem.
Huh? The problem was that I forgot to add code to
OPEN() a file. My bug was missing code! How can
STOP() or MESSAGE() ever find that
problem? Of course this exposed a few more lines of code I needed
to write (error checking, adding or changing the converted data,
etc). Needless to say, it was not one of my better moments (or
programs)!
In this case, the debugger showed to me that there was an error
being returned by the PUT() statement: "File not
open." MESSAGE() and STOP() can report
this, but you have to write the code to display the error. If I had
thought of that, I would have written the error checking in the
first place! This lesson taught me a few things, and the first one
was humility. In this example, STOP() and
MESSAGE() would not have helped. Neither will be of
much help in getting into deeper issues either, so you might as
well stop using them.
Why not use STOP() and MESSAGE() for
debugging? Well, let me be a bit blunt. Neither is designed for
debugging! STOP() is designed to suspend
program execution (see the Help topic). This means it requires a
response from the user. The MESSAGE() procedure
displays a Windows-standard message box, typically requiring only a
Yes or No response, or no specific response at all. It is also
interacting with a user.
These functions process events too! Have you ever had a
procedure filled with STOP() or MESSAGE()
statements, and it started behaving strangely or went on an
entirely new and wrong path? Guess what was messing with your event
messages? What about all those embeds you put these functions in,
only to get a phone call from your customer that he got a
STOP() message! What guarantee is there that you
removed all of them before you shipped the product?
I hate to break this to you, but STOP() and
MESSAGE() are not safe debugging tools! You
have enough to debug as it is!
OK, I admit it. I prefer the 32-bit debugger to the 16-bit. I have my reasons. I think the interface is cleaner and easier to navigate quickly. Also, the startup process makes it easier to get to the code where I think the bug lives. You can also use the 32-bit debugger on NT. The 16-bit debugger won't run on NT - period. NT is designed to crash low level 16-bit applications. And I also like how much easier it is to add watch variables in the 32-bit debugger.
I will now walk through the debugger using the MailList application, which you can find in your Clarion /examples directory. First I'll explain typical debugging techniques, and later I'll show you how the debugger is also a good testing tool (this is one of the secrets I discovered that does not require anyone to pick up a dinner tab). Also I'll give you some tips for using the debugger outside of the Clarion IDE (yes, you read that correctly).
Any project you want to use the debugger on must have Debug turned on in the project settings (this is the shipping default). See Figures 1 and 2. If you have changed these settings to Build Release then you must uncheck this and set Debug to Full. You'll also need to set the Target OS to Windows 32 bit, as this article focuses on the 32 bit debugger.

TIP: If you use string slicing or other code to step through arrays, then set the Array Index checkbox on. This will trap out of range subscript errors..

Save your project settings and choose Project|Debug from the main menu or click on the debug button on the toolbar. (Editor's note: If you still have some Clarion 1.0 beta disks sitting around in your archives, dust them off and have a look at the debugger EXE with a resource editor you'll see some entertaining alternative images to the standard bug-with-the-red-line-drawn-over-it)
After compiling and making the executable, you see something similar to Figure 3:

Now what? The first thing I do is select the procedure I want to
debug. You can select more than one, but lets keep it simple for
now. I will debug the BrowsePeople procedure. Scroll
down the procedures list until you find that procedure name.
At this point, as soon as you click on the procedure name, its source file is instantly visible in the source window. I minimized the rest of the windows at this point and resize the source window so that it fills about 3/4 of the visible screen (it can go virtual if you stretch it past the dimensions of the screen, but I don't like that, so I don't use it).
At the bottom of the debugger window, you have the Trace, Disassembler, Procedure (See figure 3), Stack Trace and Globals windows, all minimized. Here's a brief tour of some of these windows.
What if you want to see a variable defined in your dictionary? Since variables declared here are considered global, restore the Globals window. Where are the file variables? The entire display is collapsed (it is a tree display). All dictionary variables are under the node named after your application, in this case, MailList. Variables declared under the global button in your app can also be found here.
Expand the People tree. All variables are sorted alphabetically
to make them easier to find. Highlight PEO:FirstName
and right click- a pop-up menu appears. There is only one item on
the pop-up: Watch Variable. Click on this and presto! The watch
window appears. It is at this time that I like to resize and move
the watch window under my source window. Now add
PEO:LastName in the same fashion. You should have two
variables in the watch window. Since the debugger has not yet begun
to move through the code, the contents of these should be
blank.
If I were to add numeric data or single elements of a string
(strings are arrays of characters), then I would have had two more
items in the pop-up window, Edit Variable and Examine Memory. You
cannot edit an entire string, but you can edit or examine a
specific string array element. Expand the PEO:LastName
variable and then right click on one of the elements to see these
options.
Minimize the global window as there is no further need of it. Move and resize the two open windows so they are orderly. Here is how my screen looks like after I have completed this step:

Page down to BRW1.SetQueueRecord while in the
source window. Click on the first line after CODE.
This line formats the name fields into a local variable. The line
should be green in color. Press the letter "B" or double click or
right click and select Set Breakpoint. This sets a hard break. Hard
break means that until the debugger hits this line of code, you do
not have to step through each line until you get to this
point. The debugger gets focus when this line of code is hit. The
line also changes color to red indicating a hard break point. It
isn't red? Press the down arrow key to highlight the next
line. It is now. The current position is green and hard breaks are
red. Red and green make yellow. Cute, eh?
Now you are ready to debug, sort of. You cannot add any local variables to the watch window as you are not really in this procedure. But you soon will. To start the debug process, I like pressing the letter G (or you can right-click and choose Go from the popup menu, or from the pulldown menu, Debug/Go). Your program is running normally.
Now choose Browse|People to send mailings. The debugger now gets
focus at the breakpoint. Since Clarion uses a stack calling
convention (a fancy term for resource management), you will see
that there is also a Stack Trace window. This is where you will
find your local variables as they have now been pushed onto the
stack. Restore the stack trace window. Expand
BrowsePeople. Now find the local variable named
LastFirst. Right click on this and choose Watch
Variable to add it to the watch window. You see the variable is now
blank. Minimize the Stack Trace window to move it from the
workspace as it is no longer needed. The source window should be
the active window.

If you right click and choose Step Source (or press the letter
T), the current line moves down one line of code and right before
your eyes, LastFirst is changed in the watch
window.
Now for some fun press G and wait for the debugger to go around and update the global variables. Now press T and watch the local variable change to its new contents. It will do this until the list box is filled.
You can set as many break points as you wish to get the job done, inspecting the contents of any variable you wish (as long as it is in scope). Remove the hard break, by repeating how you set it. Now press the Go button and your app runs normally. You can now choose another source file to debug, if you wish.
If you are done debugging, there is something very important you must do. Close down the client application normally, then close down the debugger. Closing down the debugger with the debuggee still active could cause memory leaks, or worse.
What Did You Discover?
STOP() and
MESSAGE().STOP() and
MESSAGE() statements (and safer!).DLLs are applications too, and the same rules apply.
Here's a common example. Do you use a data DLL (no procedures, just dct declarations and template variables)? If this DLL's debug options are set to Release Mode, guess what you cannot see, let alone place, in a watch window? Global data. This means every field defined in your dictionary. You won't find a one. So remember to change your project settings there too, before you debug.
One of the tricks I have "taught" the debugger to do (or is it the other way around?) is to assist me testing an application. Any good developer tries to predict what a user can and will do and writes code accordingly. How many of you have had customers call you and say, "I did this and it went bang?" Customers are a very creative lot.
As an example I will use a program that simply reports the event that has fired. This application is used in the Essentials class, so any former Essential student has this program and can follow along. You can also download the application. The NameEvent routine that returns the event string also checks for a user defined event and unknown errors. The code looks sound (it is quite simple), but here is the question: How can I test this to prove that it works? I don't want to write code that force things down a certain logic path, as that can be risky. Writing more code, no matter how simple, adds complexity to a project, increasing risk of logic errors. What would be ideal is that I steer the program down the "once in a blue moon" logic path, without writing any extra code. Besides, I am too lazy for that!
I can now safely test each condition coded to see if it works at runtime, just like your customers will do. Then I know it works as I have seen it perform as I designed it. Is that not how you should test an application, like a customer would use it?
What I need to do is setup my project to use the debugger (again 32-bit) and start it. In this case, all the code is in one source file. The code I am interested in is at the end of a function that returns a string informing the user what event has just executed, as seen in Listing 1.
NameEvent FUNCTION(EventNumber)
ReturnString STRING(20)
CODE
CASE EventNumber
OF 01H
ReturnString = 'EVENT:Accepted'
! Many more event tests omitted
OF 213H
ReturnString = 'EVENT:Maximize'
OF 214H
ReturnString = 'EVENT:MInimize'
OF 215H
ReturnString = 'EVENT:Completed'
OF 400H TO 0FFFH
ReturnString = EventNumber & ' - User Defined EVENT'
ELSE
ReturnString = EventNumber & ' - Unknown EVENT'
END
RETURN(ReturnString)
As you can see, this code is part of a large CASE
structure, so I have set my break point at CASE
EventNumber. At the first event that fires, the debugger
gets focus because least one event has fired. Now I need to bring
up the Stack Trace window and find the local variable called
EventNumber (which is also seen in figure 5 above).
Once I do this, right click and select Edit Variable.
Here is something useful. The listing above is testing hex numbers, but the edit window is prompting for a decimal number. No problem. Simply enter the new value as shown in Figure 6:

Click on OK and then "Step Source". Notice in Figure 7 where the cursor has landed?

The code works as expected and now I have proof it works. The value 501h is certainly between the range reserved for user defined events and the code is picking this up. I can also test for the out of range condition by giving some value that cannot be trapped in this case structure. In this case, I would get the "unknown event."
The debugger can test for all kinds of conditions in your code. If the code doesn't work, then it is a simple matter of setting a few more breakpoints and clearing breakpoints no longer needed until you narrow down what the exact problem is.
A very good friend of mine once told me "the correct 'why' opens the door for handling." And my grandfather told me that all solutions are simple. The debugger is certainly living proof of both sayings.
Sometimes the biggest bugs in our code are staring at right at us, like my embarrassing example earlier. In my experience, the nastiest bugs are hiding in plain sight. It is a relief and a real shout of "doh!" or a slap of the forehead when you finally find them. Once you find the problem, then writing the correction is quite simple.
If you would like to run the debugger outside of the Clarion IDE (i.e. right click on your EXE and launch the debugger), then try adding these entries to the registry (backup your registry file first - you have the tools needed to do this already. Regedit will do this and Windows 98 will do this upon startup). You may need to change the Clarion drive and/or path to suit your own machine.
[HKEY_CLASSES_ROOT\exefile\shell\16bit Debugger] [HKEY_CLASSES_ROOT\exefile\shell\16bit Debugger\command] @="c5db.exe c:\\clarion5\\bin\\clarion5.red c:\\clarion5\\bin\c5ee.ini %1" [HKEY_CLASSES_ROOT\exefile\shell\32bit Debugger] [HKEY_CLASSES_ROOT\exefile\shell\32bit Debugger\command] @="c5dbx.exe c:\\clarion5\\bin\\clarion5.red c:\\clarion5\\bin\c5ee.ini %1" [HKEY_CLASSES_ROOT\exefile\shell\Restart Debugger] [HKEY_CLASSES_ROOT\exefile\shell\Restart Debugger\command] @="c5db.exe /r"
These settings will launch the debugger from Explorer with a right click (pop-up menu). The last grouping restarts the 16-bit debugger with all of your previous settings and windows. This is not yet available in the 32-bit debugger.
If you install the 32-bit debugger as your system debugger (from the debugger's Setup menu), then when your program that crashes (GPFs), you get this familiar window with a small addition, as shown in Figure 8.

If you press the debug button, then the 32-bit debugger starts and tries to find the source that caused the problem. This works with non-Clarion programs too (although you may not have the source), although in this case you will get the disassembly window.
OK, I have a confession to make. I have used the debugger with a bit of an evil twist.
As I stated above, this works on other programs. I have used this to report bugs to other vendors. I simply grab the screen shots of the disassembly window (which shows the code that crashed) along with the memory register contents. I then send the contents to the support folks and ask them "When will I get my fix?" I would be less than honest if I said that I did not get some sort of satisfaction with that statement! In case anyone is interested, I did get a fix! And it worked. If anyone is curious, it was a VB program 'nuff said.
The debugger does not support conditional breakpoints, I regret to say. This missing feature is known about and when it will make its debut is beyond the scope of this article. However, you are not stuck. You can code your own break points. How?
Imagine you are processing a large number of records (lets make
it a cool half million). The process hangs or goes bang every time
the process reaches record number 486,457, just when you're
almost done! The problem is that you are using a key and filters
and this means that physical record number 486,457 is very likely
not the cause. Give the debugger some embedded code to check for.
In ThisProcess.TakeRecord, you can write the safe code
in Listing 2.
?IF Count# = 486457 ? i" = 'Set hard break here' ?ELSE ? Count# += 1 ?END
Since this code is for the debugger only, there's no problem with using implicit variables. It really is throw-away code. So set your hard break on the string and choose Go. When the condition is met (486,457th record in sequence), the debugger becomes active. Now go find which record this is (by adding variables to the watch window) and use tools such as TopScan to find out what is this record's problem. I have run into problems with programs I wrote only to find the source of the problem was bad data, so be on the lookout for it.
It's your turn to try this. How would you write conditional code to find out when a value changes? Sometimes you need to test for a condition that should not change, but it does.
This technique takes almost no overhead, and if you leave it in your code it really doesn't do any harm. The trick is to keep debug code totally encapsulated. It is far more dangerous to use real variables as other processes can come along and change the values.
If you really do not like using implicits (and there are a few of you out there), then add variables in a suitable data embed. That is equally as safe.
TIP: The "?" symbol means that this code is compiled only when debug is turned on. If you switch to release mode, then the code is not compiled at all.
I certainly hope that you got some good information out of this article. You should now see that the Clarion debugger is a more useful tool than most people know.
I showed you how to step through code that you are interested in. This means that you do not have to step through all manner of code that has nothing to do with your problem, just the lines that you need to look at.
You can use the debugger to test logic flows in your code (especially those that may not be run that often).
I showed some setup tips that make getting ready to use the debugger easier to use. I also explained why STOP() and MESSAGE() are not really designed for debugging and why they are not safe to do so.
I enjoyed writing up my notes and sharing these with you. I am still wearing that smile. In all of these discussions, I have to ask you one last question:
Did anyone see me debug the ABC classes?
Russ Eggen has been using Clarion since 1986. Until about 1996, he was using it for business applications, mostly accounting programs. Afterwards he joined Topspeed as a consultant, and later as an instructor. He was a founding member of SoftVelocity when that company formed from Topspeed in May 2000. He left SoftVelocity in January 2001 and now works for his own company, RadFusion Inc. He still teaches and lectures, and is currently working on a new book and setting up a local Clarion classroom. Russ enjoys flying, scuba, and applied philosophy, and with great effort you might coax him into political discussions.
Copyright © 1999-2008 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $184
(includes all back issues since '99)
Renewals from $134
Two years: $274
Renewals from $224