SWIG et les fonctions avec codes d'erreur et strerror

SWIG et les fonctions avec codes d'erreur et strerror

Table des Matières

SWIG est un générateur de code de glue (wrapper) capable de connecter des bibliothèques compilées à un tas de langages de script. Le processus est principalement automatique, mais pour gérer certains cas particuliers, il faut aider le générateur à faire les choses correctement. Dans ma bibliothèque, toutes les fonctions renvoient un entier, qui est un code d’erreur. Une fonction spéciale, suivant le même comportement que strerror_r, peut être utilisée pour récupérer la signification d’un code d’erreur particulier. C’est un mécanisme assez courant pour le code C. Mais ce n’est pas ainsi que fonctionnent les langages de script. Dans leur monde, les fonctions sont plutôt censées lever des exceptions.

Si vous êtes familier avec SWIG, vous pouvez préférer passer directement à la dernière section.

Bibliothèque d’exemple

Imaginons une petite bibliothèque simple pour servir d’exemple aux explications ci-dessous. Elle a des fonctions init et do_something. Les deux renvoient un code entier spécial qui indique si l’appel a réussi et sinon, quelle erreur s’est produite. Une fonction de type strerror est disponible pour récupérer une description textuelle d’un code d’erreur donné. De cette façon, une application utilisant notre bibliothèque peut afficher à son utilisateur final une meilleure explication de ce qui a mal tourné.

/**
 * @brief Initialise la bibliothèque
 *
 * @return 0 en cas de succès, ou MYLIB_GUEULEDEBOIS_ERROR si la bibliothèque ne
 *         peut pas démarrer correctement (pour une raison inconnue...).
 */
int mylib_init( void );

/**
 * @brief Faire quelque chose
 *
 * @return 0 en cas de succès, MYLIB_VENDREDIAPREM_ERROR si la bibliothèque a
 *         une bonne excuse de ne pas le faire.
 */
int mylib_do_something( void );

/**
 * @brief Récupérer la description d'un code d'erreur
 *
 * @param[in] errorno Le code d'erreur à vérifier
 * @param[out] buf Un tampon alloué où la description sera copiée
 * @param[in] buflen La taille maximale du tampon
 * @return Toujours 0 (succès)
 */
int mylib_strerror_r(int errorno, char *buf, size_t buflen);

#define MYLIB_SUCCESS              (0)
#define MYLIB_GUEULEDEBOIS_ERROR   (-1)
#define MYLIB_VENDREDIAPREM_ERROR  (-2)

Wrapping de base avec SWIG

Dans notre situation, un wrapping direct de la bibliothèque ne fonctionnera pas. En effet, comme mylib_strerror_r prend des arguments en sortie, il faut guider SWIG pour les utiliser correctement. Cela se fait en injectant du code de glue à différentes positions lorsque SWIG tombe sur le motif d’arguments spécifié.

/* Nom de la bibliothèque python résultante */
%module mylib

/* Nous avons besoin de ces bibliothèques SWIG pour wrapper strerror_r */
%include exception.i
%include typemaps.i

%{
#define SWIG_FILE_WITH_INIT
#include "mylib.h"
%}

/*
 * Changer la section "in" lorsque le motif "char *buf, size_t buflen" est
 * trouvé. Cette section est utilisée pour préparer les arguments d'entrée à la
 * fonction wrappée.
 * Indiquer que pour ce motif, aucune entrée n'est attendue de la part du
 * script. Ensuite, allouer un tampon pour recevoir la sortie.
 * Si le tampon ne peut pas être alloué, lever une exception côté script.
 */
%typemap(in, numinputs=0) (char *buf, size_t buflen) {
   $1 = (char*) malloc(255*sizeof(char));
   $2 = 255;
   if (!$1) {
      SWIG_exception_fail(SWIG_MemoryError, "Erreur d'allocation mémoire");
   }
}

/*
 * Changer la section "argout" lorsque le motif "char *buf, size_t buflen" est
 * trouvé. Cette section est utilisée pour préparer la sortie qui sera retournée
 * au script.
 * Le tampon que nous avons créé devrait maintenant être rempli avec les données
 * de sortie.
 * On utilise le fragment SWIG_FromCharPtr pour le convertir en type de données
 * 'string' habituel que le script comprend.
 */
%typemap(argout,fragment="SWIG_FromCharPtr") (char *buf, size_t buflen) {
   %append_output(SWIG_FromCharPtr($1));
}

/*
 * La section "free" est utilisée à la fin de la glue pour libérer les variables
 *  allouées de manière personnalisée. C'est ici que l'on libère le tampon.
 */
%typemap(freearg) (char *buf, size_t buflen) {
   free($1);
}

/*
 * Se débarrasser des préfixes de fonction, car le langage de script utilisera
 * l'espace de nom du module.
 */
%rename("%(strip:[mylib_])s") "";

%include "mylib.h"

Remarque : Nous pourrions utiliser %new_array et %delete_array pour gérer le tampon, ce qui rendrait le code compatible avec C++. Mais ces macros ne gèrent pas les échecs d’allocation (en tous cas avec la version 3.0.8).

>>> import mylib
>>> mylib.init()
-1
>>> mylib.strerror_r(-1)
[0, 'Erreur de gueule de bois']
>>> mylib.MYLIB_GUEULEDEBOIS_ERROR
-1

Si nous étions paresseux, la bibliothèque pourrait être utilisée telle quelle. Comme en C, il suffirait de comparer la valeur retournée et de faire des actions la valeur indique une erreur.

ret = mylib.init()
if ret != mylib.MYLIB_SUCCESS:
    raise RuntimeError(mylib.strerror_r(ret)[1])

Mais ce n’est pas comme ça qu’un module est censé fonctionner dans le monde des langages de script.

Lever des exceptions

Ce que nous voulons faire, c’est en fait transformer la valeur de retour. Nous voulons que SWIG vérifie la valeur de retour et, si elle est 0, ne retourne rien. Sinon, il doit lever une exception. Si possible, nous voulons un code qui supporte autant de langages que possible. Pas seulement Python, par exemple, donc pas de PyErr_SetString.

/* Nom de la bibliothèque python résultante */
%module mylib

/* Nous avons besoin de ces bibliothèques SWIG pour wrapper strerror_r */
%include exception.i
%include typemaps.i

%{
#define SWIG_FILE_WITH_INIT
#include "mylib.h"
%}

/*
 * Transformer la valeur de retour lorsqu'elle est un "int". Si la valeur
 * retournée n'est pas 0, appeler strerror_r. Lever une exception d'exécution
 * avec le message récupéré. En cas de succès, ne rien retourner à la place du
 * code d'erreur.
 */
%typemap(out) int {
   if (result < 0) {
      char* errstr = (char*) malloc(255*sizeof(char));
      if (!errstr) {
         /* Adopter un comportement similaire à glibc en cas d'échec
          * d'allocation mémoire lors de la gestion d'erreur. */
         SWIG_exception_fail(SWIG_RuntimeError, "Erreur inconnue");
      }
      /* Cette fonction retourne toujours SUCCESS */
      (void) mylib_strerror_r(result, errstr, 255);
      SWIG_exception(SWIG_RuntimeError, errstr);
      free(errstr);
      SWIG_fail;
   }
#if defined(VOID_Object)
   $result = VOID_Object;
#endif
}

/*
 * Changer la section "in" lorsque le motif "char *buf, size_t buflen" est
 * trouvé. Cette section est utilisée pour préparer les arguments d'entrée à la
 * fonction wrappée.
 * Indiquer que pour ce motif, aucune entrée n'est attendue de la part du
 * script. Ensuite, allouer un tampon pour recevoir la sortie.
 * Si le tampon ne peut pas être alloué, lever une exception côté script.
 */
%typemap(in, numinputs=0) (char *buf, size_t buflen) {
   $1 = (char*) malloc(255*sizeof(char));
   $2 = 255;
   if (!$1) {
      SWIG_exception_fail(SWIG_MemoryError, "Erreur d'allocation mémoire");
   }
}

/*
 * Changer la section "argout" lorsque le motif "char *buf, size_t buflen" est
 * trouvé. Cette section est utilisée pour préparer la sortie qui sera retournée
 * au script.
 * Le tampon que nous avons créé devrait maintenant être rempli avec les données
 * de sortie.
 * On utilise le fragment SWIG_FromCharPtr pour le convertir en type de données
 * 'string' habituel que le script comprend.
 */
%typemap(argout,fragment="SWIG_FromCharPtr") (char *buf, size_t buflen) {
   %append_output(SWIG_FromCharPtr($1));
}

/*
 * La section "free" est utilisée à la fin de la glue pour libérer les variables
 *  allouées de manière personnalisée. C'est ici que l'on libère le tampon.
 */
%typemap(freearg) (char *buf, size_t buflen) {
   free($1);
}

/*
 * Se débarrasser des préfixes de fonction, car le langage de script utilisera
 * l'espace de nom du module.
 */
%rename("%(strip:[mylib_])s") "";

%include "mylib.h"

Maintenant, on a une bibliothèque bien plus intégrée à un environnement de script.

>>> import mylib
>>> mylib.init()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: Erreur de gueule de bois
>>> mylib.init()
>>> mylib.do_something()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: Erreur de vendredi aprèm
>>> mylib.do_something()
>>> mylib.strerror_r(-1)
'Erreur de gueule de bois'

Articles Similaires

“Salut, l’ami ! Comment ça va ? Tu te souviens de ce petit projet que j’avais sur le CubieTruck? Je m’y suis remis. Mais c’est dommage, j’ai perdu mon mot de passe root… Je vais devoir tout recommencer. Tu te souviendrais du mot de passe idiot qu’on avait choisi ensemble par hasard ?”

Lire la suite

La semaine dernière, je préparais un rapport d’analyse de données avec Jupyter, Pandas et Matplotlib (pour ne citer que quelques briques de ce fabuleux framework). Une des figures contenait deux sous-graphiques : le second étant un agrandissement d’une région du premier. Pour bien illustrer cela et, au passage, montrer au fanclub MATLAB à quel point ils sont restés “so 90s”, j’ai décidé de tracer une flèche entre les deux sous-graphiques.

Lire la suite

Utiliser GNOME Keyring sur XFCE sous Manjaro n’est pas compliqué, mais la documentation de ArchLinux n’est pas adaptée et peut vous laisser vous gratter la tête pendant des heures. Allez, ne perdez plus de cheveux, je vous aide.

Lire la suite