====== 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}}