1-by-1 black pixel for creating lines
1-by-1 black pixel for creating lines
EricGiguere.com > Books > Palm Database Programming > Chapter 3
Printer-friendly version Set your preferences
Read my blogs on AdSense and affiliate marketing
 
 
  
Learn about these ads

Palm Database Programming — The Electronic Version

Chapter 3: Development Tools and Software Development Kits

This material was published in 1999. See the free Palm OS Programming online course I developed for CodeWarriorU for some updated material.

Prev Chapter | Prev Section | Next Section | Contents

C/C++ Programming Issues

This section lists a few of the limitations you have to be aware of when programming in C or C++ for the Palm Computing platform.

The C Runtime Library

The C programming language is very concise. Unlike other languages, there are no built-in functions in C. Instead, C (and C++) uses a set of standard routines referred to as the C runtime library, which defines familiar routines such as printf, strcpy, malloc, and so on. As of Release 5, however, CodeWarrior does not provide a standard C runtime library for use with Palm OS. The GNU tools do provide a standard library, but unless you're sure you'll never want to move your project to CodeWarrior, you're better off not using any of its functions.

Note that the C runtime library isn't required for C/C++ programming  you can program without the runtime library if the operating system provides the functions you need or you write your own functions. For example, the Palm OS String Manager has functions for comparing, copying, and converting strings that are very similar to the string functions in the C runtime library like strcmp, strcpy, and atoi. Instead of writing your own versions of functions, use the equivalent Palm OS functions whenever possible  since the Palm OS functions are in ROM, less RAM is required by the application. See Table 3.2 for a list of equivalent Palm OS functions.

Table 3.2 Selected Palm OS Equivalents to C Runtime Library Functions

C Runtime Library Function

Palm OS Equivalent

atoi

StrAToI

bsearch

SysBinarySearch

clock

TimGetTicks

free

MemPtrFree

itoa

StrIToA, StrIToH

malloc

MemPtrNew

memcmp

MemCmp

qsort

SysQSort, SysInsertionSort

rand

SysRandom

realloc

MemPtrSize

sprintf

StrPrintF

strcat

StrCat

strchr

StrChr

strcmp

StrCompare

strcpy

StrCopy

strerror

SysErrString

stricmp

StrCaselessCompare

strlen

StrLen

strncat

StrNCat

strncmp

StrNCompare

strncpy

StrNCopy

strnicmp

StrNCaselessCompare

strstr

StrStr, FindStrInStr

strlwr

StrToLower

vsprintf

StrVPrintF

 

Assertions and Error Messages

An assertion is a runtime sanity check often used when developing an application, used to ensure that specific conditions or assumptions hold true. If an assertion fails, a message is displayed with details about the failure, including the line number and filename of the failure. On most platforms, assertions are available via macros defined in the header file <assert.h>. On the Palm Computing platform, assertions are macros defined in the header file <System/ErrorMgr.h>.

The most basic macro is ErrDisplay, an unconditional assertion, which takes a string as its only parameter:

      
ErrDisplay( "Find is not yet supported" );

When ErrDisplay is encountered at run time, Palm OS displays a dialog similar to the one shown in Figure 3.12, replacing the message with the argument to ErrDisplay and adjusting the line number and filename to reflect the source of the message. The only way to dismiss the dialog is to reset the device, which of course stops the application. When running the application on the Palm OS Emulator, discussed later in this chapter, the dialog is as shown in Figure 3.13, and you're given the opportunity to debug the application or ignore the message instead of just resetting the (emulated) device.

Figure 3.12 A fatal error message.

Figure 3.13 A fatal error message when using the emulator.

ErrDisplay is only enabled messages if the error-checking level, as defined by the ERROR_CHECK_LEVEL macro, is set to ERROR_CHECK_PARTIAL or ERROR_CHECK_FULL at compile time. The error-checking levels are defined as follows:

      
#define ERROR_CHECK_NONE    0
#define ERROR_CHECK_PARTIAL 1
#define ERROR_CHECK_FULL    2

If not otherwise defined, ERROR_CHECK_LEVEL is set to ERROR_CHECK_FULL in <BuildRules.h>, which is called from <Common.h>, which is ultimately included from <Pilot.h>. To disable the ErrDisplay macro and other error checking, set ERROR_CHECK_LEVEL to 0 (ERROR_CHECK_NONE) before including <Pilot.h>.

Error Levels and Precompiled Headers

CodeWarrior supports precompiled headers. A precompiled header is a header that is processed in a separate step and converted to a compact binary representation that can be quickly loaded by the compiler. Including precompiled headers (ending in .mch) in place of normal headers (ending in .h) greatly speeds the time required to compile most source files.

The <Pilot.h> header file includes the precompiled header <Pilot.h.mch> (for C) or <Pilot.h++.mch> (for C++) unless the macro PILOT_PRECOMPILED_HEADERS_OFF is defined; so by default any macros you define before including <Pilot.h> have no effect on what's actually included by <Pilot.h>. To redefine the error level, for example, either define PILOT_PRECOMPILED_HEADERS_OFF in addition to defining ERROR_CHECK_LEVEL or else rebuild the precompiled header files by following the directions in the <Pilot.h> source and the CodeWarrior manual.

Conditional assertions are done using the ErrFatalDisplayIf and ErrNonFatalDisplayIf macros:

      
#define ErrFatalDisplayIf( condition, message )
#define ErrNonFatalDisplayIf( condition, message )

Both macros take two arguments: a Boolean expression and a string. At run time the expression is evaluated and if the result is true the message is displayed just as if ErrDisplay had been called. Note that this is different from a traditional assertion, where the message is displayed if the expression returns false. If you're used to the traditional form of assertions, you can easily define another macro:

      
#define assert(expr,msg) ErrFatalDisplayIf(!(expr),msg)

The difference between ErrFatalDisplayIf and ErrNonFatalDisplayIf is that ErrFatalDisplayIf is enabled if the error-checking level is ERROR_CHECK_FULL or ERROR_CHECK_PARTIAL, while ErrNonFatalDisplayIf is enabled only with the ERROR_CHECK_FULL level. Neither is enabled if the level is ERROR_CHECK_NONE.

ErrDisplay, ErrFatalDisplayIf, and ErrNonFatalDisplayIf all use the function ErrDisplayFileLineMsg to display the error dialog.

Palm OS 3.2 adds an ErrAlert function that displays a message from a predefined set of strings, and when dismissed allows the application to continue running. This kind of informational dialog is easily implemented on prior platforms using an alert resource, as described in Chapter 4.

Exception Handling

CodeWarrior implements full C++ exception handling, although it's off by default so you'll have to turn it on from the Target Settings window. The Error Manager also defines macros and functions in <System/ErrorMgr.h> for implementing a restricted form of exception handling that can be used with C and C++, as demonstrated here:

      
ErrTry {
    VoidPtr p = MemPtrNew( 5000 );
    if( p == NULL ){
        // You can throw any long (32-bit) integer value
        // You can throw it from any function called within
        // the "try" block
        ErrThrow( memErrNotEnoughSpace );
    }
}
ErrCatch( err ) {
    // This code only executes if an exception is thrown.
    // It defines "err" as a long that holds the exception
    // value.
    if( err == memErrNotEnoughSpace ){
        ErrDisplay( "Memory allocation failed" );
    }
} ErrEndCatch

Refer to the Palm OS Programmer's Companion for details.

Callbacks and GCC

A callback function is a way for the operating system to call back into your application while performing an operation. For example, you can ask Palm OS to let you draw the individual items in a list. You do this by registering the callback function with Palm OS, which then calls the function as required.

Callback functions compiled with GCC require the use of special macros at the start and end of the function. These macros perform some internal housekeeping to ensure that the callback function can access global and static data. These macros are not required by programs compiled with CodeWarrior, just those compiled with GCC. You would use the macros like this:

      
void CallbackFunction()
{
    #ifdef __GCC__
        CALLBACK_PROLOGUE
    #endif

    ....

     #ifdef __GCC__
        CALLBACK_EPILOGUE
    #endif
}

Use the CALLBACK_PROLOGUE macro before attempting to access any global or static data. The macros are defined in the file Callback.h, which is not included with GCC but can be found on the CD-ROM and on the Web site for this book. Callback.h was written by Ian Goldberg, who has graciously allowed us to include it with this book. The code for Callback.h is shown in Figure 3.14.

      
#ifndef __CALLBACK_H__
#define __CALLBACK_H__

/* This is a workaround for a bug in the current version of gcc:

   gcc assumes that no one will touch %a4 after it is set up in crt0.o.
   This isn't true if a function is called as a callback by something
   that wasn't compiled by gcc (such as FrmCloseAllForms()). It may also
   not be true if it is used as a callback by something in a different
   shared library.

   We really want a function attribute "callback" which will 
   insert this prologue and epilogue automatically.     

   - Ian Goldberg
     iang@cs.berkeley.edu
     http://now.cs.berkeley.edu/~iang/
*/

register void *reg_a4 asm("%a4");

#define CALLBACK_PROLOGUE \
void *save_a4=reg_a4; \
asm("move.l %%a5,%%a4; sub.l #edata,%%a4" : :);

#define CALLBACK_EPILOGUE reg_a4 = save_a4;

#endif

Figure 3.14 The Callback.h header file for use with GCC.

C++ Programming Issues

C++ programmers have a few more issues to deal with than C programmers. The first question you should ask yourself is if you even need to use C++ to do your programming. C++ programs can be large if you're not careful about how you structure and use your classes.

Object Allocation

By default, the new operator allocates fixed blocks of memory from the dynamic heap and then invokes the constructor to initialize the newly created object. To use moveable blocks (to reduce heap fragmentation) or blocks allocated from a storage heap, overload the new operator as follows:

      
// Add this to a header file
extern void *operator new( unsigned long size, void *mem );

// Add this to a source file
void *operator new( unsigned long, void *mem )
{
    return mem;
}

This is the placement variant of the new operator. It doesn't actually allocate memory, it just returns the pointer that was passed to it as the second argument (the first argument to operator new is always the size of the memory block to allocate). If you look at the code that a C++ compiler generates, you'll see that immediately after a call to operator new the compiler invokes the constructor for the newly allocated object. Our overloaded operator doesn't allocate any memory, but the compiler still invokes the constructor. You use the placement variant like this:

      
char       memPtr[ sizeof( SomeClass ) ];
SomeClass *object = new( memPtr ) SomeClass();

Here's a more concrete example using some of the Palm OS Memory Manager APIs:


class MyClass {
    public:
        MyClass( int v ) : val( v ) {}
    
        int getValue() { return val; }

    private:
        int val;
};

// Allocate chunk to hold an instance
VoidHand hdl = MemHandleNew( sizeof( MyClass ) );
VoidPtr  mem = MemHandleLock( hdl );

// Invoke the constructor
MyClass *c = new( mem ) MyClass( 13 );

// Now unlock it...
MemHandleUnlock( mem );

When using the placement variant of new, don't delete the object with delete, instead invoke the destructor directly and then delete the memory as appropriate:

      
MyClass *c = (MyClass *) MemHandleLock( hdl );
c->~MyClass(); // invoke destructor
MemHandleUnlock( mem );
MemHandleFree( hdl );

For moveable blocks, always lock the block before accessing the object:

      
MyClass *c = (MyClass *) MemHandleLock( hdl );
int x = c->getValue();
MemHandleUnlock( hdl );

Be sure to use the correct sizes when allocating memory and don't forget to free the memory when you're done with it.

Virtual Function Strategies

In C++, virtual functions are implemented using dispatch tables, one table for each class that has virtual functions. These dispatch tables are considered to be global data, but global data is not always available, as we'll see in the next chapter. If you use virtual functions when global data is not available, your program will crash. You can try this for yourself quite easily by defining a simple class with a single virtual function:

      
class CrashMe {
    public:
        CrashMe() : v( 0 ) {}
        virtual void crash( int val ) { v = val; }
    private:
        int v;
};

Then add the following code to the PilotMain function of an application 'you're working with to create a new instance of the class and invoke the virtual function:

      
CrashMe *crashMe = new CrashMe();
crashMe->crash( 10 );

Compile and run the application. To make the crash occur you need to invoke the application without its global data: Switch to another application such as the Address Book and then perform a global Find with a random string of data. As part of its processing, the Find operation will briefly start the application, but without initializing any of its global data, causing a crash.

This restriction makes it hard to write base classes that define an interface (i.e., an abstract class) or some common behavior that is overridden by derived classes. You'll need to choose one of the following approaches.

Avoid or limit the use of virtual functions. Don't use any classes with virtual functions (including virtual destructors), or else limit their use to contexts where you know global data is available, as discussed in the next chapter. Unfortunately, this is quite limiting if you like to make extensive use of virtual functions, because it's not unusual for your application to be called without access to its global data.

Simulate virtual functions with dispatch tables. There's nothing particularly magic about virtual functions  with a bit of work you can simulate what the compiler does without relying on the global data block. First, though, you need to understand how virtual functions work.

The compiler creates a virtual function dispatch table, or vtable, for each class that has virtual functions. The vtable is akin to static data, in that there is only one copy of the table per class. Each entry in a vtable points to one of the virtual functions in the class. The vtable of a derived class is based on the vtable of the base class, with additional entries for any new (noninherited) virtual functions. If a derived class does not override a particular virtual function, that function's entry in the vtable is copied from the base class' vtable, otherwise the address of the overriding function is stored in the vtable. When an instance of a class is created, the compiler silently adds a data member that points to the vtable for the class. When a virtual function is invoked, the correct function is located using an offset into the vtable.

To simulate virtual functions without access to global data, you need a vtable allocated on the stack or in dynamic memory. We can't replace the real vtable that the compiler generates, so we remove any virtual functions and use our own dispatch table to achieve the same effect. It's easier to demonstrate this with an example. Consider the classes AV, BV, and CV defined in Figure 3.15, where BV derives from AV and CV derives from BV. AV defines two virtual functions, Func1 and Func2, and BV defines a third, Func3. BV overrides Func1 and CV overrides Func2 and Func3. We're going to convert these into classes A, B, and C, classes that use dispatch tables to simulate vtables.

      
// 
// Three simple classes with virtual functions.
//

class AV {
    public:
        AV() : value( 1 ) {}
        ~AV() {}
    
        virtual int Func1() { return value; }
        virtual int Func2( int val ) 
                            { return 2 * val; }
    
    protected:
        int value;
};

class BV : public AV {
    public: 
        BV() { value = 2; }
        ~BV() {}

        int          Func1() { return 5 * value; }
        virtual bool Func3( int val1, int val2 ) 
                             { return val1 > val2; }
};

class CV : public BV {
    public:
        CV() { value = 3; }
        ~CV() {}
    
        int  Func2( int val ) { return 2 * val + 1; }
        bool Func3( int val1, int val2 ) 
                              { return !BV::Func3( val1, val2 ); }
};

Figure 3.15 A simple example of virtual functions.

The first step in transforming the AV class into the A class is to change each virtual function (for example, Func1) into two separate functions: a nonvirtual function (Func1) and a static function (virtFunc1). The A class is shown in Figure 3.16. The nonvirtual function is public and is declared identically to the original virtual function  it presents the public interface that other classes will invoke. The static function is protected and has an additional parameter to it and a different name, but is otherwise identical to the original virtual function. (Note that static functions and static data are stored in two different areas: static data is stored as part of the global memory block for the application, while static functions live with the rest of the code in the storage heap. Restrictions on accessing global and static data do not affect access to static functions.) The additional parameter ("self") is a pointer to an object of class A and takes the place of the "this" pointer that is implicit to member functions. The code that was in the definition of the original virtual function is moved into the new static function, altered suitably to use the "self" pointer.

The next step is to implement a dispatch table. A dispatch table is just a series of function pointers, easily represented by a structure. For convenience, we define typedef equivalents for the various function pointers; however, this isn't necessary. The important part is the definition of the dispatch table, DispatchTableA, and the addition of a reference to the dispatch table as member data. The dispatch table has an entry in it for each of the virtual functions originally defined in class AV. The code for each nonvirtual equivalent to the original virtual functions (i.e., the new Func1) uses the dispatch table to invoke the "real" virtual function. In Figure 3.16 the nonvirtual functions perform some error checking to ensure that the dispatch table entry they're using isn't null, but that isn't strictly necessary  you could replace that code with assertions.

The final step is to fill in the dispatch table and to modify the constructor for the original class. To fill in the dispatch table we define a static function called FillDispatchTable that takes a reference to a DispatchTableA. FillDispatchTable then fills it with the addresses of the static function equivalents to the original virtual functions. The constructor is modified to take a reference to a dispatch table and store it as member data. The transformation of class AV into class A is now complete.

      
// Transform class AV into a class without virtual functions
// but that uses a dispatch table to achieve the same effect.

class A {
    protected:
        // Some typedefs, for convenience only.

        typedef int (*Func1Dispatch)( A *self );
        typedef int (*Func2Dispatch)( A *self, int val );
        
        // The dispatch table: one entry for each virtual
        // function.

        struct DispatchTableA {
            Func1Dispatch func1;
            Func2Dispatch func2;
        };

        // Member data: a reference to the dispatch table.
        
        const DispatchTableA & dispatchTable;

    public:        
        // Constructor takes a reference to the dispatch table.

        A( const DispatchTableA & table );
        
        // Destructor is not changed.

        ~A() {}

        // Virtual functions become regular, nonvirtual functions.
    
        int Func1();
        int Func2( int val );

        // Function to fill the dispatch table.
        
        static void FillDispatchTable( DispatchTableA & table );
        
    protected:
        int        value;

        // Static equivalents of the original virtual functions.
        // Note the extra "self" parameter.
        
        static int virtFunc1( A *self );
        static int virtFunc2( A *self, int val );
};

// Constructor changed to store away the reference to
// the dispatch table.

A::A( const DispatchTableA & table )
: dispatchTable( table )
, value( 1 )
{
}

// Places addresses of static functions into the
// dispatch table.

void A::FillDispatchTable( DispatchTableA & table )
{
    table.func1 = virtFunc1;
    table.func2 = virtFunc2;
}

// Originally a virtual function, now a nonvirtual function
// that uses the dispatch table to invoke the correct static
// equivalent, passing the "this" pointer as the first argument.

int A::Func1()
{
    return( dispatchTable.func1 != NULL ? 
            (*dispatchTable.func1)( this ) : 0 );
}

// Ditto.

int A::Func2( int val )
{
    return( dispatchTable.func2 != NULL ? 
            (*dispatchTable.func2)( this, val ) : 0 );
}

// The code for the original virtual Func1, transformed to use the
// "self" parameter in place of the implicit "this" parameter.

int A::virtFunc1( A *self )
{
    return self->value;
}

// The code for the original virtual Func2.

int A::virtFunc2( A *self, int val )
{
    return 2 * val;
}

Figure 3.16 Transforming class AV into class A.

Similar transformations are then used to convert class BV into B and class CV into C, as shown in Figure 3.17. Inheritance is respected, so B derives from A and C derives from B, just like BV derives from AV and CV from BV. B and C define new dispatch tables, DispatchTableB and DispatchTableC, which mirror the inheritance tree: DispatchTableB derives from DispatchTableA and DispatchTableC derives from DispatchTableB. However, only new virtual functions (as opposed to overridden virtual functions) are added to these dispatch tables. Class B defines a new virtual function Func3, so an entry is added to DispatchTableB, but no virtual functions are defined in class C, so DispatchTableC does not include any additional members. New virtual functions are split into two functions as before, but overridden functions are just converted into static functions. Both classes define FillDispatchTable static functions to fill the dispatch table. Each FillDispatchTable first calls the base class's FillDispatchTable and then sets the values for each new or overridden virtual function, leaving the other entries untouched. Finally, the constructors for each class are modified to take a reference to a dispatch table and pass that reference along to the base class's constructor.

Virtual destructors can also be simulated with this code: just move all the code from the destructors into separate routines that you call separately before deleting an object.

      
// Transform class BV into a class B that doesn't use virtual
// functions and inherits from class A instead of class AV.

class B : public A {
    protected:
        typedef bool (*Func3Dispatch)( B *self, int val1, int val2 );

        // Define a new dispatch table structure.  This one inherits
        // from the base class's dispatch table and adds a single
        // entry  overridden functions are not added, only new
        // virtual functions.

        struct DispatchTableB : public DispatchTableA {
            Func3Dispatch func3;
        };    
    
    public:
        B( DispatchTableB & dispatchTable );
        
        ~B() {}
        
        // Func3 from class BV becomes a regular, nonvirtual function.

        bool Func3( int val1, int val2 );
        
        static void FillDispatchTable( DispatchTableB & table );
        
    protected:
          
        // Class BV overrides Func1, so class B defines a static
        // function but no regular, nonvirtual equivalent.

        static int  virtFunc1( B *self );

        // The code for the new Func3 function.

        static bool virtFunc3( B *self, int val1, int val2 ); 
};

// Constructor must pass the dispatch table back up to the
// parent. This works because DispatchTableB derives from
// DispatchTableA.

B::B( DispatchTableB & table )
: A( table )
{
    value = 2;
}

// Fills in the dispatch table for class B. First calls
// the base class's routine, then overrides the Func1
// entry and adds the new Func3 entry.

void B::FillDispatchTable( DispatchTableB & table )
{
    A::FillDispatchTable( table );
    table.func1 = (Func1Dispatch) virtFunc1;
    table.func3 = virtFunc3;
}

// The overridden version of Func1.

int B::virtFunc1( B *self )
{
    return 5 * self->value;
}

// The new Func3 dispatcher.

bool B::Func3( int val1, int val2 )
{
    DispatchTableB & d = *((DispatchTableB *) &dispatchTable);
    
    return( d.func3 != NULL ? (*d.func3)( this, val1, val2 ) : 0 );
}

// The code for Func3.

bool B::virtFunc3( B *self, int val1, int val2 )
{
    return val1 > val2;
}

// Transform class CV into class C in much the same way.

class C : public B {
    protected:
        // Class CV does not add any new virtual functions,
        // so we don't need to add anything to the dispatch
        // table.  We could just use DispatchTableB, but this
        // is cleaner and makes it simpler to add virtual
        // functions later.
        struct DispatchTableC : public DispatchTableB {
        };
    
    public:
        C( DispatchTableC & dispatchTable );
        
        ~C();
        
        static void FillDispatchTable( DispatchTableC & table );
        
    protected:
        // Class overrides Func2 and Func3.
        static int  virtFunc2( C *self, int val );
        static bool virtFunc3( C *self, int val1, int val2 );
};

C::C( DispatchTableC & table )
: B( table )
{
    value = 3;
}

C::~C()
{
}

void C::FillDispatchTable( DispatchTableC & table )
{
    B::FillDispatchTable( table );
    table.func2 = (Func2Dispatch) virtFunc2;
    table.func3 = (Func3Dispatch) virtFunc3;
}

int C::virtFunc2( C *self, int val )
{
    return 2 * val + 1;
}

bool C::virtFunc3( C *self, int val1, int val2 )
{
    return !B::virtFunc3( self, val1, val2 );
}

Figure 3.17 Transforming classes BV and CV into classes B and C.

Using one of the classes A, B, or C is simple. First, you declare a variable to hold the dispatch table. You then initialize the dispatch table by calling FillDispatchTable for the appropriate class (for example, if you're using class B, you would declare a variable of type DispatchTableB and pass it as a parameter to B::FillDispatchTable). Then every time you create an instance of a class, you pass the initialized dispatch table as a parameter to its constructor. Then start using the object. A simple example is shown in Figure 3.18. If you run this code inside an application and step through it with the debugger you'll see that the classes behave exactly as if they had declared virtual functions.

      
// Declare space for the dispatch tables. 
// You could store them in dynamic memory
// if necessary.

A::DispatchTableA tableA;
B::DispatchTableB tableB;
C::DispatchTableC tableC;
  
// Fill the dispatch tables. You have to
// do this for each class you're going to
// instantiate.

A::FillDispatchTable( tableA );
B::FillDispatchTable( tableB );
C::FillDispatchTable( tableC );
  
// Instantiate the classes, passing in
// the dispatch tables. Make sure the tables
// match the class, that is, class B uses
// DispatchTableB only.

A a( tableA );
B b( tableB );
C c( tableC );
  
// Refer to the classes through their
// base classes, to prove that the virtual
// functions are working.

A & afromb( b );
A & afromc( c );
B & bfromc( c );

int x;
  
x = a.Func1();             // returns 1
x = a.Func2( 2 );          // returns 4

x = afromb.Func1();        // returns 10
x = afromb.Func2( 2 );     // returns 4
    
x = afromc.Func1();        // returns 15
x = afromc.Func2( 2 );     // returns 5

x = b.Func1();             // returns 10
x = b.Func2( 2 );          // returns 4
x = b.Func3( 3, 4 );       // returns 0 (false)
    
x = bfromc.Func1();        // returns 15
x = bfromc.Func2( 2 );     // returns 5
x = bfromc.Func3( 3, 4 );  // returns 1 (true)
  
x = c.Func1();             // returns 15
x = c.Func2( 2 );          // returns 5
x = c.Func3( 3, 4 );       // returns 1 (true)

Figure 3.18 Testing out the simulated virtual functions.

It's certainly more work to define classes this way, but it allows you to obtain the benefits of virtual functions in situations where you normally can't use them. There is a bit more overhead than using compiler-generated virtual functions, but it's not substantial. This technique is used in Chapter 4 with the Phone Book sample.

Simulate virtual functions with a cover class. Alternatively, you can write a cover class that stores a pointer to an object of a known type. In effect, the cover class is a proxy for the "real" or target object. Function calls on the cover class are redirected to the appropriate function on the target object, as shown in Figure 3.19.

      
class X {
    public:
        X() {}
        X( const X & ) {}
        int Func() { return 1; }
};

class Y {
    public:
        Y() {}
        Y( const Y & ) {}
        int Func() { return 2; }
};

class Z {
    public:
        Z() {}
        Z( const Z & ) {}
        int Func() { return 3; }
};

// Define a cover class for X, Y, and Z. The target
// object is passed in as an argument to the constructor
// and must exist as long as the cover object exists.

class Cover {
    private:
        enum classType { 
            TypeX, TypeY, TypeZ
        };
        classType  type;
        void      *obj;
    public:
        Cover( X & x ) : type( TypeX ), obj( &x ) {}
        Cover( Y & y ) : type( TypeY ), obj( &y ) {}
        Cover( Z & z ) : type( TypeZ ), obj( &z ) {}

        // Copy constructor just copies the pointer over

        Cover( const Cover & cov ) : type( cov.type ), obj( cov.obj ) {}

        // Assignment operator just copies the pointer over

        Cover& operator=( const Cover & cov ) {
            if( &cov != this ){
                type = cov.type;
                obj = cov.obj;
            }
            return *this;
        }

        int Func() {
            switch( type ){
                case TypeX:
                    return ((X *) obj)->Func();
                case TypeY:
                    return ((Y *) obj)->Func();
                case TypeZ:
                    return ((Z *) obj)->Func();
                default:
                    return 0; // shouldn't happen
            }
        }
};

Figure 3.19 Using cover classes.

Writing a cover class is not difficult, but you have to manage the ownership of the target object quite carefully. The example in Figure 3.19 assumes that somebody else creates the target object and that the target object remains valid while the cover object is valid. A more typical scenario is to have the cover class create and destroy the target object, as shown in Figure 3.20.

      
// Define a cover class for X, Y, and Z. The
// cover completely manages the creation of the
// objects.

class Cover {
    public:
        enum classType { 
            TypeX, TypeY, TypeZ
        };
    private:
        classType  type;
        void      *obj;
    public:
        // Constructor creates a new object.

        Cover( classType type ) : type( type ) {
            switch( type ){
                case TypeX:
                    obj = new X();
                    break;
                case TypeY:
                    obj = new Y();
                    break;
                case TypeZ:
                    obj = new Z();
                    break;
                default:
                    // error, throw exception or assert
                    break;
            }
        }

        // Copy constructor invokes the target object's
        // copy constructor to create a new target object.
        // An alternative scheme would use reference counting
        // to keep track of how many cover objects hold a 
        // reference to a particular target object.

        Cover( const Cover & cov ) : type( cov.type ) {
            switch( type ){
                case TypeX:
                    obj = new X( *((X *) cov.obj) );
                    break;
                case TypeY:
                    obj = new Y( *((Y *) cov.obj) );
                    break;
                case TypeZ:
                    obj = new Z( *((Z *) cov.obj) );
                    break;
                default:
                    // error, throw exception or assert
                    break;
            }
        }

        // Ditto for the assignment operator.

        Cover& operator=( const Cover & cov ) {
            if( &cov != this ){
                DeleteTarget();
                type = cov.type;
                switch( type ){
                    case TypeX:
                        obj = new X( *((X *) cov.obj) );
                        break;
                    case TypeY:
                        obj = new Y( *((Y *) cov.obj) );
                        break;
                    case TypeZ:
                        obj = new Z( *((Z *) cov.obj) );
                        break;
                }
            }
            return *this;
        }

        // Destructor destroys target object as well.

        ~Cover() {
            DeleteTarget();
        }

        int Func() {
            switch( type ){
                case TypeX:
                    return ((X *) obj)->Func();
                case TypeY:
                    return ((Y *) obj)->Func();
                case TypeZ:
                    return ((Z *) obj)->Func();
                default:
                    return 0; // shouldn't happen
            }
        }

    private:
        // To delete the target you have to cast
        // the generic pointer to a specific type so
        // that the compiler knows which destructor to call.

        void DeleteTarget() {
            switch( type ){
                case TypeX:
                    delete ((X *) obj);
                    break;
                case TypeY:
                    delete ((Y *) obj);
                    break;
                case TypeZ:
                    delete ((Z *) obj);
                    break;
            }
        }
};

Figure 3.20 A cover class that manages the target object.

No matter who manages the target object, be sure to define copy constructors and assignment operators in your cover classes to ensure that target objects are correctly copied when a cover object is itself copied.

Relaunch the application. The undocumented technique of relaunching the application using the SysAppLaunch function can be used in some situations to "recover" the application's global data. This technique is not sanctioned by Palm Computing and should be used sparingly. The idea is quite simple: When an application first starts, it checks to see if its global data is available. If not, it uses SysAppLaunch to call itself recursively with globals enabled, returning immediately once the recursive call returns. The recursively called application can then use virtual functions because globals are enabled. The technique is described in more detail in Chapter 4.

You should carefully consider your other options before using this technique, because it's unsupported and decreases the amount of available dynamic memory. It may be the only way, however, to add support for some common Palm operations such as global Find to applications that use code developed for other systems. The UltraLite-based Phone Book sample discussed in Chapter 8 uses this technique precisely for that reason.

Of course, if your application doesn't use virtual functions, then none of these strategies are necessary.

Multisegment Applications

Palm applications are normally limited to a single 64K code segment. What's worse, no jumps of greater than 32K (backward or forward) are allowed in the generated code. If the code segment is greater than 32K in size, it's conceivable that a function near the start of the code segment could call a function near the end of the code segment and cause an error when your application was linked. While you can certainly write useful Palm applications in 32K or less, at some point you'll want to write a larger application.

With CodeWarrior it's fairly simple to avoid the 32K jump limit by using the segments view of the project window to control the object code linking order. You control the relative order of the files by dragging them into new positions in the view. Group the files that call each other close together to avoid the 32K jump limit. You can also use pragmas (compiler directives, see the CodeWarrior documentation) in your source files to indicate which functions are to be grouped together into segments. A third alternative is to use the "smart code" model (see the CodeWarrior documentation), which tells CodeWarrior to simulate jumps longer than 32K by using a series of jumps. This bloats the code, however, so use it only when necessary.

If your application requires more than a single 64K code segment, start your project using the "Palm OS Multi-Segment" project stationery and follow the directions in the Multi-Segment Read Me.txt file.

If you use the GNU tools, the situation is more complicated; refer to the CD-ROM for instructions.

Floating-Point Support

Palm OS 1.0 did not support floating-point computations. If applications required floating-point capabilities they had to link in a library of routines for performing basic floating-point arithmetic. With Palm OS 2.0 and higher those routines are now part of the operating system and are called automatically in the generated code when the CodeWarrior compiler is used. If your application requires floating-point numbers, be sure to check that you're running on a Palm OS 2.0 machine or higher  see the next chapter for the code to do this. If you want to support Palm OS 1.0 users, you can still link in the floating-point support manually, your application will just be a bit larger.

GCC supports floating-point with its own emulation software. There are ways to call the system routines instead; refer to the CD-ROM for details.

Prev Chapter | Prev Section | Next Section | Contents

Copyright ©1999 by Eric Giguere. All rights reserved. From Palm Database Programming: The Complete Developer's Guide. Reprinted here with permission from the publisher. Please see the copyright and disclaimer notices for more details.

If you find the material useful, consider buying one of my books, linking to this site from your own site or in your weblog, or sending me a note.

Google Web www.ericgiguere.com   
1-by-1 black pixel for creating lines
 
Copyright ©2003-2012 Eric Giguere | Send mail about this page | About this site | Privacy policy
Site design and programming by Eric Giguere | Hosting by KGB Internet Solutions
This site is Java-powered
Other sites: The Unofficial AdSense Blog | Google Suggest Explorer | Invisible Fence Guide | Synclastic
This page was last modified on November 23, 2003
1-by-1 black pixel for creating lines
1-by-1 black pixel for creating lines