Rolinh

Rolinh' release

Les Pointeurs Sur Fonction en C

Cela fait longtemps que je n’avais pas publié d’article sur mon blog. J’ai toutefois décidé de me remettre à l’écriture par le biais d’une brève explication sur les pointeurs sur fonction en C.

L’idée d’en parler m’est venue car, dans le cadre d’un projet en entreprise, j’ai été amené à mettre en place un protocole permettant de contrôler une application à distance via le réseau. Pour situer le contexte: l’application contrôlée est écrite en C, embarqué oblige, et elle tourne sur une carte comprenant un processeur ARM et un DSP. Pour les besoins du protocole, un daemon écoute sur une socket dédiée et reçoit de la part de clients des commandes sous la forme NOM_DE_LA COMMANDE [OPTIONS].

Le protocole en question doit pouvoir évoluer afin de prendre en charge des nouvelles commandes. Par conséquent, la contrainte que je me suis fixée est qu’il faut que l’ajout d’une nouvelle commande au protocole implique un minimum de changements au niveau du code. C’est afin de répondre à ce problème que j’ai eu recours aux pointeurs sur fonction.

Pour l’illustration, je vais donc donner un exemple, volontairement simplifié, en m’imaginant que je dois pouvoir traiter les commandes du protocole et appeler des fonctions internes à mon programme à cette fin.

En m’inspirant du design pattern “chaîne de responsabilité”, je vais écrire une fonction prenant en argument une commande du protocole avec ses options et qui se se charge d’appeler une fonction de traitement spécifique à ladite commande.

Avant de pouvoir faire cela, il est nécessaire de définir une structure contenant le nom d’une commande du protocole ainsi que sa fonction de traitement associée:

1
2
3
4
struct proto_cmd {
    const char *name;
    int (*process) (char *);
};

Si le premier élément de ma structure est un type très courant, les lecteurs non familier avec les pointeurs sur fonction sont certainement intrigués par le deuxième. Pour ceux-là, pas de panique, l’explication arrive.

Un protocole n’étant, en principe, pas constitué que d’une seule commande, il est également nécessaire d’avoir un moyen de stocker la liste des commandes. Cela peut se faire de plusieurs façons comme en utilisant le principe d’une liste chainée ou encore d’une table de hashage (bon, dans ce cas là le protocole devrait comporter un grand nombre de commandes pour que ça se justifie). Pour rester simple, je vais simplement stocker la liste des commandes dans un tableau:

1
2
3
4
5
struct proto_cmd cmd_table[NBCMD] = {
    {"quit", process_quit},
    {"start", process_start},
    {"stop", process_stop}
};

NBCMD correspond évidemment au nombre de commandes du protocole (seulement trois pour l’exemple ici).

A partir du moment où je possède une table avec la liste des commandes du protocole, il est facile d’écrire la fonction dont je vous parlais tout à l’heure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
select_cmd(char *cmd, char *args)
{
  int i;

  for (i = 0; i < NBCMD; i++)
      if (strcmp(cmd, cmd_table[i].name) == 0)
          return cmd_table[i].process(args);

    /*
     * Si on arrive la, alors cela veut dire que la commande ne fait pas partie
     * du protocole...
     */
     (void)fprintf(stderr, "Unknown command: %s\n", cmd);
     return 1;
}

La ligne 9 est celle qui revêt ici le plus d’intérêt. C’est en effet là que se fait l’appel à la fonction associée à la commande, ceci grâce au pointeur sur fonction. Il est à noter que le traitement des commandes se fait dans l’ordre des éléments du tableau. Si le protocole comporte de nombreuses commandes, il faudrait voir pour envisager une solution plus efficace.

Bien évidemment, il reste une chose à écrire: toutes les fonctions de traitement associées aux commandes. Dans ma table je les ai nommées process_quit, process_start et process_stop.

1
2
3
4
5
6
int
process_quit(char *args)
{
    /* on fait notre traitement de la commande "quit" ... */
    return 0;
}

Et ainsi de suite pour les autres commandes. Par conséquent, si l’on souhaite ajouter une commande au protocole, il suffit d’ajouter la commande en question dans la table et d’implémenter sa fonction process. Et c’est tout! Aucun autre changement n’est donc nécessaire dans le code. Sympa non?

Pour la dernière explication, je vais revenir sur la structure proto_cmd. On peut maintenant se rendre compte que, pour le deuxième élément, int correspond au type de retour de la fonction process, que (*process) correspond au pointeur sur la fonction et que (char *) s’associe à l’argument de la fonction process.

J’espère que cette brève explication illustrée était clair. Si ce n’est pas le cas, n’hésitez pas à me contacter par e-mail afin que je puisse modifier cet article en conséquence.