Concevoir et réaliser un composant logiciel en C

Dernière mise à jour : 25/04/2009 22:32:54

Un forum web "Bien programmer en C" permet de poser des questions sur les articles, le C en général, la programmation etc.

Home

Introduction

Lors du développement d'un projet logiciel, il est courant d'écrire des portions de codes qui pourraient être utiles dans d'autres projets. Pour ce faire, il faut définir une stratégie de développement de modules de logiciels dont le comportement est clairement spécifié, les performances et les limites connues, et qui soient facilement intégrables.

Ce type de module logiciel est appelé 'composant logiciel', par analogie avec les composants utilisés dans l'industrie de l'électronique, par exemple.


Stratégie de développement d'un composant logiciel

Les qualités requises pour un composant logiciel sont :

Modélisation d'un composant logiciel

L'expérience montre que la plupart des composants logiciels servent à effectuer un traitement sur des données.


              ---------------
  Entrées    |               | Résultat
  ---------> |  Traitements  | ----------->
             |               |
              ---------------

Certains évènements extérieurs peuvent agir sur les données (entrées). Il est bien sûr possible de fournir un résultat immédiat par valeur de retour ou par un paramètre de sortie, mais certains traitements peuvent aussi donner lieu à des réactions en fonction des résultats et/ou de l'état courant (seuil, erreur etc.). Se sont les sorties.

On peut donc modéliser un composant logiciel de la façon suivante :


    ----------------------------------------
   |          Composant logiciel            |
   |----------------------------------------|
   |                                        |
   |             ---------------            |
   |    Entrées |               | Sorties   |
  ------------> |  Traitements  | ----------->
   |   Résultat |               |           |
  <------------ |               |           |
   |             ---------------            |
   |                                        |
    ----------------------------------------

Implémentation du modèle.

Les entrées sont implémentées par de simples appels de fonctions appelées 'points d'entrées'. Vu de l'extérieur, il s'agit d'une interface (API). Chaque interface de fonction est décrite par son nom, ses paramètres (nom et type) et son type de retour.

Les traitements sont le corps des fonctions. Certains traitements sont des actions directes sur les données, et sont retournées immédiatement, d'autres nécessitent de la mémoire permanente pour gérer des configurations, états, compteurs, files etc. Il peut aussi y avoir des fonctions de lecture qui permettent d'accéder à certaines données internes. Enfin, selon des critères bien définis, le traitement peut générer des appels à des fonctions de sorties (évènements).

Les fonctions de sorties sont extérieures au composant. Leur appel se fait donc via une variable contenant l'adresse de la fonction. Par contre, la spécification de l'interface des fonctions de sorties appartient à la définition du composant. Il est probable que dans le cadre de l'intégration du composant dans un projet, les fonctions de sorties doivent être développées projet par projet. C'est une opération simple qui nécessite de fournir l'interface spécifiée, et l'appel à la fonction extérieure (qui peut très bien être un point d'entrée d'un autre composant logiciel, voire du même), parfois au prix d'un transcodage de valeur.

La modélisation détaillée d'un composant logiciel et de son environnement est donc la suivante :


    --------------------------------------------------------------------
   |                      Application                                   |
   |--------------------------------------------------------------------|
   |         ----------                   ------------------            |
   |        | Appelant |                 |                  |           |
   |         ----------                  |                  v           |
   |            |^                       |               --------       |
   |            ||                       |              | Sortie |      |
   |            ||                       |               --------       |
    ------------||-----------------------|------------------------------
                v|                       |
           --------------------------    |
    ------|         Entrées          |---|------
   |C L |  --------------------------    |      |
   |o o |        |^                      |      |
   |m g |        ||                      |      |
   |p i |        v|                      |      |
   |o c |  ----------------              |      |
   |s i | |                |         ---------  |
   |a e | |  Traitements   | -----> | Sorties | |
   |n l | |                |         ---------  |
   |t   | |                |                    |
   |    |  ----------------                     |
   |    |         |^                            |
    --------------||----------------------------
                  ||
                  v|
              --------------
             | Bibliothèque |
             |   standard   |
              --------------


Etude de cas : Un gestionnaire d'alarme à niveaux

Spécifications

On dispose d'un système qui fait des mesures de grandeurs physiques. On souhaite ajouter un mécanisme qui déclenche une alarme lorsque un certain seuil est dépassé, et qui supprime l'alarme lorsqu'on repasse en dessous d'un autre seuil.

La gamme de mesure acceptable couvre les valeurs de -32767 à 32767. Les seuils sont programmables sur toute la gamme de mesure. Le seuil de retour doit être inférieur au seuil de déclenchement.

Conception

A partir du modèle de composant logiciel standard, on ajoute les spécificités du projet :


       ----------------------------------------------
      |         Composant logiciel 'Alarme'          |
      |----------------------------------------------|
      |                                              |
      |             ---------------                  |
      |    Entrées |  Traitements  | Sorties         |
      |            |               |                 |
      |            |               | Alarme (ON/OFF) |
      |  Reglage   |               | ------------------>
      |  seuils    |               |                 |
  ---------------> |               |                 |
  <--------------- |               |                 |
      |            |               |                 |
      |  Mesure    |               |                 |
  ---------------> |               |                 |
  <--------------- |               |                 |
      |             ---------------                  |
      |                                              |
       ----------------------------------------------

Le traitement peut être décrit par un algorithme textuel :

Si la mesure est supérieure ou égale au seuil de déclenchement,
et que l'on est pas en alarme, declencher l'alarme.

Si l'alarme est déclenchée et que la mesure est inférieure au seuil
de retour, arréter l'alarme.

Ce que l'on peut traduire en langage algorithmique de la façon suivante :

IF NOT in_alarme
   IF mesure >= seuil_declenchement
      in_alarme := TRUE
      sortie_alarme (ON)
   ENDIF
ELSE
   IF mesure < seuil_retour
      in_alarme := FALSE
      sortie_alarme (OFF)
   ENDIF
ENDIF

Les données permanentes sont donc

Les fonctions nécessaires à l'exploitation sont :

Réalisation en C

Nommage du composant logiciel

Le composant logiciel est nommé 'alarme'. Le prefixe est donc 'alarme_'

Interface

Les données sont rassemblées dans la structure suivante.

typedef struct
{
   int seuil_declenchement;
   int seuil_retour;
   unsigned active:1;
}
alarme_s;

Les seuils sont de type int, qui couvre la plage requise par la spécification. Le champ 'active', qui contient l'état courant de l'alarme, peut prendre les valeurs 0 et 1, d'où l'utilisation d'un champ de bits de 1 bit.

Il manque le champ qui contient l'adresse de la fonction de sortie. Il est très pratique d'utiliser un typedef pour spécifier le type de cette fonction, surtout qu'à ce stade de la réalisation, on n'a pas une vision très claire de cette fonction. Le type risque donc d'évoluer, d'où l'intérêt du typedef qui concentre la définition du type en un seul endroit. Dans un premier temps, je propose :

/* ---------------------------------------------------------------------
   alarme_out_f
   ---------------------------------------------------------------------
   type de la fonction de sortie.
   ---------------------------------------------------------------------
   I: on : 0 = OFF (alarme inactive) 1 = ON (alarme active)
   O: pas de retour
   --------------------------------------------------------------------- */
typedef void alarme_out_f (int on);

La structure peut donc s'écrire

/* donnees internes */
typedef struct
{
   /* configuration */
   int seuil_declenchement;
   int seuil_retour;
   alarme_out_f *pf_out;

   /* etat */
   unsigned active:1;
}
alarme_s;

Il reste à définir le prototype des fonctions d'exploitations. Il est d'usage d'implémenter un système de gestion d'erreur minimum, qui consiste à retourner 0 quand tout va bien, et 1 en cas de problèmes.

/* ---------------------------------------------------------------------
   alarme_install_out()
   ---------------------------------------------------------------------
   Enregistrement de la fonction de sortie 'alarme'
   ---------------------------------------------------------------------
   I: contexte
   I: adresse de la fonction
   O: retour : 0 = OK 1 = erreur
   --------------------------------------------------------------------- */
int alarme_install_out (alarme_s *this, alarme_f *pf);

/* ---------------------------------------------------------------------
   alarme_trigger_level()
   ---------------------------------------------------------------------
   Reglage du seuil de declenchement
   ---------------------------------------------------------------------
   I: contexte
   I: valeur du seuil
   O: retour : 0 = OK 1 = erreur
   --------------------------------------------------------------------- */
int alarme_trigger_level (alarme_s *this, int level);

/* ---------------------------------------------------------------------
   alarme_return_level()
   ---------------------------------------------------------------------
   Reglage du seuil de retour
   ---------------------------------------------------------------------
   I: contexte
   I: valeur du seuil
   O: retour : 0 = OK 1 = erreur
   --------------------------------------------------------------------- */
int alarme_return_level (alarme_s *this, int level);

/* ---------------------------------------------------------------------
   alarme_eval()
   ---------------------------------------------------------------------
   Evaluation de la mesure. Detection des reactions eventuelles.
   ---------------------------------------------------------------------
   I: contexte
   I: valeur a evaluer
   O: retour : 0 = OK 1 = erreur
   --------------------------------------------------------------------- */
int alarme_eval (alarme_s *this, int value);

Implémentation

Maintenant que l'interface des fonctions est spécifiée, on peut écrire l'implémentation des fonctions publiques (points d'entrée). Il suffit de recopier l'interface et d'écrire le corps des fonctions. Il est d'usage de sécuriser les fonctions en effectuant des tests de validité (valeur, domaine) sur les paramètres. D'autre part, afin d'éviter des erreurs à venir, il est fortement recommandé de définir les paramètres 'non-modifiables' à l'aide du qualificateur 'const'. Ca évite de perdre une valeur qui aurait pu être précieuse.

/* ---------------------------------------------------------------------
   alarme_install_out()
   ---------------------------------------------------------------------
   Enregistrement de la fonction de sortie 'alarme'
   ---------------------------------------------------------------------
   I: contexte
   I: adresse de la fonction
   O: retour : 0 = OK 1 = erreur
   --------------------------------------------------------------------- */
int alarme_install_out (alarme_s *const this, alarme_f *const pf)
{
   /* par defaut, pas d'erreur */
   int err = 0;

   /* pas question de continuer si le contexte est NULL */
   if (this != NULL)
   {
      /* enregistrer l'adresse de la fonction de sortie */
      this->pf = pf;
   }
   else
   {
      err = 1;
   }

   return err;
}

/* ---------------------------------------------------------------------
   alarme_trigger_level()
   ---------------------------------------------------------------------
   Reglage du seuil de declenchement
   ---------------------------------------------------------------------
   I: contexte
   I: valeur du seuil
   O: retour : 0 = OK 1 = erreur
   --------------------------------------------------------------------- */
int alarme_trigger_level (alarme_s *const this, int const level)
{
   int err = 0;

   if (this != NULL)
   {
      if (level >= ALARME_VALUE_MIN
       && level <= ALARME_VALUE_MAX)
      {
         this->seuil_declenchement = level;
      }
      else
      {
         err = 1;
      }
   }
   else
   {
      err = 1;
   }

   return err;
}

/* ---------------------------------------------------------------------
   alarme_return_level()
   ---------------------------------------------------------------------
   Reglage du seuil de retour
   ---------------------------------------------------------------------
   I: contexte
   I: valeur du seuil
   O: retour : 0 = OK 1 = erreur
   --------------------------------------------------------------------- */
int alarme_return_level (alarme_s *const this, int const level)
{
   int err = 0;

   if (this != NULL)
   {
      if (level >= ALARME_VALUE_MIN
       && level <= ALARME_VALUE_MAX)
      {
         this->seuil_retour = level;
      }
      else
      {
         err = 1;
      }
   }
   else
   {
      err = 1;
   }

   return err;
}

/* ---------------------------------------------------------------------
   alarme_eval()
   ---------------------------------------------------------------------
   Evaluation de la mesure. Detection des reactions eventuelles.
   Implementation de l'algorithme :

   IF NOT in_alarme
      IF mesure >= seuil_declenchement
         in_alarme := TRUE
         sortie_alarme (ON)
      ENDIF
   ELSE
      IF mesure < seuil_retour
         in_alarme := FALSE
         sortie_alarme (OFF)
      ENDIF
   ENDIF

   ---------------------------------------------------------------------
   I: contexte
   I: valeur a evaluer
   O: retour : 0 = OK 1 = erreur
   --------------------------------------------------------------------- */
int alarme_eval (alarme_s *const this, int const value)
{
   int err = 0;

   if (this != NULL)
   {
      if (valuel >= ALARME_VALUE_MIN
       && value <= ALARME_VALUE_MAX)
      {
         if (!this->active)
         {
            if (value > this->seuil_declenchement)
            {
               this->active = 1;

               out (this, 1);
            }
         }
         else
         {
            if (value < this->seuil_retour)
            {
               this->active = 0;

               out (this, 0);
            }
         }
      }
      else
      {
         err = 1;
      }
   }
   else
   {
      err = 1;
   }

   return err;
}

Pour terminer l'implémentation, il reste à définir les constantes ALARME_VALUE_MIN et ALARME_VALUE_MAX ...

#define ALARME_VALUE_MIN -32767
#define ALARME_VALUE_MAX 32767

... et la fonction interne (privée) out().

/* ---------------------------------------------------------------------
   out()
   ---------------------------------------------------------------------
   appel de la fonction de sortie enregistree (si disponible)
   ---------------------------------------------------------------------
   I: contexte
   I: etat de l'alarme
   O: pas de retour
   --------------------------------------------------------------------- */
static void out (alarme_s *this, int on)
{
   if (this->pf_out != NULL)
   {
       this->pf_out (on);
   }

   /* si le pointeur est NULL, on peut appeler un 'stub()'
    * qui simule la sortie (utile en debug)
    */
}

L'ensemble du composant logiciel 'alarme' est stocké ici (Ver 1.0) : alarme.h alarme.c avec le fichier de test rudimentaire (rien à voir avec un véritable test unitaire) test.c

Nous disposons d'une sorte de 'maquette' qui permet de faire quelques tests, de vérifier l'algorithme etc.

Tant qu'il n'a pas passé une séance de validation unitaire intense, ce n'est certainement pas un produit fini.


Valid XHTML 1.0! Valid CSS!  Use OpenOffice.org Club d'entraide des développeurs francophones Code::Blocks
© Emmanuel Delahaye 2004-2009 | emmanuel dot delahaye at gmail dot com | Home | Forum | Livre d'or