Thursday, December 3, 2009

Les dessous d'Android

Aujourd'hui j'ai trouvé un article assez intéressant sur le site http://www.unixgarden.com.  L'auteur parle d'Android... Je vous laisse lire l'article à ce lien.

Wednesday, December 2, 2009

Quelques conseils de post-programmation

J'ai trouvé sur un site de l'universite de Paris 8, quelques conseils de programmation
d'un maitre de conférence en informatique. Alors je paratge avec vous.




Quelques conseils de post-programmation
Jean Méhat
Licence MIME, Méthodologie
janvier 2001


Ouf, finalement, votre programme fonctionne (au moins sur les données sur lesquelles vous l'avez testé). On peut passer au problème suivant ?

La réponse est : NON !

Une fois que votre programme semble fonctionner, il faut revenir dessus d'une part pour le tester plus sérieusement et d'autre part pour le transformer en un _beau_ programme, élégant, rapide, facile à lire et à modifier. Cette étape est particulièrement importante si vous êtes en cours d'apprentissage : cette étape d'amélioration vous permettra, la prochaine fois, d'écrire directement un beau programme élégant et rapide.

Je vous propose ici une liste de choses à vérifier dans votre programme avant de considérer que le travail est terminé. Les premières choses sont faciles à vérifier et à corriger ; j'aborde ensuite des problèmes pour lesquels la bonne manière de faire est moins nette et demande plus de jugement ; finalement, je présente des domaines où il existe plusieurs façons de faire.

-- Automatique --

Les points suivants peuvent se traiter de manière quasi-mécanique, sans réfléchir sur la signification du programme.


o Est-ce que votre programme est indenté de façon cohérente ?

Il est essentiel d'indenter ses programmes toujours de la même manière pour les lire plus facilement ; votre oeil s'habitue à lire des programmes indentés d'une certaine façon et celà vous aide à interpréter le code.

Vous avez tout intérêt à indenter de la même manière que les autres, pour vous habituer à leur façon d'indenter ce qui facilitera votre lecture de leurs programmes et pour leur rendre la lecture de votre programme plus facile. Si vous avez des doutes sur la bonne manière d'indenter, laissez un programme le faire à votre place ; par exemple utilisez la commande "indent-region" de emacs.


o Les messages d'erreurs sont-ils tous imprimés sur stderr ?

Une erreur de débutant consiste à imprimer ses messages d'erreurs sur la sortie standard ; il *faut* le faire sur la sortie d'erreur stderr, pour deux raisons : d'une part cela permettra d'utiliser les données produites par le programme sur la sortie standard comme entrée d'un autre programme sans traiter les messages d'erreur comme des données valides ; d'autre part, les autres programmeurs et utilisateurs de votre programme, s'ils cherchent des messages d'erreur, regarderont sur la stderr, peut-être en ignorant la sortie standard ; si vous mélangez les sorties correctes et les messages d'erreur sur la sortie standard, c'est impossible.


o Les messages d'erreurs sont-ils compréhensibles ?

Regardez chaque message d'erreur avec un esprit neuf : dans six mois, quand vous aurez probablement tout oublié du code que vous venez d'écrire, est-ce que le message d'erreur sera toujours compréhensible ? Est-ce qu'il permettra, quand c'est possible, de corriger l'erreur ? Par exemple,
"erreur d'argument"
n'aide pas beaucoup, alors que
"usage: toto [-z] N fichier"
est beaucoup mieux. De même
"erreur"
est moins bon que
"malloc a raté: plus de mémoire".

Pour toutes les opérations qui correspondent à des appels systèmes, n'hésitez pas à utiliser la fonction perror qui donne un message standard indiquant la raison pour laquelle l'appel à échoué. Ainsi le fragment de code :

f = fopen("foo", "w")
if (f == 0)
perror("foo");
n'a pas besoin d'être compliqué pour imprimer, suivant les cas:
foo: Permission denied
foo: No such file or directory
foo: Read-only file system
Ces messages d'erreur standards vous seront familiers quand vous serez un utilisateur averti et neuf fois sur dix, vous pourrez corriger l'erreur sans vous poser de question.


o Avez-vous desactivé le code de mise au point ?

Parfois, plutôt que d'utiliser le debugger, on est tenté d'ajouter des
appels à la fonction printf pour savoir où se situe le problème dans
un programme qu'on met au point. Ce n'est pas toujours une bonne idée,
mais c'est parfois plus simple que de relire toute la documentation du
debugger. Cependant, une chose est certaine : une fois que le
programme est mis au point, il ne faut plus faire ces impressions !

Une manière simple consiste à utiliser le pré-processeur ; ainsi,
si au début de votre programme vous avez placé :
# define DEBUG if(1)fprintf
vous pouvez mettre un peu partout dans le code des choses comme
DEBUG(stderr, "la variable x vaut %d\n", x);
et ensuite retirer toutes les impressions superflues en modifiant
la définition en
# define DEBUG if(0)fprintf
Si plus tard vous avez de nouveau besoin de modifier le code, vous
n'avez qu'une ligne à modifier pour ré-activer les impressions de mise
au point.

Ne vous inquietez de l'espace que ça utilise, vous pouvez faire
confiance au compilateur pour détecter qu'un test est toujours faux
et dans ce cas, ne pas placer l'appel à printf dans le code généré.


o Vérifiez-vous tout ce qui a de bonnes chances de poser problème ?

o argc/argv
o malloc/realloc
o fopen
o scanf/fscanf

Un programme doit fonctionner quand on lui donne les entrées qu'il
attend, mais il ne doit pas planter lamentablement quand on lui donne
quelque chose qu'il n'attendait pas (ou qu'on ne lui donne pas quelque
chose qu'il attendait). Le minimum est de vérifier ce qu'il y a dans
argc et argv avant de l'utiliser et d'examiner les valeurs renvoyées
par les fonctions malloc et realloc ainsi que par fopen, scanf et
fscanf. Si ces opérations échouent, le minimum est d'imprimer un
message d'erreur et d'arrêter le programme.



o Le code est-il libre de toute constante ?

Toutes les constantes doivent être définies via un const, un #define
ou dans un enum ; ceci permet de savoir à quoi ces constantes
correspondent et le programme sera plus facile à modifier. Comparez :
p = malloc(2034);
avec :
p = malloc(MAX_ELEMENTS * sizeof p[0]);
Dans le premier cas, on n'a aucune idée de ce que signifie le 2034
alors que dans le second, on a déja une idée de ce à quoi correspond
le nombre d'octets dont on demande l'allocation.

On peut faire une exception pour les constantes qui correspondent à
des codes de caractères. Ainsi, il n'y a pas vraiment de problème
avec :
if (c >= 'a' && c <= 'z')
parce qu'on voit la signification des constantes 'a' et 'z' ; en
revanche, il est ridicule d'écrire le même code avec :
if (c >= 97 && c <= 123)
On ne gagne _rien_ par rapport à la première version ; on perd en
lisibilité et en portabilité. Bien sur le mieux est de connaître la
librairie C et d'écrire directement
if (islower(c))
En plus, islower sera probablement plus rapide que toute autre forme
de test, puisqu'il va seulement regarder dans une table.


o Avez-vous supprimé les variables inutiles ?

Retirez la déclaration des variables qui ne sont jamais utilisées (le
compilateur gcc les indique si on compile avec l'option -Wall).


o Vos fonctions qui retournent des valeurs en renvoient-elles toujours ?

Une fonction qui retourne une valeur doit toujours renvoyer une
valeur, même dans le cas où une erreur se produit (dans ce cas, elle
doit probablement renvoyer une valeur spéciale qui indique qu'il y a
une erreur). Le compilateur gcc prévient quand une fonction s'arrête
sans renvoyer de valeur si on compile avec l'option -Wall.


o Citez-vous vos sources ?

Indiquez, dans les commentaires et dans la documentation, toutes les
sources que vous avez utilisées. Si vous partez du programme de Joe
Jill qui traite les foobars et que vous y ajoutez la transformation
des foos en bars, ne dites pas « j'ai écrit un programme qui traite
les foobars en transformant les foos en bars » ; en faisant cela, vous
mettrez Joe Jill de mauvaise humeur (et le professeur aussi). En
revanche si vous dites « j'ai ajouté au programme de traitement des
foobars de Joe Jill la transformation des foos en bars », vous êtes
quelqu'un de cultivé, qui ne fait pas de travail inutile, qui sait
trouver les bonnes informations et vous faites plaisir à Joe Jill.

Ce n'est pas la peine de citer les sources que tout le monde connait,
comme les pages du manuel Unix ; en revanche, mentionnez les livres
d'algorithmiques (avec le numéro de la page) et les sources d'autres
programmes (avec si possible l'endroit où on peut le trouver) et les
sites Web (avec l'url, au minimum).

Il y a des conventions compliquées, que vous avez le droit d'ignorer
pour le moment, sur la manière dont on cite une référence ; le
principe général consiste à donner tout ce qui peut être nécessaire
pour la retrouver. Dans tous les cas, essayez de trouver le nom de
l'auteur (en plus, ça lui fera plaisir) et la date de création du
document ou du programme.


o Utilisez-vous argv plutôt que scanf ?

Quand vous écrivez un programme qui a besoin de quelques paramètres,
il est presque toujours préférable d'utiliser des arguments sur la
ligne de commande plutôt que de les lire, une fois le programme
lancé, avec des scanfs.

Cette manière de procéder est plus naturelle sous Unix (quelles
commandes connaissez-vous sous Unix qui ne lisent pas leurs paramètres
sur la ligne de commande ?).

De plus, une commande qui lit ses paramètres sur la ligne de commande
est bien plus aisée à lancer dans un programme shell qu'une commande
qui les lit sur son entrée standard.


o Tous vos switch ont-ils un default, tous vos if ont-il un else ?

Même si vous savez que le cas ne peut pas se produire, prévoyez
toujours que vos programmes peuvent vous étonner ; considérez toujours
tous les cas :
enum {
Max = 1000,
};

struct Bidule table0[Max], table1[Max], table2[Max];

/* trouver -- cherche str dans la table 0, 1 ou 2 */
struct Bidule *
trouver(char * str, int n){
if (n == 0)
return chercher(str, table0);
else if (n == 1)
return chercher(str, table1);
else if (n == 2)
return chercher(str, table2);
else {
fprintf(stderr, "trouver: n = %d: impossible\n", n);
exit(1);
}
}
Quand on a écrit la fonction trouver, on savait que le second argument
ne pouvait prendre qu'une valeur entre 0 et 2 ; pourtant, on a prévu
ce que doit faire le programme quand trouver sera appelée avec une
autre valeur plutôt que de se planter silencieusement un peu plus
tard.


o Avez-vous des variables dont les noms se terminent par des chiffres ?

Quand on se met à avoir des noms de variables dont le nom se termine
par un chiffre, c'est en général que le moment est venu d'utiliser un
tableau. Ainsi, dans l'exemple précédent, on a trois variables table0,
table1 et table2, ce qui est mauvais signe : il vaut mieux faire:
enum {
Max = 1000,
Ntable = 3,
};

struct Bidule table[Ntable][Max]

/* trouver -- cherche str dans la table N */
struct Bidule *
trouver(char * str, int n){
if (n < 0 || n >= Ntable){
fprintf(stderr, "trouver: n = %d: impossible\n", n);
exit(1);
}
return chercher(str, table[n]);
}
Ce programme est plus concis et plus élégant, donc plus facile à
comprendre ; de plus, il sera plus facile à modifier que le précédent
si on se rend compte plus tard qu'il fallait quatre tables et non
trois.


-- Un peu plus délicat --

Les points qui suivent sont un peu plus délicats ; pour les appliquer,
il faut réfléchir un peu sur le programme.


o Avez-vous un jeu d'essai complet ?

Quand vous avez mis votre programme au point, vous l'avez testé sur
certaines données ; une fois qu'il marche sur ces données, il est
indispensable qu'il fonctionne également sur tous les autres types
de données qu'il est censé traiter. Reprenez donc l'énoncé du
problème que vous êtes en train de résoudre et concevez un jeu
d'essai qui contienne bien tous les cas possibles. Assurez-vous
que le jeu d'essai complet fait bien tourner au moins une fois
chaque instruction du programme, y compris les routines de traitement
d'erreur.

La conception d'un jeu d'essai est une étape importante ; c'est
souvent une bonne idée de commencer la conception d'un programme par
là (la langue de bois appellerait cela de l'étude de cas). Même dans
ce cas, recommencez cette inspection une fois que le programme tourne
parce que, en développant le programme, vous avez peut-être vu dans le
problème des aspects qui vous étaient restés cachés au premier abord.

Les jeux d'essais font partie de la documentation d'un programme ;
quand on passe une soutenance de projet, on apporte aussi le jeu
d'essai (en plus du programme et de la documentation).


o Vos tableaux ne sont-ils pas trop petits ?

Vous n'avez pas intérêt à chipoter sur la taille de vos structures
de données. Ainsi, quand on s'apprête à lire une ligne, on ne déclare
pas un tableau de 80 caractères (même si c'est la longueur maximum
de beaucoup de lignes) ; il vaut mieux déclarer un tableau de
quelques kilo-octets, ce n'est pas celà qui remplira la mémoire.


o Votre programme contient-il le bon nombre de lignes vides ?

Un
programme
plein
de
lignes
vides
est
plus
difficile
à
lire
qu'un
programme

elles
sont
judicieusement
placées.

L'usage en C veut qu'on saute une ligne entre les déclarations et les
instructions d'une fonction. Pour le reste, utilisez les lignes vides
pour regrouper vos lignes de codes en paragraphes, qui ont une
signification à eux tous seuls. En C, une ligne vide tous les
cinq ou dix lignes donne en général de bons résultats.


o Avez-vous fait le bon choix entre read, fread, fgetc et fgets ?

La seule bonne raison de lire ses données avec read, c'est si on
souhaite, avec un seul appel système, lire plus de données que ce
n'est possible avec la stdio (habituellement 4 kilo-octets).

La seule bonne raison de lire ses données avec fread, c'est si
on souhaite lire des données dans un format interne spécifique
du processeur sans interférences de la librairie (habituellement,
des nombres entiers ou des flottants : attention, les données ne
seront pas portables).

Le choix entre fgetc et fgets dépend de l'organisation des traitements
de données : si on traite ligne par ligne, on utilise fgets ; si les
lignes n'ont pas de signification et qu'on traite caractère par
caractère, on utilise fgetc. Par exemple, il est ridicule d'écrire :
i = 0;
while((c = fgetc(fd)) != EOF){
if (c != '\n'){
ligne[i++] = c;
continue;
}
ligne[i++] = 0;
traiter(ligne);
i = 0;
}
quand on aurait pu écrire la même chose avec :
while(fgets(ligne, sizeof ligne, fd) != 0)
traiter(ligne);
Dans la première version, il faut faire un effort avant de réaliser
que les données sont traitées ligne par ligne ; dans le second, ceci
saute aux yeux et le programme est plus simple.


o Avez-vous fait le bon choix entre while et for ?

Il faut se poser la question de la meilleure structure de contrôle à
utiliser pour les boucles ; d'une façon générale, n'utilisez le
do... while(); que quand vous avez besoin que le corps de la boucle soit
exécuté au moins une fois. La perte de performance par rapport à un while
est minime ; elle est largement compensée par le gain de lisibilité.

Le choix entre boucle for et boucle while est plus délicat. Un
bon principe est d'utiliser une boucle for chaque fois qu'on peut
identifier une variable de boucle. Ainsi ne faites pas :
i = 0;
while(ligne[i] != 0){
/* 250 lignes compliquées */
i++;
}
mais préférez :
for(i = 0; ligne[i] != 0; i++)
/* 250 lignes compliquées */
Les références aux modifications de l'index de boucle sont regroupées
au même endroit. Les instructions break; et continue; seront plus
facile à interpréter. Les bonnes sources déconseillent fortement de
modifier la variable d'index dans le corps de la boucle, mais comme
toutes les règles, celle-ci souffre quelques exceptions.


o Votre programme contient-il le même code (ou presque) 3 fois ou plus ?

Quand votre programme contient plusieurs fois du code qui se ressemble,
il faut envisager de faire une fonction. N'écrivez pas :
switch(c){
case 'a':
p = xmalloc(sizeof p[0]);
p->car = 'a';
p->next = 0;
break;
case 'b':
p = xmalloc(sizeof p[0]);
p->car = 'b';
p->next = 0;
break;
case 'c':
p = xmalloc(sizeof p[0]);
p->car = 'c';
p->next = 0;
break;
...
}
mais utilisez une fonction pour regrouper le code identique :
struct Bidule *
faire(char c){
struct Bidule * p;

p = xmalloc(sizeof p[0]);
p->car = c;
p->next = 0;
return p;
}
ainsi vous pourrez faire (ce qui est mieux) :
switch(c){
case 'a':
p = faire('a');
break;
case 'b':
p = faire('b');
break;
case 'c':
p = faire('c');
break;
...
}
(voir le point suivant)


o Avez-vous des switch avec des case qui se ressemblent ?

Quand le code de traitement de case différents dans un même switch
se ressemble beaucoup (particulièrement quand il ne diffère que par
la valeur utilisée pour choisir le case), c'est que le switch est
probablement inutile. Ainsi l'exemple précédent est beaucoup plus
facile à ré-écrire avec :
if (islower(c))
p = faire(c);
else
erreur("c a une valeur interdite %c\n", c);


o Avez-vous documenté le programme ?

Quand le programme marche, oubliez tout ce que vous savez dessus
et écrivez une documentation pour quelqu'un qui souhaite l'utiliser.
Mentionnez le problème résolu par le programme, les différentes
façons de l'utiliser, donnez un exemple. Vous pouvez trouver un
bon modèle dans les pages de manuel des commandes d'Unix.

La documentation fait partie du programme. Elle doit accompagner
les sources. Elle est à présenter pour une soutenance de projet.


-- Délicat --

Les points restant sont réellement difficiles à mettre en oeuvre,
parce qu'ils demandent de trouver un juste milieu. Si vous en faites
trop, le resultat ne sera probablement pas meilleur que si vous n'en
faites pas assez.


o Avez-vous des fonctions trop longues ou trop compliquées ?

Pour qu'un programme reste lisible, les fonctions doivent rester
compréhensibles ; pour cela, elles doivent avoir la bonne longueur.
Un bon principe est qu'une fonction fasse une chose et une seule ;
une fonction qui remplit trois tâches différentes est en général
à remplacer par trois fonctions. Un autre bon principe est qu'une
fonction dans laquelle on atteint le cinquième niveau d'indentation
avec les règles usuelles d'indentation des programmes C est trop
compliquée.

(voir le point suivant)


o Avez-vous des fonctions trop courtes, ou appelées une seule fois ?

Pour qu'un programme reste lisible, il ne doit pas contenir trop
de fonctions ; pour celà il ne faut pas faire des fonctions inutiles.
Une bonne façon de reconnaître une fonction inutile consiste à
regarder combien de fois elle est appelée : si on l'utilise une
seule fois dans tout le programme, la fonction est suspecte, il
vaudrait peut-être mieux remplacer l'appel par le code de la fonction
lui-même.

(voir le point précédent)


o Avez-vous des noms de variables et de fonctions trop courts ?

Le nom des variables doit aider le lecteur (vous dans un premier temps) à
comprendre à quoi elles sont utilisées ; ne faites pas :
f(t, p, d, v)
qui est carrément incompréhensible, mais plutôt :
trouver(table, premier, dernier, valeur)

Notez que dans un nom de variable, le début est plus important pour
l'oeil que la fin ; si on a un tableau et l'index d'un élément à
passer en argument, comparer :
trouver(tableau, tableaui)
avec :
trouver(tableau, itableau)
Trouvez-vous, vous aussi, que la seconde version est plus lisible ?
Pour cette raison, évitez d'utiliser la « convention hongroise » dans
laquelle on utilise les premiers caractères d'un nom de variable pour
décrire son type.

(voir le point suivant)


o Avez-vous des noms de variables et de fonctions trop longs ?

Les noms des variables doivent rester d'une taille raisonnable.
Préférez résolument :
trouver(table, premier, dernier, valeur)
au verbeux :
trouver_element(tableau, indice_du_premier_element,
indice_du_dernier_element,
valeur_recherchee)
On peut aussi la faire à la C++ sans être beaucoup plus lisible :
TrouverElement(Tableau, IndiceDuPremierElement,
IndiceDuDernierElement, ValeurRecherchee)
Dans les deuxième et troisième exemples, on a même du mal à voir
que la fonction a quatre arguments.

Une bonne idée est de nommer toujours ses fonctions avec des verbes et
toutes ses variables avec des substantifs.

Quand une variable revient tout le temps, comme le pointeur sur la
structure Display dans un programme X11, ce peut être une bonne idée
de lui affecter un nom bref, pour ne pas encombrer les sources du
programme. Cela rendra la première lecture plus difficile, mais
facilitera une lecture détaillée : une fois que le lecteur aura saisi
que la variable d fait toujours référence au Display, la brièveté
de son nom permettra de désencombrer les arguments des fonctions de
X11 qui en ont bien besoin. N'abusez pas de cette méthode.

On peut aussi utiliser les conventions usuelles ; dans un programme
C, une variable nommée i est très souvent utilisée pour balayer un
tableau, une variable nommée c pour contenir un caractère le temps
d'un traitement et une variable nommée p pour pointer sur n'importe
quoi pendant quelques lignes seulement.

(voir le point précédent)


o Y a-t-il des commentaires inutiles ?

Les commentaires sont là pour vous aider à relire le programme plus
tard ; ce n'est pas la peine de commenter le code qui vous semblera
évident plus tard. Un exemple ridicule est :
i ++; /* Incrémenter la variable i */

Les lignes de commentaires pleines d'étoiles sont à proscrire
également ; elles n'apportent pas grand chose à la lecture, sinon
le découpage en paragraphe qui sera agréablement remplacé par des
lignes vides judicieusement placées.

(voir le commentaire suivant)


o Y a-t-il assez de commentaires ?

Au minimum, mettez un commentaire par fichier pour décrire son
contenu et un commentaire d'une ligne par fonction et par donnée
globale.

Quand vous écrivez votre code, un peu d'expérience vous permet
de savoir ce qui sera difficile à la re-lecture ; ce sont ces
parties du programmes dans lesquelles il faut mettre des commentaires.
Ne faites pas :
sens = (((((x ^ (x - 1)) & x) << 1) & x) == 0); /* hack */
mais :
faible = (((x ^ (x - 1)) & x); /* bit à 1 le plus a droite */
gauche = (faible << 1) & x; /* bit a gauche du bit à 1 */
sens = (gauche == 0); /* 0, tourner à gauche, 1 à droite */
sauf si vous voulez impressionner quelqu'un.


o Pouvez-vous généraliser votre programme ?

Votre programme traite un problème particulier. Regardez si vous
pouvez, sans trop de complication, traiter le même problème d'une
façon plus générale.

Remplacez vos tableaux, qui ont une taille fixe, par des zones de
mémoire allouées avec malloc et realloc qui pourront grandir pour
traiter plus de données que vous n'aviez prévu au départ : c'était
difficile quand vous mettiez le programme au point, mais maintenant
qu'il fonctionne, vous pouvez le faire d'une façon presque
automatique. Si vous avez conservé vos jeux d'essais, ce n'est
difficile de s'assurer que le programme fonctionne toujours.


o Vous sentez-vous de recommencer ?

Une bonne façon d'améliorer un programme : maintenant que vous l'avez
écrit, que vous savez comment les choses doivent être faites, quelles
structures de données utiliser, mettez-le à la poubelle et recommencez
tout. La version suivante sera mieux écrite.


-- A lire --

Voici deux références dont la lecture ne peut que vous être
profitable. Il faut lire aussi des programmes écrits par des gens
expérimentés ; c'est une autre manière efficace d'améliorer son style.

Henri F. Ledgard, _Proverbes de programmation_, traduit par J. Arsac,
Dunod, 1978.
Ce livre ne semble plus être disponible en librairie, mais on
le trouve dans toutes les bonnes bibliothèques (celle de Paris 8
par exemple) ; il traite de la bonne façon de concevoir et d'écrire
un programme depuis de départ.

Richard M. Stallman, _Gnu Coding Standards_, décembre 2000,
http://www.gnu.org/prep/standards_toc.html
Ce document présente une norme, un peu lourde et compliquée, qui
décrit comment Richard Stallman, l'auteur de emacs et de gcc, pense
que des programmes doivent être écrits et présentés.

Il y a aussi une excellente référence en anglais, qui reprend les
conseils contenus ici et d'autres plus spécialisés, en parlant aussi
de java et de C++, avec des exemples détaillés: il s'agit de :
Brian W. Kernigham, Rob Pike, _The Practice of Programming_, Addison
Wesley, 1999.



Les binaires Java ME

Toute application Java ME est composée de deux fichiers avec les extensions .jar et .jad. La compréhension du processus de fabrication et des contenus de ces fichiers est primordiale.

JAD: Java Application Descriptor

JAD, pour Java Application Descriptor, est un fichier texte qui contient des informations sur l’application. Plusieurs règles sont à respecter lors de l’écriture de ce fichier. Comme il existe plusieurs configurations et profils, il serait dommage qu’après avoir téléchargé un programme, on se rende compte qu’il ne peut pas fonctionner sur le téléphone à cause d'un problème de compatibilité de configuration ou de profil ou encire qu’il n’y ait pas assez d’espace mémoire libre pour l’installer. Grâce au JAD, le téléphone peut connaître à l’avance les spécificités du programme que l’on aimerait télécharger.

C’est un fichier qui contient toutes les informations utiles concernant la MIDlet (taille du JAR, version de MIDP, version de CLDC, …). Il est généralement téléchargé avant le programme, ce qui permet de s’assurer de la compatibilité complète du terminal hôte.

Il contient des attributs obligatoires qui commencent tous par "MIDlet-" et qui sont séparés par “:” de leurs valeurs. Exemple:

MIDlet-Vendor : Y2R

Les attributs obligatoires sont:
  • MIDlet-Name 
  • MIDlet-Version 
  • MIDlet-Vendor 
  • MIDlet-Jar-URL
  • MIDlet-Jar-Size
  • MIDlet-: MIDletName , [IconPathname] , ClassName
  • MicroEdition-Profile 
  • MicroEdition-Configuration

Sans ces attributs le Application Management Software refusera d'installer l'application.

Un bionaire Java ME (jar) peut contenir une ou plusieurs MIDlet, ceci est spécifié grâce à l''attribut MIDlet- qui se remplie comme suit:

  • n: correspond au numero de la MIDlet, il commence à 1 et est incrémenté de 1 pour chaque MIDlet supplémentaire dans le cas d'une MIDlet Suite.
  • MIDletName:  nom de la MIDlet et équivalent à la valeur de l'attribut MIDlet-Name
  • IconPathname: chemin vers le fichier image de l'icone de l'application, peut rester vide
  • ClassName: nom de la classe de la MIDlet

Voici quelques attributs optionnels:

  • MIDlet-Description 
  • MIDlet-Icon 
  • MIDlet-Info-URL 
  • MIDlet-Data-Size 
  • MIDlet-Permissions
  • MIDlet-Permissions-Opt 
  • MIDlet-Push-
  • MIDlet-Install-Notify 
  • MIDlet-Delete-Notify 
  • MIDlet-Delete-Confirm

Il est possible d'accéder à ce fichier, à partir du programme Java ME, pour récupérer les valeurs des attributs renseignés. Ceci se fait grâce à la méthode (classe MIDlet):

    String getAppProperty(String propertyName)

Ceci peut être util si jamais on souhaiterai modifier des informations utilisées par l'application sans avoir à changer le code et regénérer les binaires. Par exemple, imaginons que notre application accède à un site Internet et pendant la phase de développement nous ne disposons pas de l'adresse finale sur laquelle pointera le lien donc on pourra le spécifier dans la jad avec un attribut (exemple: URL) et récupérer sa valeur directement dans le code.

Les spécifications MIDP 2.0 introduisent le concept de domaine d'installation. En fonction de ce domaine l'AMS (Application Management Software) récupère l'information du fichier manifest.mf ou du fichier Jad pour permettre ou pas l'installation de l'application.

Voici ci-dessous le contenu d'un fichier JAD :


MIDlet-<1>: nom de la midlet, chemin vers l'icone, nom de la classe qui étend MIDlet
MIDlet-Description: une rapide description du programme
MIDlet-Icon: icône de l’application qui sera affichée dans le menu du téléphone
MIDlet-Jar-Size: taille en octet du fichier jar
MIDlet-Jar-URL: adresse à laquelle se trouve le fichier jar
MIDlet-Name: nom de la MIDlet
MIDlet-Vendor: fournisseur de la MIDlet
MIDlet-Version: 1.0
MicroEdition-Configuration: configuration
MicroEdition-Profile: version de MIDP


Exemple:
MIDlet-<1>: myMidlet,/res/icone.png, myMidlet
MIDlet-Description:  Ma MIDlet qui affiche du texte et des images
MIDlet-Jar-Size: 1234567
MIDlet-Jar-URL: myMidlet.jar
MIDlet-Name: myMidlet
MIDlet-Vendor: Y2R
MIDlet-Version: 1.0
MicroEdition-Configuration: CLDC-1.1
MicroEdition-Profile: MIDP-2.0


Quelques remarques:

  • Même si le JAD permet de s’assurer de certaines compatibilités entre le mobile et l’application, il ne permet pas de prévenir de l’utilisation d’une API non supportée par le mobile.
  • l’ordre des attributs dans le Manifest n’a pas d’importance
  • “:” sépare l’attribut de sa valeur
  • les espaces entres l’attribut et sa valeur sont ignorés
  • un seul attribut par ligne (cette ligne doit se terminer par un retour chariot)
  • le développeur peut utiliser un attribut personnel à condition qu’il ne commence pas par MIDlet- ou MicroEdition-
  • il faut respecter la casse pour l’écriture des attributs
  • les attributs ne doivent pas être dupliqués dans le Manifest
  • Le JAD est utilisé pour valider que l'application peut être installée sur le téléphone. Mais une fois l'application installée, il n'est pas conservé. Au contraire, le Manifest est utilisé par l'AMS pour toute la durée de vie de l'application. C'est la raison pour laquelle le JAD et le Manifest sont si similaires tout en étant indispensables tous les deux.
  • Certains opérateurs ou constructeurs ont leurs propres attribues. Exemples:
    •  Nokia 
      •  Nokia-MIDlet-Category
      • Nokia-Scalable-Icon
    • Vodafone 
      •  MIDxlet-ScreenSize
      • MIDxlet-Application-Range
      • ...
 
JAR: Java Archive

    Il s’agit d’un simple fichier archive avec :l’extension .jar : il contient les classes de l’application et tous types de fichiers nécessaires pour l’exécution (images, sons, textes…). Cette archive contient un répertoire « META-INF» qui, lui-même, contient un fichier « MANIFEST.MF ». Ce dernier est indispensable à l’installation et l’exécution de la MIDlet sur le téléphone : sans celui-ci l’installation sera impossible. Ce fichier ressemble beaucoup au fichier JAD. Sun a défini des règles très strictes à respecter lors de la génération de ce fichier (cf. Remarques dans le paragraphe précédent).

A l'exception des fichiers .class, il est possible d'accéder à des ressources stockées dans le Jar (divers types de fichiers...). Ceci se fait grâce à la méthode java.lang.Class.getResourceAsStream(String) qui renvoie la ressource en sous la forme d'un java.io.InputStream.

Exemples

Lire le fichier res.txt qui se trouve dont le chemin dans le jar est /res/txt/res.txt
    InputStream is = getClass().getResourceAsStream(/res/txt/res.txt );

On peut aussi lire une image grâce à la méthode suivante:
    Image img = Image.createImage("/PATH");   

La génération du Jar est semblable à celle d'un programme Java standard mais avec une phase supplémentaire (la pré-vérification).

Etape 1: Pre-processing
Dans le monde de la programmation Java ME, souvent les développeurs ont recours à l'utilisation d'un pré-processeur afin de faire face à la problématique de portabilité des applications sur les différents mobiles que l'application doit couvrir. Le préprocesseur prend en entrée un fichier Java avec des directives de précompilation (#if, #else...) et fournit en sortie un fichier Java standard.

Exemple
Voici un bout de programme Java qui contient des directives de pré-compilation. En fonction du choix du développeur, on affectera à la variable message soit "Bonjour le monde!" soit "Hello World!":

    String message = null;
#if FR
    message = "Bonjour le monde!";
#elif EN
    message = "Hello World!";
#endif

Utilisation sous Linux:
        Pour que le message soit en français ou en anglais, il faudra utiliser respectivement soit l'option -DFR soit -DEN

$ cpp -P -DFR MyClass.java.c -o MyClass.java


Notes
Sous Netbeans, un pré-processeur est intégré au module Java ME, on peut l'utiliser directement dans le code (réf. Netbeans et Java ME).
Autres méthodes similaires au preprocesseur pourraient être utilisées (m4, conditionnement Java...)

Etape 2: Compilation
La compilation d'un programme Java se fait avec la commande javac, il suffit d'utiliser l'option -bootclasspath pour dire au compilateur que les fichiers .class resultantes seront executées dans un environnement MIDP/CLDC. Le compilateur prend en entrée un fichier .java et fournie en sortie un fichier .class.

Exemple
$ javac -bootclasspath myMidlet.java /home/ry/lib/midpapi10.jar:/home/ry/lib/cldcapi10.jar

Etape 3: Obfuscation

L'obfuscation est une action sur un programme généré (ici les fichiers .class) qui sert essentiellement à:
  • protéger le code du reverse engineering
    • renommage des identifiants
    • modification de la visibilité des variables
    • renommage des classes
    • ...
  • optimiser les binaires générés
    • suppression du code inutil 
    • suppression des commentaires 
    •  ...

    L'outil le plus utilisé dans le monde Java ME est Proguard (http://proguard.sourceforge.net/). Il s'agit d'un programme Java qui permet d'obfusquer un autre programme Java. Il possède différents niveaux d'obfuscation.

Exemple :

            On pourrait lancer Proguard avec les options suivantes pour obfusquer une application (Pour plus de détail cf. http://proguard.sourceforge.net/).

-injars in.jar
-outjars out.jar
-libraryjars /usr/local/java/wtk2.1/lib/midpapi20.jar
-libraryjars /usr/local/java/wtk2.1/lib/cldcapi11.jar
-overloadaggressively
-repackageclasses ''
-allowaccessmodification
-microedition
-keep public class mypackage.MyMIDlet

Remarques:
  • Ne jamais utilisé l’obfuscateur lors de la phase de développement (surtout débogage) parce que javac ne saura pas vous donnez la bonne ligne qui génère l’erreur vu que ce type d'information est aussi supprimé par l'obfuscateur.
  • Certaines options de Proguard ne sont pas acceptées par quelques mobiles (BlackBerry)
  • L'obfuscation agit sur tout le code sauf la classe qui étend MIDlet d'où l'intérêt de mettre le minimum d'information dans celle ci (protection contre le reverse engineering).
  • L'obfuscation ne supprime pas les ressources non utilisées (images, son, fichiers...)


Etape 4: Pré-verification

    Il s'agit d'une étape spécifique au monde Java ME (cette phase se fait à l'exécution pour un programme Java SE). Elle permet de s'assurer que le bytecode Java est conforme à une liste de règles et introduit des annotations, dans les bytecode, indispensables à l'execution de la MIDlet par la VM. Sun fournit un outil pour effectuer cette étape, il s'agit de l'exécutable "preverify". Proguard permet aussi de le faire grâce à l'option (-microedition).

Exemple:
$ preverify -classpath /home/ry/lib/midpapi10.jar:/home/ry/lib/cldcapi10.jar  -d . myMidlet
 
Etape 5: Packaging

    La phase finale est la génération des fichiers finaux indispensables pour l'installation et l'exécution sur mobile. Il s'agit du fichier de description .jad et du fichier archive .jar (cf. Jar & Jad). Le fichier Jad est un simple fichier texte qu'on peut créer à la main alors que je fichier .jar doit être créé avec l'utilitaire de Sun "jar".

Exemple:
$ jar cvmf manifest.mf myJar.jar myMidlet.class

Cycle de vie d'une MIDlet

En lançant la MIDlet, l'AMS exécutera la méthode startApp() et l'arrete en appelant destroyApp(), l'arret pourrait se faire suite à une exeception (mémoire insuffisante, erreur d'exécution...) ou suite à une demande de l'utilisateur. Enfin l'application pourrait etre en pause suite à un évenement externe (appel entrant...) ou suite à la demande de l'utilisateur.


Sunday, November 29, 2009

L'AppStore d'Apple

Lors du lancement de l'iPhone, par le fondateur de la marque à la pomme, Steve jobs a présenté l'iPhone comme un produit innovant qui réinvente le concept de téléphone portable. Aujourd'hui tout le monde est d'accord pour dire que ce mobile est un vrai bijou technologique: premier écran multi-touche sans stylet, détecteur de position, reconnaissance vocale, clavier virtuel... Mais, à mon avis, la vraie révolution n'est pas dans l'appareil lui même mais dans le bras tendu par Apple aux développeurs du monde entier pour réaliser et distribuer leurs créations grâce à l'AppStore. Comme son nom l'indique il s'agit d'une boutique d'application en ligne qui contient à ce jour plus de 100 000 applications développées par des entreprises spécialisées, qu'on a vu naître avec l'arrivée de l'iPhone, ou par des amateurs passionnés de nouvelles technologies. Ce concept a rencontré un succès sans précédant avec plus d'un milliard d'applications téléchargées. Même le nouveau spot publicitaire télévisé de l'iPhone met en avant le nombre d'applications disponibles plutôt que le téléphone lui même. Après ce fulgurant succès, tous les géants du marché se sont pressés de créer leurs propres « stores ». Windows a annoncé son MarketPlace, Nokia (Ovi Store), Google (Android Market), Palm (App Catalog), Samsung (Samsung Mobile Applications), Sony Ericsson (PlayNow)... Nous sommes entrain d'assister à une révolution dans le domaine de l'application mobile où l'applicatif prend le dessus sur le matériel. Avant l'arrivée de l'iPhone, les constructeurs de mobiles avaient toujours la même approche, à savoir concevoir des téléphones de plus en plus beau, de plus en plus sophistiqués, de plus en plus orientés vers l'appareil gadget! Mais Apple voulait un téléphone qui offre de plus en plus de fonctionnalités.


Bien avant Apple, quelques entreprises avaient déjà eu l'idée de lancer des « stores », des sites comme « handandgo » ou « getjar » proposent aux développeurs de mettre en ligne leurs applications mobiles. Pour un utilisateur lambda, la démarche pour télécharger et installer un logiciel sur son portable n'est pas évidente et peut rapidement devenir un vrai casse-tête. Avec l'iPhone, plus besoin de se compliquer la vie, tout ce fait directement avec le téléphone. Notons aussi que les constructeurs de téléphones mobiles ont toujours donné une marge de manœuvre très limitée aux développeurs et souvent ceci a été justifié par des raisons de sécurité! Les fonctionnalités qui leurs sont offertes ne permettaient que la réalisation de jeux 2D très basiques type Snake, Tetris et Pack-man... qui s'adressaient à un public limité. Sur l'AppStore, on trouve des jeux 3D qui n'ont rien à envier aux jeux sur PC, des applications utiles (horaires des prières pour les musulmans, GPS, horaires des vols en temps réel, accès aux informations, Facebook...) et des applications professionnelles (agenda synchronisé, diffusion marketing, campagne publicitaire, cours des actions en bourse, pages jaunes...).

Une fois de plus Apple a su créer une vraie révolution en réinventant le mobile. Cette réussite est due, en grande partie, à la créativité et à l'imagination des développeurs du monde entier.

Friday, November 27, 2009

MIDlet

Le point d'entrée de tout programme MIDP est une classe qui hérite de la classe MIDlet du package javax.microedition.midlet.* Voici le plus petit programme Java ME qui fonctionne mais qui ne fait rien:

import javax.microedition.midlet.MIDlet;

/**
* Pppj2me.java
* Plus petit programme Java ME qui fonctionne
*/
public class Pppj2me extend MIDlet {
public void startApp(){}
    public pauseApp(){}
    public void destroyApp(boolean _b){}
}

La classe MIDlet étant abstraite, notre classe Pppj2me doit donc implémenter toutes les méthodes abstraites de MIDlet à savoir startApp(), pauseApp() et destroyApp(boolean b) dont voici les rôles:

  • startApp() est le point d'entrée (équivalent de la méthode main() dans un programme Java standard ou la méthode init() dans les applets)
  • pauseApp() est appelée quand la MIDlet entre en mode pause
  • destroyApp(boolean b) est appelée pour quitter l'application et libérer toutes les ressources qu'elle utilise

L'appel de ces méthodes se fait par l'AMS (Application Management Software) qui est un programme faisant partie de l'OS du mobile et non pas de la plateforme Java ME. L'AMS s'occupe aussi de:
  • l'installation, cycle de vie (active, pause...) suppression, lancement (opère comme la commande « java » en Java standard) des MIDlets
  •  l'intéractions entre l'OS et l'application (APIs, connexions I/O...). 
  •  définition du domaine d'installation de la MIDlet 
  •  l'écoute des connexions et alarmes pour le lancement automatique
  •  ...

La Fragmentation Java ME

Si on récapitule brièvement ce que l'on vient de dire dans ce premier article, on notera qu'il ya :
  • de plus en plus de mobile avec des caractéristiques différentes (taille d'écran, nombre et disposition des touches, écran tactile...)
  •  différents fournisseurs de KVM
  •  2 versions de CLDC
  •  3 (~4) versions de MIDP
  • des APIs optionnelles et/ou proprétaires
  •  ...
Ainsi on se retrouve avec un marché de mobile dans lequel les téléphones sont plus ou moins différents les uns des autres. Malheureusement pour les développeurs, ceci pose un problème assez connu dans le monde Java ME et qui est le problème de portabilité. Ce problème fait que le développement d'une application pour plusieurs téléphones avec un code unique devient un enjeux difficile. Plusieurs méthodes et actions ont été mises en place, en voici les plus intéressantes.

JTWI
La première action pour résoudre ce problème était la JSR 185 appelée Java Technology for Wireless Industry (JTWI) qui impose le support de MIDP 2.0, CLDC (1.1 ou 1.0), WMA et laisse optionnel celui de MMAPI.

MSA
Après la spécification JTWI, de plus en plus d'APIs ont vu le jour alors une nouvelle spécification appelée Mobile Service Architecture (MSA JSR 248) a été définie. Pour les mobiles a capacités relativement limitées, une sous couche de MSA a été mise en place.


MSA définie deux types de JSR: obligatoire ou obligatoire à condition. Les APIs qui nécessitent un matériel spécifique sont de type obligatoire à condition comme par exemple Location API (JSR 179) qui nécessite que le mobile soit équipe de GPS ou encore comme pour l'API Bluetooth.

Tout au long de ce live nous détaillerons toutes les APIs une par une et nous présenerons des exemples d'utilisation.

« Write  once,  run  anywhere »  ou  « ?»
    Java est un langage portable. Nous avons tous appris ça quand on a commencé à programmer sauf que ce qu'on oublie souvent de dire est que la portabilité n'est assurée que si on utilise la même machine virtuelle. Donc je dirais Java est portable si on l'exécute sur une JVM unique. Malheureusement, dans le monde de la téléphonie mobile, ce n'est pas le cas, on vient de voir que les téléphones embarquent des KVM avec un nombre d'APIs qui varient de 3 à 16!!
En plus vu que certaines APIs dépendent du matériel présent sur le mobile, on imagine bien que ceci peut être un facteur important quant à l'implémentation de l'API sur un téléphone donné. L'image ci dessous montre le nombre important de facteurs qui pourraient jouer sur la portabilité d'une application.

Malgré le JTWI et le MSA, aujourd'hui tous les développeurs ont recours à des outils et/ou des méthodes de développement pour s'assurer du bon fonctionnement de leurs applications sur un nombre important de téléphone.  Dans le prochain chapitre nous présenterons l'outil le plus utilisé: le pré-processeur.