2007-07-08

Un système de "logging" en C

Je travail sur une librairie depuis quelques mois et je désirais y intégrer un système de "logging". Le but est donc de pouvoir suivre ce que fait cette librairie lors de son utilisation. J'ai découvert les fonctions vprintf, etc,.. qui se comportent exactement comme les fonctions printf, mais qui ont le mérite de prendre une va_list comme argument. Du coup, c'est extrêmement pratique pour un système de "logging".

Deux en-têtes sont donc indispensables. Les vprintf appartiennent au stdio.h tout comme les printf, et le va_list est disponible depuis le stdarg.h. Cet article sous-entend donc que vous connaissez déjà ces deux en-têtes. Et donc que vous savez que le stdarg à le mérite de permettre d'écrire des fonctions à paramètres optionnels.

Les messages de log
Nous voulons par exemple générer un message dans le stderr à l'aide de notre fonction de log. Mais dans ce message on aimerait pouvoir passer des valeurs de variables. Comme par exemple:

my_log("variable x=%i, variable y=%s", my_int, my_string);

Il est donc nécessaire de considérer que plusieurs variables peuvent être entrées tout comme pour une fonction printf. Donc notre fonction my_log() doit forcément accepter un nombre optionnel de paramètres.

Exemple:

static void my_log(const char *msg, ...) {
..// (...)
}


Maintenant essayez d'imaginer l'implémentation de cette fonction si vous utilisiez printf ou fprintf. Il serait nécessaire de récupérer chaque paramètres optionnels puis de les interpréter afin de les afficher l'un après l'autre tout en ayant analysé le *msg pour déterminer leurs emplacements. Bref, c'est beaucoup de travail pour pas grand chose. C'est là qu'interviennent les fonctions vprintf.

Quand vous utilisez les arguments optionnels, vous déclarez forcément une variable va_list. Et la fonction va_start() permet de peupler la variable va_list avec les paramètres. Il se trouve que vprintf réclame une variable de type va_list.

Reprenons alors notre exemple en le complétant:

static void my_log(const char *msg, ...) {
..va_list va;

..va_start(va, msg);
..if (msg) {
....vfprintf(stderr, msg, va);
....fprintf(stderr, "\n");
..}
..va_end(va);
}


vfprintf va donc nous éviter un code monstrueux en exploitant directement la liste de paramètres. Ainsi notre fonction de "logging" (minimaliste) est terminée!

2007-06-29

Pseudo-polymorphisme en C

Mais kesako? Je ne suis pas là pour vous expliquer ce qu'est le polymorphisme. Tout programmeur en langage à objets (ou orienté objets) y a touché de près ou de loin. Mais tout l'intérêt de mon message réside dans le fait de faire du polymorphisme, mais sans objet!
L'intérêt? Pourquoi pas? Tout ce qui est bon est à prendre. Il peut arriver d'en dans certains cas d'en avoir besoin et de regretter de ne pas travailler en langage à objets. Alors je vais vous montrer une implémentation très simple du polymorphisme en langage C.


Le langage C
Le langage C est tout sauf objet (c'est un langage impératif), c'est bien pour cela que le C++ a été inventé. Ainsi l'implémentation que je vais vous montrer atteint très vite ses limites. Mais dans des cas particuliers cela peut s'avérer très utile. En objet pour réaliser du polymorphisme il nous serait nécessaire de créer des classes et donc des objets. Voici les correspondances que l'on va faire :

classe > structure
objet > pointeur sur une variable déclarée comme étant une structure
méthode > référence dans la structure à une fonction

Cas en exemple
Comme exemple nous allons prendre un cas où il y a deux pseudo-objets, et chacun de ces pseudo-objet à une pseudo-méthode print(). Nous voulons appeler celle-ci depuis un même tableau de ces pseudo-objets. Commençons par définir nos pseudo-classes.

typedef struct myh_s {
..void (*print) (void);
..int x;
} myh_t;

typedef struct myb_s {
..void (*print) (void);
..double x;
} myb_t;

/* Generic pseudo-class */
typedef struct gen_s {
..void (*print) (void);
} gen_t;

Notre exemple comportera deux pseudo-objets. Une pseudo-classe générique est nécessaire afin de pouvoir déclarer un tableau pour contenir nos pseudo-objets (une sorte de pseudo-héritage). Attention, il faut veiller à définir les pseudo-méthodes dans le même ordre partout! Il faut les définir au début de chaque structure. Les variables double et int sont là uniquement pour l'exemple.

Maintenant que nous avons les structures, il faut définir les fonctions qui feront office de pseudo-méthode.

void print_hello(void) {
..printf("Hello Linuxfr users!\n");
}

void print_bye(void) {
..printf("Bye Linuxfr users!\n");
}


Avant de s'attaquer à la fonction main(), il est important de créer encore deux fonctions pour déclarer nos pseudo-objets en mémoire. Pour cela nous allons faire de l'allocation de mémoire dynamique avec malloc(). Sans oublier d'assigner les références des fonctions à la pseudo-méthode de chaque pseudo-objet.

myh_t *new_h(void) {
..myh_t *my;

..my = malloc(sizeof(myh_t));
..my->print = print_hello;

..return my;
}

myb_t *new_b(void) {
..myb_t *my;

..my = malloc(sizeof(myb_t));
..my->print = print_bye;

..return my;
}


Bien sûr, il faudrait tester les variables afin de s'assurer que l'allocation à pu s'effectuer avec succès. Néanmoins pour cet exemple ce n'est pas fatal. A vous de faire les choses le plus correctement possible dans votre implémentation.
A l'aide de ce travail, notre main() se présente un peu comme un langage à objets et le pseudo-polymorphisme peut agir dans la boucle for.

int main(void) {
..int i;
..myh_t *my_hello;
..myb_t *my_bye;
..gen_t *my[2];

../* Create pseudo-objects */
..my_hello = new_h();
..my_bye = new_b();

..my[0] = (gen_t *)my_hello;
..my[1] = (gen_t *)my_bye;

../* Use pseudo-objects */
..for (i = 0; i < 2; i++)
....my[i]->print();

../* Delete pseudo-objects */
..free(my_hello);
..free(my_bye);

..return 0;
}


La sortie du programme correspond donc à:

Hello Linuxfr users!
Bye Linuxfr users!

Je vous conseil néanmoins de ne pas abuser de ce genre "d'astuce", car il est bien clair que le C n'est pas un langage fait pour l'objet.