Documentation  |   Table of Contents   |  < Previous   |  Next >   |  Index

15    Debugging Strategies

Palm OS® Programmer's Companion

Volume I

     

You can use a Palm OS® system manager called the error manager to display unexpected runtime errors such as those that typically show up during program development. Final versions of applications or system software won't use the error manager.

The error manager API consists of a set of functions for displaying an alert with an error message, file name, and the line number where the error occurred. If a debugger is connected, it is entered when the error occurs.

The error manager also provides a "try and catch" mechanism that applications can use for handling such runtime errors as out of memory conditions, user input errors, etc.

This section helps you understand and use the error manager, discussing the following topics:

Displaying Development Errors
Using the Error Manager Macros
The Try-and-Catch Mechanism
Using the ARM Debug Nub
Summary of Debugging API

This chapter only describes programmatic debugging strategies; to learn how to use the available tools to debug your application, see the book Palm OS Programming Development Tools Guide.

Displaying Development Errors ^TOP^

The error manager provides some compiler macros that can be used in source code. These macros display a fatal alert dialog on the screen and provide buttons to reset the handheld or enter the debugger after the error is displayed. There are three macros: ErrDisplay, ErrFatalDisplayIf, and ErrNonFatalDisplayIf.

  • ErrDisplay always displays the error message on the screen.
  • ErrFatalDisplayIf and ErrNonFatalDisplayIf display the error message only if their first argument is TRUE.

The error manager uses the compiler define ERROR_CHECK_LEVEL to control the level of error messages displayed. You can set the value of the compiler define to control which level of error checking and display is compiled into the application. Three levels of error checking are supported: none, partial, and full.

If you set ERR_CHECK_LEVEL to...

The compiler...

ERROR_CHECK_NONE (0)

Doesn't compile in any error calls.

ERROR_CHECK_PARTIAL (1)

Compiles in only ErrDisplay and ErrFatalDisplayIf calls.

ERROR_CHECK_FULL (2)

Compiles in all three calls.

During development, it makes sense to set full error checking for early development, partial error checking during alpha and beta test periods, and no error checking for the final product. At partial error checking, only fatal errors are displayed; error conditions that are only possible are ignored under the assumption that the application developer is already aware of the condition and designed the software to operate that way.

Using the Error Manager Macros ^TOP^

Calls to the error manager to display errors are actually compiler macros that are conditionally compiled into your program. Most of the calls take a boolean parameter, which should be set to true to display the error, and a pointer to a text message to display if the condition is true.

Typically, the boolean parameter is an in-line expression that evaluates to true if there is an error condition. As a result, both the expression that evaluates the error condition and the message text are left out of the compiled code when error checking is turned off. You can call ErrFatalDisplayIf, or ErrDisplay, but using ErrFatalDisplayIf makes your source code look neater.

For example, assume your source code looks like this:


result = DoSomething(); 
ErrFatalDisplayIf (result < 0, 
"unexpected result from DoSomething"); 

With error checking turned on, this code displays an error alert dialog if the result from DoSomething() is less than 0. Besides the error message itself, this alert also shows the file name and line number of the source code that called the error manager. With error checking turned off, both the expression evaluation err < 0 and the error message text are left out of the compiled code.

The same net result can be achieved by the following code:


result = DoSomething(); 
#if ERROR_CHECK_LEVEL != ERROR_CHECK_NONE 
if (result < 0)  
ErrDisplay ("unexpected result from
DoSomething"); 
#endif 

However, this solution is longer and requires more work than simply calling ErrFatalDisplayIf. It also makes the source code harder to follow.

The Try-and-Catch Mechanism ^TOP^

The error manager is aware of the machine state of the Palm Powered handheld and can therefore correctly save and restore this state. The built-in try and catch of the compiler can't be used because it's machine dependent.

Try and catch is basically a neater way of implementing a goto if an error occurs. A typical way of handling errors in the middle of a routine is to go to the end of the routine as soon as an error occurs and have some general-purpose cleanup code at the end of every routine. Errors in nested routines are even trickier because the result code from every subroutine call must be checked before continuing.

When you set up a try/catch, you are providing the compiler with a place to jump to when an error occurs. You can go to that error handling routine at any time by calling ErrThrow. When the compiler sees the ErrThrow call, it performs a goto to your error handling code. The greatest advantage to calling ErrThrow, however, is for handling errors in nested subroutine calls.

Even if ErrThrow is called from a nested subroutine, execution immediately goes to the same error handling code in the higher-level call. The compiler and runtime environment automatically strip off the stack frames that were pushed onto the stack during the nesting process and go to the error handling section of the higher-level call. You no longer have to check for result codes after calling every subroutine; this greatly simplifies your source code and reduces its size.

Using the Try and Catch Mechanism ^TOP^

The following example illustrates the possible layout for a typical routine using the error manager's try and catch mechanism.

Listing 15.1  Try and Catch Mechanism Example


ErrTry { 
p = MemPtrNew(1000); 
if (!p) ErrThrow(errNoMemory); 
MemSet(p, 1000, 0); 
CreateTable(p); 
PrintTable(p); 
} 
 
ErrCatch(err) { 
// Recover or clean up after a failure in the  
// above Try block."err" is an int  
// identifying the reason for the failure. 
 
// You may call ErrThrow() if you want to  
// jump out to the next Catch block. 
 
// The code in this Catch block doesn't  
// execute if the above Try block completes 
// without a Throw. 
 
if (err == errNoMemory) 
ErrDisplay("Out of Memory"); 
else 
ErrDisplay("Some other error"); 
} ErrEndCatch 
// You must structure your code exactly as  
//above. You can't have an ErrTry without an  
//ErrCatch { } ErrEndCatch, or vice versa. 

Any call to ErrThrow within the ErrTry block results in control passing immediately to the ErrCatch block. Even if the subroutine CreateTable called ErrThrow, control would pass directly to the ErrCatch block. If the ErrTry block completes without calling ErrThrow, the ErrCatch block is not executed.

You can nest multiple ErrTry blocks. For example, if you wanted to perform some cleanup at the end of CreateTable in case of error,

  • Put ErrTry/ErrCatch blocks in CreateTable
  • Clean up in the ErrCatch block first
  • Call ErrThrow to jump to the top-level ErrCatch

Using the ARM Debug Nub ^TOP^

On an ARM-based device, the ARM debug nub allows the desktop-based debugger (such as PODS) to break, examine registers, and so forth in your ARM-native code. In order to debug your ARM-based code, your application needs to both activate the debugger nub and register your PNOs with the Palm OS Debugger.

For information on creating PNOs and ARM-native code, see Chapter 14, "PalmOS Garnet ARM Programming."

Activating the ARM Debugger Nub ^TOP^

The ARM debugger nub is not active on an ARM-based device until an application activates it. Your PNO must first enable the ARM debugger nub by calling the AdnDebugEnableSet() macro, as shown in Listing 15.2.

Listing 15.2  Activating the ARM debugger nub


	// Tell the debugger we want to enable full debugging 
	UInt32 flags = AdnDebugEnableGet(); 
	flags |= kAdnEnableMasterSwitch | kAdnEnableFullDebugging; 
	AdnDebugEnableSet(flags); 

Register with Palm OS Debugger ^TOP^

Palm OS Debugger needs to be able to resolve where each ARM subroutine (PNO) is located in memory. Your ARM subroutine must register with Palm OS Debugger by invoking the AdnDebugNativeRegister() macro. You need to invoke AdnDebugNativeRegister() after the code resource has been locked, typically on the first call into the ARM subroutine.

Prior to being unlocked your code should unregister itself by invoking the AdnDebugNativeUnregister() macro.

These macros do not act as breakpoints; they do not halt device operation. However, they may cause breakpoints to become resolved or unresolved.

Listing 15.3 shows a call to AdnDebugNativeRegister().

Listing 15.3  Registering a PNO with the debugger


	// Tell the debugger where our code lives in memory: 
	AdnDebugNativeRegister( 
				sysFileTApplication, appFileCreator, 
				NativeResourceType, NativeResourceID);  

The ARM subroutine needs to define appFileCreator, NativeResourceType, and NativeResourceID in order for Palm OS Debugger to locate the corresponding debug symbol file.

For example, if you have an ARMC resource with ID 0001, the debug symbol file is typically of the form ARMC0001.bin.elf. The ELF file must be in the same directory as the PRC file that contains the ARM subroutine you are debugging.

Summary of Debugging API ^TOP^