Programmer le Palm sous GCC

Cet article a été publié dans le numéro 3 (novembre 1999) du magazine Team Palmtops.
Il est reproduit avec l'aimable autorisation de Posse Press.

 

Le mois dernier, nous avions installé et configuré un environnement de développement reposant sur GCC-0.5.0. Le moment est venu d'entrer dans le vif du sujet, en nous attaquant à notre première application Palm.

Les fichiers mentionnés dans cet article sont contenus dans l'archive ex01.zip.

La tradition veut que le premier programme écrit, lorsque l'on découvre un nouvel environnement de développement, ait pour fonction d'afficher un petit message d'accueil. Nous ne dérogerons pas à cette règle, d'autant que la tâche n'est pas aussi triviale qu'elle en a l'air. L'interface utilisateur de cette application est ultra-simple, puisqu'elle consiste en un unique formulaire comportant un label qui contient notre message d'accueil (nous avons francisé le sempiternel "Hello, world"...). En voici la définition au format PilRC, que l'on va stocker dans le fichier app.rcp :

#include "rc.h"

form id idFormMain at (0 0 160 160)
begin
	title "Exemple"
	label "Bienvenue à bord !" autoid at (center 30)
end 

Petite explication de texte : app.rcp définit un formulaire identifié par idFormMain, qui occupe toute la surface de l'écran, c'est-à-dire le carré dont le coin supérieur gauche est en (0,0) et dont les deux côtés mesurent 160 pixels. Le titre de ce formulaire est "Exemple" : c'est le texte qui apparaîtra en blanc sur noir, dans la barre de titre standard. Le formulaire comporte par ailleurs un label, c'est-à-dire une zone de texte non-éditable, dont le libellé est "Bienvenue à bord !". Ce label, dont l'identifiant est automatiquement défini par PilRC (clause autoid), est centré horizontalement à 30 pixels de la limite supérieure du formulaire.

idFormMain est l'identifiant associé à notre formulaire. Il est déclaré dans le fichier rc.h inclus lors de la définition précédente (la valeur 1000 est arbitraire) :

// Forms
#define idFormMain 1000 

Le code

Pour notre exemple minimaliste, nous n'avons pas besoin d'une programmation sophistiquée : l'application doit simplement afficher le formulaire idFormMain lorsqu'elle démarre, et se terminer lorsque l'utilisateur le décide (en passant à une autre application). La programmation sous PalmOS est fondée sur l'approche événementielle : les applications sont pilotées par des événements. Ceux-ci signalent qu'il s'est passé quelque chose pouvant intéresser l'application : déplacement du stylet sur l'écran, pression sur un bouton, saisie d'un caractère Graffiti, etc. C'est ainsi que nous allons trouver, au coeur d'une application, une boucle qui effectue deux opérations : attendre le prochain événement, puis le traiter, et ce jusqu'à ce que l'application s'achève (ce qui est d'ailleurs également signalé par un événement).

Schéma synoptique d'une application.

Voici la procédure principale de notre application. Elle s'apparente au main() d'un programme C classique :

DWord PilotMain(Word cmd, Ptr cmdPBP, Word launchFlags)
{
	if (cmd == sysAppLaunchCmdNormalLaunch) {
		if (StartApplication()) {
			EventLoop();
			StopApplication();
		}
	}
	return 0;
}

La procédure PilotMain() est appelée par PalmOS au démarrage de l'application. L'argument cmd indique dans quelles conditions se fait le démarrage : lancement normal, recherche d'information, réinitialisation, etc. Nous ne nous intéressons ici qu'au lancement normal. Les autres options sont purement et simplement ignorées. Nous commençons par appeler une fonction StartApplication(), dont le rôle est d'effectuer les initialisations. Puis, si StartApplication() retourne la valeur true pour indiquer que tout s'est bien passé, nous invoquons une fonction EventLoop(), qui contient la boucle évoquée plus haut. Nous y restons jusqu'à ce que la terminaison de l'application soit demandée. Enfin, nous appelons une fonction StopApplication(), chargée de "faire le ménage" avant de rendre la main au système.

Voici les fonctions StartApplication() et StopApplication() :

static Boolean StartApplication(void)
{
	FrmGotoForm(idFormMain);
	return true;  // tout s'est bien passé
}

static void StopApplication(void)
{
	// vide
}

StartApplication() s'appuie sur l'appel système FrmGotoForm() pour demander l'affichage du formulaire principal idFormMain. StopApplication() ne fait rien, étant donné la simplicité de l'application.

Examinons à présent la boucle EventLoop() :

static void EventLoop(void)
{
	EventType event;
	do {
		EvtGetEvent(&event, evtWaitForever);
		ProcessEvent(&event);
	} while (event.eType != appStopEvent);
}

Pas de surprise : on récupère le prochain événement via l'appel système EvtGetEvent(), quitte à attendre indéfiniment qu'il en survienne un, puis on le traite en appelant une fonction ProcessEvent(), et ce jusqu'à réception de l'événement appStopEvent, qui signale que l'application doit se terminer.

Comment l'événement est-il traité par ProcessEvent() ?

static void ProcessEvent(EventPtr e)
{
	Word error;

	if (SysHandleEvent(e)) return;
	if (MenuHandleEvent(NULL, e, &error)) return;
	if (AppHandleEvent(e)) return;
	FrmDispatchEvent(e);
}

Ici entre en jeu la notion de gestionnaire d'événements : c'est une fonction laquelle on soumet un événement, qui le traite et retourne un booléen pour indiquer si l'événement a été consommé. Le cas échéant, on peut passer à l'événement suivant. Dans ProcessEvent(), l'événement est tout d'abord soumis à PalmOS, par l'intermédiaire de l'appel système SysHandleEvent(). Ce gestionnaire prend par exemple en charge la reconnaissance des caractères saisis au stylet dans la zone Graffiti, en interceptant les événements qui signalent les déplacements du stylet dans cette zone. S'il n'a pas été consommé, l'événement est ensuite soumis au gestionnaire de menus, par l'intermédiaire de l'appel système MenuHandleEvent(). Ce gestionnaire est chargé de traiter les mouvements de stylet lorsqu'un menu est affiché. Si l'événement est toujours présent, il est alors soumis à un gestionnaire appelé AppHandleEvent(), dont le rôle est de gérer les changements de formulaires dans l'application. Enfin, s'il n'a toujours pas été consommé, l'événement est passé à l'appel système FrmDispatchEvent(), qui va a son tour le transmettre au gestionnaire d'événements du formulaire courant.

Pour mieux comprendre ce dernier point, intéressons-nous à la fonction AppHandleEvent() :

static Boolean AppHandleEvent(EventPtr e)
{
	Word formID;
	FormPtr frm;

	Boolean handled = false;

	if (e->eType == frmLoadEvent) {
		formID = e->data.frmLoad.formID;
		frm = FrmInitForm(formID);
		FrmSetActiveForm(frm);
		switch (formID) {
			case idFormMain :
				FrmSetEventHandler(frm, MainEventHandler);
				break;
		}
		handled = true;
	}
	return handled;
}

Lorsqu'un changement de formulaire survient, signalé par l'événement frmLoadEvent, AppHandleEvent() charge la description du formulaire demandé (appel système FrmInitForm()), puis active ce formulaire (appel système FrmSetActiveForm()) et enfin associe un gestionnaire d'événements à ce formulaire (appel système FrmSetEventHandler()). En l'occurence, pour le formulaire idFormMain, il s'agit d'une fonction nommée MainEventHandler(). C'est donc finalement à ce gestionnaire d'événements que sera soumis, via FrmDispatchEvent(), l'événement non consommé évoqué précédemment.

Il ne nous reste plus qu'à définir MainEventHandler() :

static Boolean MainEventHandler(EventPtr e)
{
	Boolean handled = true;

	CALLBACK_PROLOGUE
	switch (e->eType) {
		case frmOpenEvent :
			FrmDrawForm(FrmGetActiveForm());
			break;
		default :
			handled = false;
	}
	CALLBACK_EPILOGUE
	return handled;
}

La signification de CALLBACK_PROLOGUE et CALLBACK_EPILOGUE est précisée dans l'encadré ci-dessous.

MainEventHandler() n'intercepte que les événements frmOpenEvent, correspondant à l'ouverture d'un formulaire. Leur traitement consiste simplement à dessiner le formulaire, ce qui est fait par l'intermédiaire de l'appel système FrmDrawForm().

Callback

Les macros CALLBACK_PROLOGUE et CALLBACK_EPILOGUE sont requises avec le compilateur GCC, parce que MainEventHandler() est une callback, c'est-à-dire une fonction transmise par l'application au système, pour qu'il puisse la rappeler (to call back) ultérieurement. Leur définition est la suivante :

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;

En général, on place cette définition dans un fichier d'en-tête callback.h.

La raison d'être de ces macros résulte d'une petite différence de gestion des registres entre GCC et l'API PalmOS : en effet, GCC considère que le registre A4 du processeur est à sa disposition pendant tout le déroulement de l'application et l'utilise pour y mémoriser un pointeur sur les variables globales, alors que l'API PalmOS ne respecte pas cette convention. Pour résoudre ce problème, au début de chaque callback, CALLBACK_PROLOGUE conserve une copie de A4 avant de donner à ce registre la valeur escomptée par GCC. A la fin de la callback, CALLBACK_EPILOGUE restaure la valeur de A4 précédemment conservée.

Pour terminer, il ne nous reste plus qu'à ajouter les lignes suivantes au début de notre source :

#include <pilot.h>
#include "callback.h"  // voir encadré
#include "rc.h"

Compilation

Voici le fichier Makefile qui permet de compiler notre exemple :

# ATTENTION
# Modifier la ligne "BASE =" en fonction de votre environnement !

#---------------------------------------------------------------
# General stuff
#---------------------------------------------------------------

# Architecture
ARCH = m68k-palmos-coff

# Directories
BASE = //C/PalmSDK
TDIR = $(BASE)/bin
IDIR = $(BASE)/$(ARCH)/include/PalmOS

# Tools
CC = $(TDIR)/$(ARCH)-gcc
OBJRES = $(TDIR)/$(ARCH)-obj-res
PILRC = $(TDIR)/pilrc
BUILDPRC = $(TDIR)/build-prc

# Defines
DEF = -DDEBUG -mdebug-labels

# Includes
INC = -I$(IDIR) -I$(IDIR)/UI -I$(IDIR)/System -I$(IDIR)/Hardware

# Flags
CFLAGS = -O2 -g $(DEF) $(INC)

#---------------------------------------------------------------
# Application specific
#---------------------------------------------------------------

PRC = app.prc
EXEC = app
OBJS = app.o
ICONTEXT = "Exemple"
APPID = ex01
RES = app.rcp

#---------------------------------------------------------------
# Targets
#---------------------------------------------------------------

all: $(PRC)

$(PRC): code.stamp bin.stamp
	$(BUILDPRC) $@ $(ICONTEXT) $(APPID) *.grc *.bin
	ls -l *.prc

code.stamp: $(EXEC)
	$(OBJRES) $(EXEC)
	touch code.stamp

$(EXEC): $(OBJS)
	$(CC) $(CFLAGS) $(OBJS) $(LIBS) -o $@

bin.stamp: $(RES)
	rm -f *.bin
	$(PILRC) $(RES)
	touch bin.stamp

clean:
	rm -f *.[oa] $(EXEC) *.bin *.stamp *.grc *.bak

Veillons à bien modifier la ligne commençant par "BASE =" afin d'indiquer le répertoire de base de votre environnement. Dans notre cas, il s'agit de C:\PalmSDK, représenté par : //C/PalmSDK. La section "Application specific" précise les paramètres de construction de notre application : elle sera stockée dans le fichier app.prc, aura l'identifiant 'ex01', et sera nommée "Exemple".

Nous pouvons à présent lancer la compilation :

% make
[commandes de compilation]
-rw-r--r--   1 500      everyone     1521 May 31 08:09 app.prc

Voilà, notre application est prête ! Il ne nous reste plus qu'à l'installer sur le Palm pour la tester.

Notre première application affiche un simple message d'accueil.

Conclusion

Nous connaissons maintenant le principe général de fonctionnement d'une application... qui ne fait rien ! Nous savons également définir une interface utilisateur minimale et le code associé. Le mois prochain, nous verrons comment interagir avec l'utilisateur au moyen des composants d'interface proposés par PalmOS. Vos questions et commentaires sont bienvenus : n'hésitez pas à les transmettre par courrier électronique à l'adresse palmprog@ablivio.com.

Denis Faivre - palmprog@ablivio.com.

Copyright © 1999-2000 - Denis Faivre et Posse Press.
Tous droits réservés.