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.

The Basics: Wrapping Structures

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.

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

Callbacks and ePalm

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 to Callbacks for Form Event Handling

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:

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.

Interlude: Forms and Events

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!

Wrapping Active Structures - a Historical Note

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

Powered by Zope