Concevoir et réaliser un composant logiciel en C

Emmanuel Delahaye

Cet article présente 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.

Votre avis et vos suggestions sur cet article nous intéressent !Alors après votre lecture, n'hésitez pas : Commentez ♪

- Titre : Concevoir et réaliser un composant logiciel en C

- Auteur : Emmanuel Delahaye

- Parution : 27 janvier 2008

- Mise à jour : 7 mars 2013

- Licence : Copyright ® 2008 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Stratégie de développement d'un composant logiciel

- II-A. Modélisation d'un composant logiciel

- II-B. Implémentation du modèle.

- III. Étude de cas : Un gestionnaire d'alarme à niveaux

- III-A. Spécifications

- III-B. Conception

- III-C. Réalisation en C

- III-C-1. Nommage du composant logiciel

- III-C-2. Interface

- III-C-3. Implémentation

I - 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.

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

Les qualités requises pour un composant logiciel sont :

- une spécification claire :

- comportement,

- performances,

- interface (entrées, sorties),

- gestion des erreurs ;

- une grande fiabilité :

- respect des normes du langage,

- respect des normes de codage,

- vérification du domaine des paramètres,

- élimination des pratiques douteuses,

- testabilité (indépendance vis-à-vis des sorties),

- jeu de tests de non-régression ;

- une facilité d'intégration :

- portabilité,

- respect des normes de nommage,

- élimination des pratiques empêchant la réentrance,

- élimination des pratiques empêchant l'instanciation,

- simplicité des points de sorties.

II-A - 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.). Ce sont les sorties.

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

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

II-B - 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és 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   |
--------------

III - Étude de cas : Un gestionnaire d'alarme à niveaux

III-A - 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 lorsqu’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.

III-B - Conception

À 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) |
|  Réglage   |               | ------------------>
|  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 n’est pas en alarme, déclencher 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 données de configuration :

- le seuil de déclenchement,

- le seuil de retour,

- l'adresse de la fonction de sortie ;

- les données d'état :

- l'état courant de l'alarme.

Les fonctions nécessaires à l'exploitation sont :

- les fonctions de configuration :

- enregistrement de la fonction de sortie 'alarme',

- réglage du seuil de déclenchement,

- réglage du seuil de retour ;

- les fonctions d'évaluation :

- évaluation de la mesure courante.

III-C - Réalisation en C

III-C-1 - Nommage du composant logiciel

Le composant logiciel est nommé 'alarme'. Le préfixe est donc 'alarme_'

III-C-2 - 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()
---------------------------------------------------------------------
Réglage du seuil de déclenchement
---------------------------------------------------------------------
I : contexte
I : valeur du seuil
O : retour : 0 = OK 1 = erreur
--------------------------------------------------------------------- */
int alarme_trigger_level (alarme_s *this, int level);

/* ---------------------------------------------------------------------
alarme_return_level()
---------------------------------------------------------------------
Réglage 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()
---------------------------------------------------------------------
Évaluation de la mesure. Détection des réactions éventuelles.
---------------------------------------------------------------------
I : contexte
I : valeur à évaluer
O : retour : 0 = OK 1 = erreur
--------------------------------------------------------------------- */
int alarme_eval (alarme_s *this, int value);

III-C-3 - 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. Ça é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()
---------------------------------------------------------------------
Réglage du seuil de déclenchement
---------------------------------------------------------------------
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()
---------------------------------------------------------------------
Réglage 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()
---------------------------------------------------------------------
Évaluation de la mesure. Détection des réactions éventuelles.
Implémentation 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 à évaluer
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 enregistrée (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.cavec 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.