Hardcore Clarion - Debug Techniques and IPC

by Paul Attryde

Published 1998-11-01    Printer-friendly version

NOTE: Clarion Magazine was not able to obtain an archive of the original source code for this article. If you have the source, we'd very much appreciate it if you would email it to us so we can post it for other readers.

Debugging code, like writing the code in the first place, is a very personal thing. We have all developed our own debugging techniques, and tend to fall back on them when the need arises.

Those of you who have attended the recent Clarion roadshow will be aware of at least 3 techniques that were described during the show. For those of you who couldn't attend, or have already forgotten, they were:

  1. Use STOP or MESSAGE statements in your code. Very obvious, but ultimately intrusive - it actually affects your code, and I've had more than one error go away when I added a MESSAGE statement into my code.
  2. Display your debug message as the caption of the current window or application frame. Quite easy to use (just use Window{Prop:Text}) and you get the same result.
  3. Use the Clarion PUTINI function to write debug messages into an INI file. Very useful in debugging multi-threaded applications.

This month, I'm going to describe a fourth.

Before I do, though, a little bit of history and explanation is required. Back in the days when Windows 95 was just a twinkle in our eyes and a 386 machine was considered top of the line, the chances are we used Windows 3.1. To develop code we used the Windows Software Development Kit, which was (being honest) a rather cumbersome set of tools and utilities which were used in conjunction with other products such as Microsoft C 6.0, or Borland Turbo C.

A part of the SDK was a little known utility called DBWin.

Its primary use ws as a debugging tool and it had, among other things, the ability to view the results of calls to the Windows API function OutputDebugString.

This little-known function has a rather simple purpose - it took a string parameter and output it to an application capable of viewing the output, which is normally considered to be your debugger.

DBWin is that application, and it's not a debugger - it simply has the ability to capture the output of OutputDebugString and display it in an SDI window and redirect it to a serial port, or display it on a secondary monitor. It can also show kernel errors, such as the freeing of invalid pointers (which the Clarion environment seems to do a lot).

The first thing we need to do is prototype OutputDebugString. That's the easy bit (for 16-bit just remove the NAME attribute):

OutputDebugString(ULong),Raw,Pascal,Name('OutputDebugStringA')

To use it, you simply do this:

Loc:TempCString = 'Hello World!'
OutputDebugString(Address(Loc:TempCString))

And, hey presto, your debug string appears wherever you want it. Now, that's all fine and dandy as long as you're writing a 16-bit application but, unfortunately, things get a little bit confusing if you're writing a 32-bit application. As the following table shows, when you call OutputDebugString in a 32-bit application running on a 16-bit OS, it doesn't work.

  Windows 95 Windows 98 Windows NT
16-bit: application OK OK OK
32-bit application Fail Fail OK

The reason for this is quite simple: The developers of Windows 95 (and Windows 98) didn't implement it the same way as the developers of Windows NT.

The other thing to remember is that with the advent of new development IDEs such as Visual Basic, Visual C and Visual J++, the need for a utility such as DBWin has all but disappeared. Using those languages and development environments, with their built-in debuggers and other tools, there is little need to resort to calling OutputDebugString, and its use (if it was ever widely used in the first place) has dwindled.

For the rest of us, using OutputDebugString is still a valid aid to debugging. Over the years there have been a number of 32-bit DBWin equivalents written to capture the output from 32-bit applications.

Accompanying this months article are three such viewers - DBMON, DBWIN32 and DBWIN32C - which you can use to view your debug messages.

DBMON and DBWIN32 both capture the result of 32-bit applications calling OutputDebugString under Windows NT. The first is a rudimentary console application, and the second a full-fledged Windows application, and they both use IPC (inter-process communication) to work.

DBWIN32C is similar to DBMON, but uses a VXD, which must be installed on the machine you are going to debug on. Because of the VXD it only works under Windows 95, and doesn't require IPC to capture the output from OutputDebugString.

So, if all you do is test under Windows 95 and you don't mind installing yet another VXD, then you're all ready to start using OutputDebugString in your application. If, on the other hand, you need to test under Windows 95 and Windows NT then you'll have to use DBMON and DBWIN32.

So how does it work?

As I said earlier, DBMON and DBWIN32 work under Windows NT because they and the NT kernel use IPC to communicate. When you call OutputDebugString, the kernel writes the string into shared memory, and the viewer retrieves the string and displays it as directed.

To use OutputDebugString under Windows 95/98, we have to roll our own IPC interface to these applications in order to have our string appear correctly. We will still want to call OutputDebugString, just in case there is also a debugger running (such as Soft/ICE) which will also trap the output.

What we're going to do is write a new function, PrintDebugString, that is a wrapper around the existing API function. Out new function just takes one string parameter (Prm:DataString), and doesn't return anything.

The first thing we need to do is call OutputDebugString - that takes care of 16-bit applications, and 32-bit applications running under Windows NT. Now comes the fun part - 32-bit applications under Windows 95.

When we pass the data to the viewer applications, they are already expecting the data in a certain format. This is dictated by the viewer application, which in turn is dictated by the Windows NT implementation. They expect a 4 byte process ID, followed by a string containing the data to display. In my implementation, I limit the string you can display to 80 characters - although I believe the viewers can accept a string up to 512 bytes long, but that's far too many for my needs. I'll leave it up to you if you need to display more. To pass the data, I'll set up a group and use memcpy to copy the 84 bytes into the correct memory address.

SharedMemory     GROUP,PRE(SM)
ProcessID          LONG
OutputString       CSTRING(80)
                 END

Because C has a capability that Clarion doesn't - pointers - we'll need to use memcpy to copy the data into the correct memory address. When dealing with the Windows API it's not uncommon to have to use memcpy to set up a pointer, or dereference a return value, just so you can call an API function and pass/receive parameters correctly.

We first need to work out if we're running under Windows 95 or Windows NT, and if we are capable of sending the data string. We do that with the GetVersion() function:

GetVersion(),LONG,RAW,PASCAL

And we use it as follows:

Loc:Version = GetVersion()
IF BAND(Loc:Version,10000000000000000000000000000000B)
  ! Windows 95 or Windows 98
  IF LEN(Clip(Prm:DataString)) < 80
    ! Hand-roll our own interface
    DO OurOwnInterface
  ELSE
    ! The string is too long
    MESSAGE('The string is too long!')
  END
ELSE
  ! Windows NT
  ! Do nothing - we've already called OutputDebugString,
  ! so we don't have to do anything else
End

It's inside the OurOwnInterface routine that the interesting code happens. We first initialise all of the handles we will be using to 0. When we quit the routine, we close all handles that aren't 0.

Loc:heventDBWIN = 0
Loc:heventData = 0
Loc:hSharedFile = 0
Loc:lpszSharedMem = 0

Next, we have to make sure that an event viewer is ready to accept data. We do this by obtaining the handle of an object that the viewer application has already created. If we can't get a handle to the object, then the viewer application is obviously not running.

Loc:DBBR = 'DBWIN_BUFFER_READY'
Loc:DBBR_Ptr = ADDRESS(Loc:DBBR)
Loc:heventDBWIN = OpenEvent(EVENT_MODIFY_STATE, FALSE, Loc:DBBR_Ptr)

We then try and obtain a handle to the data object that we will use to signal the viewer application that the data is ready to be read.

Loc:DBDR = 'DBWIN_DATA_READY'
Loc:DBDR_Ptr = ADDRESS(Loc:DBDR)
Loc:heventData = OpenEvent(EVENT_MODIFY_STATE, FALSE, Loc:DBDR_Ptr);

Once we've done that, we need to set up the shared memory we'll be using to actually pass the data. The shared memory is actually a file-mapping object that we create and map into our own address space. It's into this memory that we'll put the data we're going to write.

Loc:DBB = 'DBWIN_BUFFER'
Loc:DBB_Ptr = Address(Loc:DBB)
Loc:hSharedFile = CreateFileMapping(-1, 0, |
                  PAGE_READWRITE, 0, 4096, Loc:DBB_Ptr)
IF Loc:hSharedFile <> 0
  Loc:lpszSharedMem = MapViewOfFile(Loc:hSharedFile, |
  FILE_MAP_WRITE, 0, 0, 512);
END

Once we've done all that, we wait for the viewer application to be ready to receive data. The viewer application is setting the state of the object on a regular basis, so it shouldn't be too long (IE < 1 second) before it signals it's ready to receive the data.

x# = WaitForSingleObject(Loc:heventDBWIN, INFINITE);

Once the viewer application is ready to receive data, all that's left to do is copy the data into the shared memory location and tell the viewer that the data's there. We've already got a group structure set up, so we just populate it with the correct values and use MEMCPY to copy it into the shared memory area.

SM:ProcessID = GetCurrentProcessID()
SM:OutputString = CLIP(Prm:DataString) & CHR(0)
MEMCPY(Loc:lpszSharedMem,Address(SharedMemory),|
       Size(SharedMemory))
x# = SetEvent(Loc:heventData)

The only thing left to do now is free up all the handles and shared memory we used.

IF Loc:lpszSharedMem <> 0
  x# = UnmapViewOfFile(Loc:lpszSharedMem)
END
IF Loc:hSharedFile <> 0
  x# = CloseHandle(Loc:hSharedFile)
END
IF Loc:heventData <> 0
  x# = CloseHandle(Loc:heventData)
END
IF Loc:heventDBWIN <> 0
  x# = CloseHandle(Loc:heventDBWIN)
END

And that's basically it. Of course, I haven't shown all the error checking for null handles, or the error messages that get displayed, but I'm sure you get the general idea.

By using OutputDebugString you have a simple non-intrusive way of determining your programs status. It doesn't hit the hard drive like a trace file does, so it's quicker. You can, if you want to, leave the calls in your code all the time. The performance hit is negligible, and unless your client also has a viewer (which is unlikely) he won't know they even exist. If errors occur, simply ship him the viewer and he can return the results.

On an ending note, you can also view the results of OutputDebugString in the 16-bit Clarion debugger. Under the Options|Settings menu option there's a checkbox marked 'Disable Kernel messages'. If you enable (uncheck) the box, every call to OutputDebugString will appear in a message box. It's quite (OK, very) annoying because you have to close the message before you can continue, but it does give you another way to see the results. Unfortunately I don't know of anything similar in the 32-bit debugger - unless anyone knows differently, it'll just have to remain on our wish-list for the time being.

Summary

OutputDebugString and its associated viewers are yet more tools you can use to track down your bugs. It's simple to use, doesn't impact performance and doesn't affect the underlying code. For those reasons alone, it's got to be worth considering.

Next Time...

Message filters and keyboard hooks.

Printer-friendly version

Reader Comments

To add a comment to this article you must log in.

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $169

(includes all back issues since '99)

Renewals from $119

Two years: $269

Renewals from $219

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links