mercredi 6 juin 2018

Chapitre 32 : fenêtre X11 et gestion des évenements


Nous allons reprendre le programme précèdent pour l’améliorer en incluant la correction du message d’erreur, un titre pour la fenêtre, la saisie de caractère ou l’appui sur un bouton de la souris.
Nous reprenons le début du programme avec la connexion au serveur X11, la récupération des données et la création de la fenêtre. Pour cette dernière, nous inversons les codes RGB des pixels du fond d’écran et de la bordure.
Ensuite, nous allons mettre à jour le titre de la fenêtre avec son nom par la fonction XSetWMName
. Mais cette fonction ne prend pas directement la chaine de caractères du nom, il faut auparavant la convertir en une propriété avec la fonction XStringListToTextProperty.
Ensuite nous trouvons les instructions pour corriger le message d’erreur qui apparait lors de la fermeture de la fenêtre. J’ai trouvé cette correction sur internet et elle est un peu compliquée mais elle fonctionne. Il sera necessaire de conserver ces instructions dans tous les programmes suivants. En fait, nous créons une entité par la fonction XInternAtom dont nous stockons l’identification en mémoire et nous passons cette identification au serveur par la fonction XSetWMProtocols. Cette identification nous sera retournée dans certains messages liés aux évenements de la fenêtre.
Puis nous affichons la fenêtre avec la fonction XMapWindow comme déjà vu.
Et nous trouvons une nouvelle fonction qui va autoriser la prise en compte des saisies par le gestionnaire des évènements. Ici nous autorisons la saisie d’une touche du clavier et le clic des boutons de la souris. Si dans un programme, le gestionnaire d’évènements ne réagit pas, c’est qu’une autorisation n’a pas été donnée ou  a mal fonctionnée. C’est un point sensible !!
Enfin nous trouvons la boucle de gestion des évènements avec l’appel à la fonction XNextEvent qui retourne à l’adresse mémoire passée en paramètre le contenu de l’évènement que nous avons autorisé précédemment. Nous effectuons un vidage des données pour vérification. Les 4 premiers octets contiennent le code évènement. Nous testons la valeur 2 (KEYPRESSED) pour savoir si une touche a été appuyée ou la valeur 4 (ButtonPress) si c’est un bouton de la souris. Dans le premier cas, il nous faut déterminer s’il s’agit d’un caractère alpha ou une touche de fonction. Pour cela nous appelons la fonction XLookupString qui va retourner la valeur 1 s’il s’agit d’un caractère et sa valeur sera stockée dans un buffer dont l’adresse a été passée en paramètre de la fonction. Nous testons la valeur q (pour quitter) et si c’est le cas nous fermons la connexion et terminons le programme. Vous pouvez vous amuser à appuyer sur d’autres touches et voir dans les vidages les différents codes retournés.
Si l’évènement correspond à un clic de la souris, nous nous contentons de récupérer dans les registres r1 et r2 les positions X et Y du clic et nous affichons ces registres pour voir les valeurs.
Si l’évènement correspond au code 33 (clientMessage) il s’agit d’une demande fermeture de la fenêtre par le menu système. Dans ce cas, nous vérifions que ce message contient bien la même identification que celle que nous avons positionnée et si oui, nous fermons la connexion et cette fois ci il n’y a pas de message d’erreur.
Vous remarquerez que le passage de la souris sur l’écran sans cliquer ou le redimensionnement de la fenêtre n’entraine pas de réaction du gestionnaire d’évènement. Ceci est normal puisque nous n’avons pas autorisé ces évènements par la fonction XSelectInput.

lundi 28 mai 2018

Chapitre 10A : appels de fonctions


(remarque : ce chapitre est publié en retard car je l'avais oublié).
Après ces premiers chapitres, nous allons revenir sur les appels de fonctions en C et les appels système Linux pour vérifier le passage des paramètres.
Tout d’abord, nous allons essayer la fonction printf en lui passant 5 paramètres dans les registres r0 à r4 dans un petit programme :

       

/* passage de parametres  avec la fonction printf du C */
/* problème avec plus de 4 paramètres */
.data
szMessage1: .asciz "parametre 1:%d 2:%d 3:%d 4:%d\n "
.text             /* -- Code section */
.global main      /* point d'entrée du programme  */
main:             /* Programme principal */
    ldr r0, =szMessage1      /* adresse du message */
    mov r1,#1                /* paramétres */
    mov r2,#2
    mov r3,#3
    mov r4,#4
    bl printf          /* appel de la fonction du C */
    mov r0,#0  /* code retour r0 */
    mov r7, #1 /* code pour la fonction systeme EXIT */
    swi 0      /* appel system */
 
 


 r0 contient l’adresse du message et r1 à r4 les valeurs 1 à 4.
 Voici le résultat :
parametre 1:1 2:2 3:3 4:-1225867264
Nous remarquons que la 4ième valeur ne correspond plus à la valeur en entrée. La documentation du C explique que seuls les 4 premiers registres sont utilisés pour passer les 4 premiers paramètres. Pour les autres, il est indiqué de les passer par la pile.  Nous allons donc modifier ce programme pour vérifier le passage des paramètres par la pile. Nous en profitons aussi pour mettre des valeurs dans tous les autres registres (sauf bien sur sp,cp et lr) pour vérifier leur état après l’appel et nous allons aussi vérifier l’état de la pile.
       


/* programme  affichage avec la fonction printf du C */
/* pour verification ordre parametre par push */
/* et état de la pile au retour  */
.data
szMessage1: .asciz "parametre 1:%d 2:%d 3:%d 4:%d 5:%d \n "
.text             /* -- Code section */
.global main      /* point d'entrée du programme  */
main:             /* Programme principal */
    bl vidtousregistres
 mov r5,#5   /* pour verification des registres */
 mov r6,#6
 mov r7,#7
 mov r8,#8
 mov r9,#9
 mov r10,#10
 mov r11,#11
 mov r12,#12
    ldr r0, =szMessage1      
    mov r1,#1   @ param1
 mov r2,#2    @ param2
 mov r3,#3     @ param3
 mov r4,#5    
 push {r4}     @ param5
 mov r4,#4
 push {r4}     @ param4
    bl printf          /* appel de la fonction du C */
 //add sp,#8       /* alignement de la pile */
 bl vidtousregistres
    mov r0,#0  /* code retour r0 */
 mov r7, #1 /* code pour la fonction systeme EXIT */
    swi #0      /* appel system */

 
Voici le résultat :

Nous remarquons que l’affichage des 5 paramètres est cette fois correct et si vous regardez bien le programme, vous remarquerez que le paramètre 5 a été mis sur la pile avant le paramètre 4 : il ne faudra jamais l’oublier !!. Il est aussi conseillé de passer les paramètres en nombre pair car l’adresse de la pile doit toujours être un multiple de 8 octets (ou alors d’ajouter une instruction sub sp,#4 avant le passage du ou des paramètres en nombre impairs).
Comme indiqué, après l’appel, les valeurs des registre r0 à r3 ont été modifiées, les registres R4 à r11 sont bien conservés, le registre 12 a été modifié ( ce qui est contradictoire avec certaines documentations) et plus embêtant, l’adresse de la pile n’est plus à sa valeur d’origine. En effet, c’est au programme appelant à rétablir l’adresse de la pile après l’appel en fonction du nombre de paramètres passés sur la pile : ici il faut ajouter l’instruction add sp,#8 pour remettre la pile en l’état (8 octets car 2 paramètres de 32 bits). Ceci n’est jamais à oublier lors de l’appel à des fonctions à des bibliothèques extérieures. Pour vos propres fonctions, vous pouvez faire comme vous voulez !!!
Maintenant effectuons, la même démarche pour les call système Linux mais cela est moins facile de tester plus de 4 paramètres avec l’appel Write. J’ai donc cherché dans la documentation Linux et j’ai trouvé que les call système ne nécessitait au maximum que 7 paramètres et qu’ils étaient tous passés par les registres r0 à r6 (ce qui explique entre autre que le code fonction est passée dans le registre r7). Nous allons quand même vérifier l’état des registres et l’état de la pile après un appel à la fonction WRITE. Voici le source du programme param3.s.
       


/* programme hello  avec l'appel systeme Write de Linux */
/*  */
/********************************/
/*  Données initialisées        */
/********************************/
.data
szMessage1: .asciz "Bonjour le Monde.\n"
.equ LGMESSAGE1, . -  szMessage1 /* calcul de la longueur de la zone precedente */
/********************************/
/*  Code section                */
/********************************/
.text    
.global main      /* point d'entrée du programme  */
main:             /* Programme principal */
    mov r3,#3  /* pour verification des registres */
 mov r4,#4
 mov r5,#5  
 mov r6,#6
 mov r8,#8
 mov r9,#9
 mov r10,#10
 mov r11,#11
 mov r12,#12
    mov r0,#1      /* code pour écrire sur la sortie standard Linux */
    ldr r1, =szMessage1      /* adresse du message en r1 */
    mov r2,#LGMESSAGE1       /* longueur du message */
    mov r7, #4                  /* code de l'appel systeme 'write' */
 bl vidtousregistres
    swi #0                      /* appel systeme */
 
 bl vidtousregistres
 
 /* FIN programme */
    mov r0,#0  /* code retour r0 */
 mov r7, #1 /* code pour la fonction systeme EXIT */
    swi 0      /* appel system */
  
 

 et le résultat :


Nous pouvons vérifier que les registres r3 à r12 ne sont pas modifiés et que l’adresse de la pile reste inchangée dans ce cas. Vous remarquerez que le registre r0 contient 13 en hexa ce qui est le nombre de caractères affichés par la fonction.

Chapitre 31 : création d'une première fenêtre avec X11


Commençons par exécuter à nouveau le programme du chapitre précédent pour entendre le doux son de Windows. Mais hélas, nous recevons notre premier message d’erreur :
PuTTY X11 proxy: unable to connect to forwarded X server: Network error: Connection refused
Serveur X non trouvé.
Soit nous n’avons pas utilisé la connexion créee au chapitre précédent (Par exemple dans mon Putty, j’ai plusieurs connexions) ou le serveur XMing sur le PC doit être relancé. En effet vous avez pu éteindre le PC et oubliez au redémarrage de relancer le serveur XMing. D’ailleurs le mieux c’est de le lancer systématiquement en début de session en plaçant un raccourci dans le menu démarrer.
Cette vérification faite, nous pouvons travailler sur notre deuxième programme où nous allons essayer d’afficher une fenêtre en utilisant la fonction XCreateSimpleWindow.
Pour cela, nous devons effectuer les étapes suivantes :
                Ouvrir une connexion au serveur d’affichage par XOpenDisplay
                Récupérer des informations dans la structure d’affichage (Display)
                Créer la fenêtre par XCreateSimpleWindow en lui passant 9 paramètres.
                Afficher la fenêtre par XMapWindow
                Mettre en place un gestionnaire d’évènements (et oui il faut bien arriver à fermer la fenêtre avec la souris) fonction XNextEvent
                Fermer et détruire la fenêtre par  XDestroyWindow
                Fermer la connexion au serveur par XCloseDisplay.
Etape 1 : Après l’appel de la fonction XOpenDisplay  nous récupérons un pointeur vers la structure Display dont la description en langage C est la suivante (toutes les structures sont décrites dans l’entité xlib.h) :

_XDisplay
{
                XExtData *ext_data;     /* hook for extension to hang data */
                struct _XPrivate *private1;
                int fd;                                   /* Network socket. */
                int private2;
                int proto_major_version;/* major version of server's X protocol */
                int proto_minor_version;/* minor version of servers X protocol */
                char *vendor;                   /* vendor of the server hardware */
        XID private3;
                XID private4;
                XID private5;
                int private6;
                XID (*resource_alloc)(  /* allocator function */
                               struct _XDisplay*
                );
                int byte_order;                                /* screen byte order, LSBFirst, MSBFirst */
                int bitmap_unit;              /* padding and data requirements */
                int bitmap_pad;                              /* padding requirements on bitmaps */
                int bitmap_bit_order;   /* LeastSignificant or MostSignificant */
                int nformats;                     /* number of pixmap formats in list */
                ScreenFormat *pixmap_format;             /* pixmap format list */
                int private8;
                int release;                        /* release of the server */
                struct _XPrivate *private9, *private10;
                int qlen;                              /* Length of input event queue */
                unsigned long last_request_read; /* seq number of last event read */
                unsigned long request;                /* sequence number of last request. */
                XPointer private11;
                XPointer private12;
                XPointer private13;
                XPointer private14;
                unsigned max_request_size; /* maximum number 32 bit words in request*/
                struct _XrmHashBucketRec *db;
                int (*private15)(
                               struct _XDisplay*
                               );
                char *display_name;     /* "host:display" string used on this connect*/
                int default_screen;        /* default screen for operations */
                int nscreens;                     /* number of screens on this server*/
                Screen *screens;            /* pointer to list of screens */
                unsigned long motion_buffer;  /* size of motion buffer */
                unsigned long private16;
                int min_keycode;            /* minimum defined keycode */
                int max_keycode;           /* maximum defined keycode */
                XPointer private17;
                XPointer private18;
                int private19;
                char *xdefaults;              /* contents of defaults from server */
                /* there is more to this structure, but it is private to Xlib */
}

Wauoh !!  complexe non !! Pour l’instant, nous allons nous contenter de repérer les infos intéressantes et de les récupérer par un déplacement à  ajouter au pointeur retourné par xopenDisplay. Par la suite nous utiliserons les descriptions de structures que j’ai traduites en formalisme assembleur (merci qui ?).
Ici nous avons besoin de récupérer un pointeur vers l’écran (screen) de l’affichage. Cette information se trouve au déplacement 140. Ce pointeur pointe sur une structure de type screen dont voici la description en C :

typedef struct {
                XExtData *ext_data;     /* hook for extension to hang data */            0
                struct _XDisplay *display;/* back pointer to display structure */    4
                Window root;                   /* Root window id. */                            8
                int width, height;            /* width and height of screen */                 12  et 16 ?
                int mwidth, mheight;    /* width and height of  in millimeters */    20  et 24
                int ndepths;                      /* number of depths possible */                  28
                Depth *depths;                               /* list of allowable depths on the screen */     32
                int root_depth;                               /* bits per pixel */                             36
                Visual *root_visual;       /* root visual */                            40
                GC default_gc;                 /* GC for the root root visual */                44
                Colormap cmap;                              /* default color map */                          48
                unsigned long white_pixel;                                           52
                unsigned long black_pixel;         /* White and Black pixel values */       56
                int max_maps, min_maps;         /* max and min color maps */
                int backing_store;           /* Never, WhenMapped, Always */
                Bool save_unders;
                long root_input_mask; /* initial root input mask */
} Screen;

Nous trouvons des informations intéressantes comme l’identification de la fenêtre mère (position8), la taille de l’écran(position 12 et 16), le nombre de bits de codage d’un pixel (position 36 les codes RGB du pixel blanc et du pixel noir (position 52 et 56).  Nous mettons ces infos dans les registres r1 r3,r4 et r5 ( affichage du contenu par la macro d’affichage des registres pour vérification).
Maintenant, nous pouvons créer la fenêtre avec la fonction XCreateSimpleWindow dont la signature en C est la suivante :

Window XCreateSimpleWindow(display, parent, x, y, width, height, border_width,
border, background)
Display *display;
Window parent;
int x, y;
unsigned int width, height;
unsigned int border_width;
unsigned long border;
unsigned long background;
display Specifies the connection to the X server.
parent Specifies the parent window.
xy
Specify the x and y coordinates, which are the top-left outside corner of the new
window’s borders and are relative to the inside of the parent window’s borders.
width
height Specify the width and height, which are the created window’s inside dimensions
and do not include the created window’s borders. The dimensions must be
nonzero, or a BadValue error results.
border_width Specifies the width of the created window’s border in pixels.
border Specifies the border pixel value of the window.
background Specifies the background pixel value of the window.

Donc nous mettons dans r0, le pointeur du Display, dans r1 l’identification de la racine récupérée dans la structure écran précédente, zéro dans r2 et r3 pour la position X et Y de la fenêtre et  stop !! nous ne pouvons pas alimenter les autres registres pour passer les paramètres. Il nous faut respecter la normalisation des appels de fonctions standards (voir le chapitre ) ou seuls les 4 premiers registres sont utilisés pour passer ces paramètres, les autres doivent être passés par la pile. Et il nous faut aussi respecter que la pile soit toujours alignée sur une frontière de double mot et donc empiler un nombre de registre pair. Ici, il y a 9 paramètres à passer dont 4 par les registres et il en reste 5 donc il faut faire un push de plus pour avoir un nombre pair. C’est pourquoi nous commençons par mettre zero dans le registre r8 et nous effectuons un push de celui-ci. Puis nous passons les autres paramètres et dans l’ordre inverse demandé dans la signature de la fonction. Nous passons la valeur du pixel blanc comme valeur du pixel du fond d’écran, le pixel noir pour la bordure puis la valeur 5 pour la taille de la bordure (par l’intermèdiaire du registre r8) puis la hauteur et la largeur de la fenêtre. Ouf !!
Après l’appel de la fonction, nous réalignons la pile par l’instruction add sp,#24 pour compenser les 6 push de 4 octets et nous testons le code retour de la fonction. Ceci sera à faire après chaque appel de fonction X11 pour vérifier si tout c’est bien passé et éviter des recherches laborieuses.
Si tout est OK, nous récupérons un pointeur vers une structure de type fenêtre (window), pointeur que nous allons utiliser pour afficher notre fenêtre par XMapWindow.
Puis nous avons une boucle qui va gérer les évènements reçus dans l’affichage. Ici nous nous contentons d’appeler la fonction XNextEvent en lui passant le pointeur du Display et un pointeur vers une zone de mémoire qui stockera l’évènement reçu. Remarque : si cette fonction n’est pas appelée, la fenêtre ne sera pas affichée. Eventuellement elle peut être affichée en appelant la fonction Xflush et sans la boucle.
Comme on ne gère aucun évènement, le programme ne fait rien de plus.
Au lancement du programme, une fenêtre doit apparaitre sur votre PC avec le titre Xming et les menus système habituels. Vous pouvez déplacer la fenêtre, la réduire, la redimensionner comme toute fenêtre. Vous la fermer en cliquant sur la X et vous avez le magnifique message suivant :
XIO:  fatal IO error 11 (Resource temporarily unavailable) on X server "localhost:10.0"
      after 8 requests (6 known processed) with 0 events remaining.
Nous verrons au chapitre suivant comment éliminer cette erreur.
Enfin pour terminer le chapitre, rappelons quelques principes de X11 :
Toute fenêtre est contenue dans une fenêtre mère (ou racine)
Toute fenêtre fille est contenue dans une fenêtre mère ou est tronquée
Une fenêtre parent a toujours un titre.
Les boutons, menus, boites de dialogues  sont tous des fenêtres.
Les positions et tailles des fenêtres sont mesurées en pixels.
Chaque fenêtre à son propre système de coordonnées.
Une autre question se pose : cette fenêtre s’affiche-t-elle  aussi sur l’écran du Raspberry ?  Et bien, il faut brancher l’écran, le clavier , relancer le Raspberry, et à partir de l’écran graphique rechercher dans l’arborescence de fichier le programme et l’éxecuter. Mais on peut aussi voir une image de l’écran avec la solution proposée pour les tests du Framebuffer avec le serveur x11vnc sur le raspberry et le client tightvncViewer sur Windows et ça fonctionne.

Exercice :  modifier la position et la taille de la fenêtre
                Modifier la couleur du fond de la fenêtre.
                La taille de la bordure ne semble pas fonctionner (n’est pas large de 5 pixels dans notre exemple !!), rechercher pourquoi ?
                Dans la structure Display, il y a un pointeur vers une chaine de caractères qui donne le nom du vendeur, afficher cette chaine. Vous pouvez aussi afficher les versions du système X11.