|
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.
|