====== libxosd sous guile ====== ===== Présentation ===== Bien souvent il arrive au programmeur en quête de réutilisation de vouloir se lier à une bibliothèque partagée existante, mais qui malheureusement se trouve être écrite dans un autre langage que celui qu'il compte employer. Dans le cas le plus commun, il s'agira probablement d'une bibliothèque écrite en C (ce qui correspond aux recommandations du projet GNU). Le programmeur n'a alors d'autre choix que de recourir à une couche de **liaison** (**binding** en anglais) pour importer les fonctionnalités de ladite bibliothèque dans son langage favori. Dans ce tutoriel, nous nous intéresserons à l'exploitation de la [[http://www.ignavus.net/software.html|libxosd]] dans un programme écrit (ou extensible) en [[http://www.gnu.org/software/guile/guile.html|guile]], le langage d'extension officiel du projet GNU, qui est une implémentation de scheme. On pourra notamment envisager l'utilisation de cette liaison pour l'affichage de noms de bureaux virtuels dans [[http://scwm.mit.edu|Scwm]], la génération d'un énorme message d'avertissement lorsque vos comptes (gérés par gnucash) seront dans le rouge, ou simplement l'écriture de scripts guile servant à notifier un événement quelconque. Pour être complets il convient de noter que les langages considérés ne sont pas neutres: en effet guile exporte une interface native C, ce qui simplifie notablement le portage de la bibliothèque choisie. ===== De C à guile ===== ==== Encapsulation de la structure de données ==== Pour commencer, il est bon de se rappeler (ou d'apprendre) que guile manipule un seul type de données: ''SCM'', aussi les fonctions que nous allons écrire manipuleront ce type en lieu et place des arguments C des fonctions "normales". Nous aurons donc recours à un certain nombre d'assertions et de conversions pour passer des arguments scheme aux arguments réels dans de bonnes conditions (et vice versa). La libxosd adopte un modèle objet centré autour d'une structure ''xosd''. Ce type jouant un rôle central, nous devons tout d'abord lui donner un équivalent guile. Guile permet de définir des ''smobs'' (**small objects**), qui nous permettront de faire ce que nous voulons avec les objets ''xosd''. La définition d'un ''smob'' se fait simplement en lui donnant un nom et une taille : xosd_tag = scm_make_smob_type(xosd,sizeof( xosd *)); Remarquons que nous n'avons pas fait de lien entre le ''smob'' et la structure de données que nous voulons encapsuler ''(xosd *)''. Ce lien ne sera fait qu'à la création d'un nouvel objet du type de ce ''smob''. Ainsi, ''SCM_RETURN_NEWSMOB(xosd_tag, w);'' où ''w'' est de type ''xosd*'' crée un objet guile auquel l'identifiant ''xosd_tag'' est attaché, et dont on pourra contrôler le type ultérieurement. Nous reviendrons sur cette étape lorsque nous nous intéresserons à l'écriture d'un constructeur. Passons à présent aux fonctions permettant conversion d'un ''SCM'' en son véritable type C. Nous aurons dans la majeure partie des cas à vérifier que le premier argument de la fonction considérée est bien un pointeur vers un ''xosd''. L'interface de programmation des ''smobs'' nous fournit une macro pour cela: SCM_SMOB_PREDICATE(xosd_tag,osd) qui teste l'appartenance de ''osd'' (de type ''SCM'') au type ''smob'' d'identifiant ''xosd_tag''. D'autres macros de test sont disponibles pour les types "primitifs": ainsi on pourra écrire ''SCM_INUMP(n)'' pour tester l'appartenance de ''n'' aux entiers naturels. Pour effectuer un vrai test de validité d'appel, nous avons encore besoin de la macro suivante SCM_ASSERT(condition,argument_effectif, argument_symbolique,fonction) qui se chargera d'interrompre l'exécution et de lever une erreur si ''condition'' n'est pas respectée. Enfin, lorsque toutes le vérifications sont passées, il nous reste encore à exécuter réellement les conversions de types nécessaire. Nous avons vu précédemment la macro ''SCM_RETURN_NEWSMOB'' qui nous permettait de convertir un ''xosd*'' en son smob associé, il est temps de découvrir la macro ''SCM_SMOB_DATA(x)'' qui, appliquée au résultat précédent, retourne le ''xosd*'' de départ. Dans la suite, nous utiliserons la macro suivante. #define XOSD(x) ((xosd *) SCM_SMOB_DATA(x)) Encore une fois, il existe également des macros de conversion pour les types "primitifs", ainsi ''SCM_INUM(n)'' appelé sur un ''SCM'' renverra un ''int'', et la conversion est valide sous réserve que ''SCM_INUMP(n)'' soit vrai. ==== Encapsulation d'une fonction ==== Une fois les données correctement encapsulées, l'encapsulation d'une fonction se fait assez facilement suivant un schéma constant : * vérification des arguments ; * appel du code utile ; * construction de la valeur de retour. Voyons un exemple complet d'encapsulation de la fonction ''xosd_set_horizontal_offset(xosd * osd, int n)'' static SCM _wrap_xosd_set_horizontal_offset(SCM osd, SCM n) { SCM_ASSERT(SCM_SMOB_PREDICATE(xosd_tag,osd), osd, SCM_ARG1, "xosd-set-horizontal-offset!"); SCM_ASSERT(SCM_INUMP(n), n, SCM_ARG2, "xosd-set-horizontal-offset!"); xosd_set_horizontal_offset(XOSD(osd),SCM_INUM(n)); return SCM_UNSPECIFIED; } La valeur ''SCM_UNSPECIFIED'' retournée peut être vue comme l'équivalent d'un ''void'' C: une valeur inutilisable. ==== Adaptation des interfaces ==== En passant d'un langage à un autre, il apparaît rapidement que certaines constructions, parfaitement adaptées dans l'un, se trouvent tout à fait inadaptées dans l'autre. Ainsi, la fonction suivante semble complexe, et peu transposable en scheme : int xosd_display (xosd *osd, int line, xosd_command command, ...); // command appartient à l'ensemble // {XOSD_percentage, XOSD_string, XOSD_printf, XOSD_slider} Il est bien sûr possible d'écrire une fonction scheme qui adopte le même comportement, mais la simple présence d'arguments génériques rendrait la solution inélégante. Si des adaptations mineures de l'interface sont envisageables, on pourra faire le choix de séparer les différents comportements (les différentes valeurs de ''command'') en plusieurs fonctions. D'autre part la commande ''XOSD_printf'' est clairement écrite comme un substitut à la fonction ''printf()'' du C, par conséquent, il est logique de ne **pas** la transcrire telle quelle, mais d'émuler son comportement à travers la fonction scheme ''format'' et grâce à la commande ''XOSD_string''. Ainsi les chaînes de formatage acceptées ne seront pas nécessairement les mêmes, mais elles seront cohérentes avec les habitudes liées au langage. Nous écrirons donc trois fonctions: ''xosd-display-percentage'', ''xosd-display-slider'', et ''xosd-display-string''. Une fois ceci fait, on pourra sans problème écrire ''(xosd-display-string (format "%d" n))'' pour simuler une fonction ''(xosd-display-printf)'' // affiche str à la ligne line int xosd_display_string(xosd *osd, int line, char *str) { return xosd_display(osd,line,XOSD_string,str); } static SCM _wrap_xosd_display_string(SCM osd, SCM line, SCM str) { SCM_ASSERT(SCM_SMOB_PREDICATE(xosd_tag,osd), osd, SCM_ARG1, "xosd-display-string"); SCM_ASSERT(SCM_INUMP(line), line, SCM_ARG2, "xosd-display-string"); SCM_ASSERT(SCM_STRINGP(str), str, SCM_ARG3, "xosd-display-string"); xosd_display_string(XOSD(osd),SCM_INUM(line), SCM_STRING_CHARS(str)); return SCM_UNSPECIFIED; } Dans un registre différent, il arrive qu'en C on émule le retour de plusieurs valeurs par le passage de pointeurs vers des variables locales. Ainsi la fonction ''xosd_get_colour'' a le profil suivant: int xosd_get_colour(xosd *osd, int *red, int *green, int *blue); Cette façon de faire est tout à fait inappropriée en scheme (puisque l'on n'a pas de pointeurs), où l'usage dans des cas semblables est plutôt de renvoyer une liste de résultats. Ainsi on voudra plutôt : (xosd-get-colour osd) -> (42 42 42) ==== Constructeurs/destructeurs ==== libxosd est une bibliothèque objet (bien qu'écrite en C) et fournit des fonctions pour construire et détruire des instances. Le premier réflexe (malheureux, évidemment) sera d'encapsuler ces méthodes comme les autres, et de les utiliser en l'état en lisp. Bien que cela fonctionne au premier abord, il subsiste un problème majeur: supposons que nous ayions écrit une fonction ''xosd-destroy'' qui sert de destructeur. Que se passera-t-il si nous réutilisons l'objet détruit? la même chose qu'en C, une **segfault**. D'autre part, que se passera-t-il si nous perdons un object ''xosd'' en faisant par exemple ''(set! xosd nil)'' (ceci a pour conséquence d'affecter ''nil'', qui est la valeur **nulle** de scheme à la variable ''xosd'')? absolument rien, mais il nous est désormais absolument impossible de récupérer la mémoire allouée pour cet objet (**memory leak**). Ces deux facettes du même problème mettent en exergue le fait que dans un programme scheme, la mémoire doit être gérée automatiquement, il faut donc s'assurer que les objets passent bien par le ramasse-miettes (**garbage collector**) pour ne pas avoir de problème. Guile fournit une interface pour associer à un ''smob'' une fonction de libération et une fonction de marquage. La première sert à libérer explicitement la mémoire, la seconde sert à marquer d'éventuels objets à passer au ramasse-miettes. Ces fonctions sont installées lors de la phase d'initialisation du module par les appels suivants : scm_set_smob_mark(xosd_tag,mark_xosd); scm_set_smob_free(xosd_tag,free_xosd); ils permettent au ramasse-miettes de prendre conscience de l'existence d'objets supplémentaires, et surtout de la manière dont il doit les gérer. Puisque notre ''smob'' n'est en fait rien d'autre qu'un ''xosd*'' la fonction de marquage est triviale. La fonction de libération, quant à elle, est simplement un enrobage autour de ''xosd_destroy()''. En ce qui concerne le constructeur en revanche, rien de particulier, si ce n'est qu'il ne faut pas oublier que l'on a un ''smob'' à créer (cf encapsulation de la structure). // crée un objet xosd à n lignes static SCM make_xosd(SCM n) { xosd * w; SCM_ASSERT(SCM_INUMP(n), n, SCM_ARG1, "make-xosd"); w = xosd_create(SCM_INUM(n)); SCM_RETURN_NEWSMOB(xosd_tag, w); } // rien à marquer, le smob est simple static SCM mark_xosd(SCM xosd_smob) { return SCM_BOOL_F; } // libération du smob static size_t free_xosd(SCM xosd_smob) { xosd * osd = XOSD(xosd_smob); xosd_destroy(osd); return sizeof( xosd * ); } Insistons sur le fait que la fonction de libération n'a absolument pas vocation a être appelée par l'utilisateur (d'ailleurs elle renvoie un ''size_t'' qui ne signifie rien en ''scheme''). L'appel sera exclusivement interne. Une fois ceci fait, la désallocation se fera automatiquement lors d'un passage du ramasse-miettes. Ainsi, si nous reprenons notre exemple de ''(set! xosd nil)'', au prochain passage du ramasse-miettes, l'ancien contenu de ''xosd'' sera détecté comme désormais inutilisable (car non atteignable) et ''free_xosd()'' sera appelé. ==== Définitions et export de module ==== Une fois que toutes les fonctions de la bibliothèque d'origine ont été encapsulées pour présenter un profil plus **lisp**, il est encore nécessaire de les enregistrer auprès de l'interpréteur à l'aide de la fonction ''scm_c_define_gsubr()''. scm_c_define_gsubr("make-xosd", 1, 0, 0, make_xosd); les trois paramètres entiers représentent respectivement le nombre de paramètres obligatoires, le nombre de paramètres optionnels, et une valeur booléenne signalant l'existence ou non d'un "reste" d'arguments, mais ce n'est pas le propos ici. Cette instruction associe au symbole d'identifiant "make-xosd" la fonction C ''make_xosd()''. Le langage scheme possède un système de modules (que l'on peut voir comme une série d'espaces de (re)nommage), chacun exportant une interface à destination de l'utilisateur. Par défaut, les fonctions déclarées dans un module ne sont pas exportées (elles sont privées en quelque sorte), aussi nous devons explicitement exporter chacun des symboles de fonctions définis, et ce grace à ''scm_c_export()'' qui accepte une série d'arguments terminée par ''NULL'' scm_c_export("make-xosd", ... , "xosd-display-string", NULL); ==== Compilation ==== Une simple compilation en bibliothèque partagée suffit à rendre les fonctions créées accessibles depuis guile, sous réserve qu'il existe une fonction particulière ''scm_init__module'' si la bibliothèque s'appelle ''lib.so'' et que cette fonction déclare l'ensemble des symboles et fonctions définis. SCM scm_init_xosdguile_module (void) { SCM module; module = scm_c_define_module("xosdguile", init_helper, NULL); return SCM_UNSPECIFIED; } où ''init_helper'' est une fonction chargée de la création du module proprement dit, en d'autres termes, il s'agit de la fonction contenant les instructions de définition et export vues précédemment. Si notre fichier C s'appelle ''xosd_wrap.c'', la ligne de compilation idoine est la suivante: gcc -Wall -ansi -pedantic -shared -o libxosdguile.so -lxosd xosd_wrap.c ===== Exemple ===== Donnons maintenant un petit exemple d'utilisation. Pour être original, on veut afficher un "Hello World!" en bas de l'écran, centré avec un décalage supplémentaire de 80 pixels vers la droite. En outre, on désire qu'il disparaisse de lui-même au bout de 5 secondes. (use-modules (xosdguile)) ;; chargement du module (define osd-size 5) ;; on veut 5 lignes (define osd-timeout 5) ;; les messages disparaissent en 5 secondes (define osd (make-xosd osd-size)) ;; création d'un xosd (define osd-position '(80 . 0)) ;; offset horizontal/vertical (xosd-set-timeout! osd osd-timeout) ;; activation du timeout (xosd-set-pos! osd 'bottom) ;; placement en bas (xosd-set-align! osd 'center) ;; et au centre (xosd-set-horizontal-offset! osd (car osd-position)) ;; décalage horizontal (xosd-set-vertical-offset! osd (cdr osd-position)) ;; et vertical (xosd-display-string osd 0 "Hello World !") ;; affichage Voyons une capture d'écran de la bête en fonctionnement. Franchement vous attendiez quoi d'un "Hello World" ? :) {{articles:lisposd.png?400|killer app}} ===== Références ===== [[http://mtpforge.melting-pot.org/projects/lisposd|projet (osd)]]\\ [[http://www.gnu.org/software/guile/guile.html|guile]] © Yann Hodique {{tag>lisp}}