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 libxosd dans un programme écrit (ou extensible) en 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 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);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 conditionn'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_<name>_module si la bibliothèque s'appelle lib<name>.soet 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;
}

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” ? :)

Killer app

Références

© Yann Hodique