How ePalm Interfaces with PalmOS
Finally, we have established a toolchain which will at least generate code which is compatible with the m68k Dragonball processor, and we are able to assemble the various parts of an application into an uploadable database.
Now the meat of the work can begin--we must be able to interface with PalmOS's C structures, wrap its routines, tie into its callbacks--in other words, exploit SmartEiffel's Eiffel-to-C library to its fullest.
PalmOS is, at its heart, a bunch of ROM-based C functions, which pass around C structures (or pointers to same) as arguments, and operate on them in an old-fashioned "object-based" manner (meaning that many functions operate on structures, taking a pointer to the structure as the first argument, and returning an error code as the return value).
The first task, then, was creating a simple but reasonably
effective way to wrap C structures. The answer began with a root
class, C_STRUCT
, the contents of which are listed below:
indexing contents: "C structure creation/deletion and access" author: "Victor Putz, vputz@nyx.net, from ideas in C_STRUCTURE, elj-win32" completed: no tested: no class C_STRUCT inherit MEMORY redefine dispose end feature {ANY} -- creation from_shared_memory( p : POINTER ) is --create with already-allocated memory pointed to by p; when --collected, do not free the memory do is_shared := True opaque_pointer := p ensure is_shared opaque_pointer = p end from_orphaned_memory( p : POINTER ) is --create with already-allocated memory pointed to by p; when --collected, free the memory do is_shared := False opaque_pointer := p ensure not is_shared opaque_pointer = p end as_new_memory is --create, allocating memory as a new object --when collected, free the memory do is_shared := False opaque_pointer := c_struct_allocate_memory( structure_size ) c_struct_prepare_memory_as_new_object( opaque_pointer ); ensure not is_shared opaque_pointer /= Void end opaque_pointer : POINTER --pointer to the allocated memory is_null : BOOLEAN is do Result := opaque_pointer = c_null_pointer end is_not_null : BOOLEAN is do Result := not is_null end feature {NONE} -- misc memory management c_struct_allocate_memory( size : INTEGER ) : POINTER is --allocate memory for a C structure, and fill it with zeros require size >= 0 external "C inline" alias "(MemPtrNew( $size ))" end c_struct_free( pointer : POINTER ) is --free memory from a C structure external "C inline" alias "MemPtrFree( $pointer )" end c_struct_prepare_memory_as_new_object( pointer : POINTER ) is --redefine to do further memory setup do end dispose is --when collected, free memory if made as shared object do if ( not is_shared ) then if opaque_pointer.is_not_null then c_struct_free( opaque_pointer ) opaque_pointer := c_null_pointer end end end is_shared : BOOLEAN --whether or not the memory pointed to by opaque_pointer is shared structure_size : INTEGER is deferred end --size of the C structure c_null_pointer : POINTER is external "C inline" alias "NULL" end end
C_STRUCT
encapsulates all the needed features of a wrapped C
structure; it keeps an opaque pointer to the structure in memory,
keeps track (through is_shared
) of whether or not it "owns" the
structure (in other words whether the structure's memory is
managed by C or Eiffel), and has the ability to create a structure
from shared memory (created by C, shared with Eiffel), from
"orphaned" memory (created by C but handed off to Eiffel), or as
new memory (created by Eiffel, owned by Eiffel).
The memory occupied by the structure is always created and freed
via the MemPtrNew
and MemPtrFree
functions of PalmOS; these
effectively take the place of the traditional malloc
and free
functions. The dispose
method is redefined so that when the
object is collected, the memory occupied by the C structure is
freed automatically if it is owned by Eiffel.
This simple structure class serves as the base for all C
structures which are wrapped by PalmOS; the only deferred feature
is structure_size
, which is redefined by descendant classes
(typically, simply doing an inline C alias to sizeof( type )
).
This allows us to wrap PalmOS structures at a very low level, and possibly the lowest level appropriate for a PalmOS application is the event loop.
Many modern GUI-based operating systems operate on something of a callback mechanism-- in other words, the operating system itself handles event management and "calls" the program in progress. Application-specific event handlers then handle the events as they are passed in by the OS.
PalmOS, since it is designed primarily around a one-program-at-a-time mentality, uses a somewhat more antiquated but still valid model: an event loop. A simple C event loop may look like the following (from Rhodes/McKeehan's Palm Programming: The Developers' Guide, hereafter referred to as PPTDG):
static void EventLoop( void ) { EventType event; Word error; do { EvtGetEvent( &event, evtWaitForever ); if ( !SysHandleEvent( &event ) ) if ( !MenuHandleEvent( 0, &event, &error ) ) if ( !ApplicationHandleEvent( &event )) FrmDispatchEvent( &event ); } while (event.eType != appStopEvent ); }
Take a look at the simple loop; the device sits waiting "forever" for an event to appear on the event queue; it then attempts to handle the event at the highest possible level--first by passing it off to the system event handler, then the menu event handler, then the application event handler. Finally, if the event is not handled at any level, it is dispatched by the PalmOS to the appropriate handler function.
What's not obvious about the above is that
ApplicationHandleEvent
is a user-defined function that
interprets certain kinds of events. In a C-based PalmOS
program, when on-screen forms are changed,
ApplicationHandleEvent
(or its analog) creates the form and sets
up a callback function for each form using code like the
following (from PPTDG):
if ( event->eType == frmLoadEvent ) { // Load the form resource specified in the event and activate // the form formId = event->data.frmLoad.formID; frm = FrmInitForm( formId ); FrmSetActiveForm( frm ); // Set the event handler for the form. The handler of the // currently active form is called by FrmDispatchEvent each // time it gets an event switch( formId ) { case HelloWorldForm: FrmSetEventHandler( frm, MyFormHandleEvent ); break; } }
where frm is a pointer to a form structure, and
MyFormHandleEvent
is a user-defined function of type
'FormEventHandlerType':
Boolean FormEventHandlerType( EventType *eventP )
You can see some basis for an OO solution here; there are FORMs, and perhaps some EVENT_HANDLERs. But this model of application development, while perfectly acceptable for C, presents a few significant problems for the ePalm project: specifically, callbacks.
First, callbacks using the prc-tools toolchain have some difficulties. The GCC compiler expects that the A4 register (used to access global variables) can be used throughout functions to access global variables, etc. The problem is this: during a callback function, the A4 register may not be preserved correctly during a callback function (because control passes to a PalmOS routine that calls the callback).
To avoid scrambling the A4 register, callback routines must
include the macro CALLBACK_PROLOGUE
after variable declarations
and before actual code, and the macro CALLBACK_EPILOGUE
between
code and the return statement. Another example from _PPTDG_:
static int MyCallback() { int myReturnResult; int anotherVariable; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif // do stuff in my function #ifdef __GNUC__ CALLBACK_EPILOGUE #endif return myReturnResult; }
Again, this is acceptable in a C environment. But in order to use this sort of mechanism with SmartEiffel, two obstacles must be overcome. First, it would be extremely difficult to mark Eiffel-generated functions as callbacks; second, it would be extremely difficult to have SmartEiffel generate functions that had the correct signatures.
For a while, this seemed unsolveable; PalmOS required a separate callback function for each form, but there was no obvious way to do so in SmartEiffel.
The answer is simple: the C function FrmDispatchForm
sends the
event to the currently active form--in other words, there can be
only one active form at a time. The ePalm solution provides a
single callback for ALL form events; the equivalent code to the
earlier section (where the event handler was set) looks like the
following:
inspect event.type when Frm_load_event then create form.from_form_id( event.data_frmload_formid ) form.set_as_active_form create form_handler form.set_event_handler( form_handler ) last_event_was_handled := True
The event
object, predictably, is of type 'EVENT'; the form
object of type FORM
. The form
is created from the given id
(which loads it from the .prc
resource) and set as the active
form. An object of type FORM_HANDLER
is created (inheriting
from STANDARD_EVENT_HANDLER
), and the form
object sets it as
its event handler.
Under the hood, there is only one event handler function, which looks like this:
/* vputz -- this dispatches events to the form_dispatcher once object */ /* defined in the FORM_DISPATCHER_CLIENT class */ Boolean form_dispatcher_event_handler( EventType* eventp ) { Boolean handled = false; #ifdef __GNUC__ CALLBACK_PROLOGUE #endif handled = (Boolean)(FORM_DISPATCHER_dispatch_event_from_pointer( FORM_DISPATCHER_CLIENT_form_dispatcher(eiffel_root_object), eventp )); #ifdef __GNUC__ CALLBACK_EPILOGUE #endif return handled; }
The central line looks complicated, but what you are seeing is a
bit of CECIL callback code. The single
form_dispatcher_event_handler
C callback is calling the
dispatch_event_from_pointer
feature of a singleton
FORM_DISPATCHER
object created in a once
function of the class
FORM_DISPATCHER_CLIENT
, of which PALM_APPLICATION
is a
descendant--which means that the eiffel_root_object
(the only
Eiffel object we can be sure of, the one created during main()
)
is a valid target.
Whew. The FORM_DISPATCHER_CLIENT
class holds a dispatch map
which contains a one-to-one mapping of event handlers to
pointers-to-form-structures:
dispatch_map : DICTIONARY[ EVENT_HANDLER, POINTER ]
So the process looks something like this:
A FORM
object registers itself with an EVENT_HANDLER
object
During registration, the FORM_DISPATCHER
singleton inserts
an entry in the dispatch_map
.
When an event is received that should go to a form, the
single form_dispatcher_event_handler
C callback calls
the FORM_DISPATCHER.dispatch_event_from_pointer
feature
Using the pointer-to-form-structure, the FORM_DISPATCHER
finds the appropriate EVENT_HANDLER
object and calls its
HANDLE_EVENT
member to handle the event.
This convoluted process takes a bit of memory overhead but works
like a champ, and encapsulates the concepts of FORM
and
EVENT_HANDLER
into good abstractions which can be independently
developed.
As developers, we understand basically what an event-driven GUI
framework means. An EVENT
refers to something which requires
action--the user taps on the screen or button, for example. A
FORM
also requires fairly little description as an abstract
concept. But in the context of a PalmOS application, it's worth
understanding where forms come from.
In a PalmOS application, a form is an internal data structure
describing a displayed form, containing a menu and various
gadgets. To create a form, ePalm uses the pilrc
tool described
elsewhere. This tool takes a textual description of a form and
converts it to a binary data structure that the PalmOS can load
and treat as a form object. For example, a simple form is shown
below:
FORM ID HelloWorldForm AT (0 0 160 160) MENUID 1000 BEGIN TITLE "Hello World" BUTTON "Button" ID HelloWorldButtonButton AT (59 91 36 12) LEFTANCHOR FRAME FONT 0 END
The basics are easy to get; this creates a FORM
with the given
coordinates (in this case filling the screen, but forms can be
smaller) with a given menu (described elsewhere), containing a
title string and a single button.
Some of the IDs you see here are not numbers, but identifiers (ID
HelloWorldForm
and the like). And here we learn another
idiosyncracy with ePalm.
The pilrc tool was designed to work with C code. As such, it expected that C programmers would create a single header file containing all the resource IDs that a program would need in one place; thus, pilrc can reference a header file with '#define's and refer to those '#define'd ids within a resource description file. A section of that header file would look something like this:
#define HelloWorldForm 1000 #define HelloWorldButtonButton 1003
Naturally, this works well for C but less well for Eiffel. The
problem is easily solved with a quick Ruby script,
make_constants.rb
, which takes a C header file and outputs an
Eiffel class file with the same constants present:
class MAIN_CONSTANTS feature {ANY} -- constants Hello_world_form : INTEGER_16 is 1000 Hello_world_button_button : INTEGER_16 is 1003 ... end
So ePalm users must first create a header file with the
appropriate '#define's, then use the make_constants.rb
tool to
generate the required Eiffel constants class, which is then
inherited by any class which will use those constants during the
course of the program's execution. A make
target is highly
recommended for this!
The C_STRUCT
class provided a passive wrapper for a C-style
structure, but requires modification in order to access any
members of a structure or provide easy access to C functions using
that structure.
In previous incarnations of the SmartEiffel compiler, the Eiffel-to-C interface was not particularly robust--while some calls to C functions were quite easy, more complex arrangements required crafting of custom C code, which worked alongside the Eiffel code--in other words, the Eiffel code would call a C function which massaged the data, called the PalmOS function, returned, massaged the data again, and returned control to the Eiffel function.
This sort of parallel C/Eiffel coding led to the development of a tool which would take a sort of marked-up collage of Eiffel and C code and generate separate .e and .c files for the different eiffel and corresponding external C codes.
Happily, the latest eiffel-to-c interface has dramatically improved, and only a few bits of external C code are required, and those only for specific functionality (table support, database support, etc).