compilation séparée

Une application C peut être constituée de plusieurs fichiers sources (ils constituent en quelques sortes les modules formant l’application).

Ces fichiers comportent des déclarations et définitions d’objets externes ; des déclarations et définitions de fonctions ; des directives du préprocesseur et des définitions de types (mot clé typedef).

Un objet est dit externe (externe à toutes fonctions) si elle n’est pas déclarée dans une fonction ou dans une instruction composée. Sa déclaration et sa définition apparaissent directement au premier niveau dans un fichier source.

Les fonctions en C sont toutes externes puisque l’on ne peut pas avoir de fonctions déclarées ou définies à l’intérieur d’une autre fonction.

Masquage d’information : la notion d’interface et de confidentialité

Il est possible de rendre une fonction ou un objet externe invisible de l’extérieur du fichier dans lequel elles sont définies (confidentiel). Ils ne constituent pas (ils ne font pas partie de) l’interface du fichier source, du module.

Ceci est obtenu en faisant précéder la définition de l’objet ou de la fonction par le mot clé static (attention, ce mot-clé static a un tout autre sens pour une varaiablle déclarée dans une fonction).

static int top-secret (void) ; /* Une fonction confidentielle */

static int compteur = 0 ; /* une définition de vraiable confidentielle */

Par défaut, une fonction et un objet externe ne sont pas confidentielles.

extern int publique (void) ;

constitue une déclaration externe (non confidentielle) comme ceele-ci :

int public (void) ;

On a donc en C une forme rudimentaires de modules avec masquage de l’information. Un fichier est donc composé :

  • De définitions externes (exportées). Il s’agit de définitions d’objets et de fonctions externes visibles de l’extérieur du fichier. Ces fonctions peuvent être appelées de l’extérieur, c’est à dire à partir d’autres fichiers sources. Quant aux objets, ils sont membres du fichier (one verra ce que cela veut dire en terme d’allocation mémoire), mais accessibles également à partir d’autres fichiers. Ces fonctions et objets constituent l’interface du module. S’il est, rappelons le, déconseillé d’utiliser de façon immodérée les variables globales (Les fonctions doivent de façon prioritaire communiquer explicitement via leurs paramètres, il est encore plus déconseillé de faire de la communication inter-fichiers via des variables (communiquer de préférence via des fonctions) ;
  • D’une partie cachée composée de définitions d’objets (variables) et de fonctions confidentielles. Elles sont à usage purement interne au fichier ;
  • De références externes (importées). Il s’agit de déclarations de variables et de fonctions dont les définitions se trouvent dans un autre fichier. On souhaite, à partir d’un fichier, appeler ces fonctions et accéder aux variables (qui appartiennent –définitions externes – à un autre fichier).

Distinguer une définition d’une déclaration externe (référence externe)

Quand un fichier source est compilé, le compilateur ne dispose d’aucune information sur les autres fichiers source, voir les bibliothèques. Les compilations se font de façon complètement indépendante.

C’est le rôle de l’édition de lien de regrouper les fichiers sorce et d’effectuer certaines vérifications.

Il doit vérifier entre autres les règles suivantes :

  • Règle 1 : 1 seule définition doit exister pour une même variable ou une même fonction externe (non confidentielle). Il ne doit pas y avoir deux définitions externes de variables ou de fonctions introduisant le même nom. Il ne doit pas y avoir d’ambigüités concernant la fonction appelée ou la variable accédée.
  • Règle 2 : Quand un fichier comporte une déclaration externe (non confidentielle), mais ne comporte pas de définition correspondante, cette dernière doit obligatoirement se trouver dans un autre fichier.

L’éditeur de liens n’est pas contraint d’effectuer beaucoup plus de vérifications que celle déduites de ces règles.

Attention

Il est possible, par exemple en C que la définition et les déclarations d’une même fonction dans des fichiers différents ne coïncident pas (nombre et type de paramètres), et que l’éditeur ne dise rien. Le même problème existe avec les variables externes inter-fichiers.

Un peu de méthodologie

Nous allons montrer comment utiliser la compilation séparée. Nous verrons l’intérêt de la clause #include du préprocesseur.

L’approche descendante de la programmation

Un exemple de décomposition d'un problème

Un problème est décomposé en sous-problèmes selon le schéma ci-dessous. Ce programme peut être formé d’un seul fichier, ou de plusieurs fichiers en utilisant la compilation séparée.

La version compilation séparée peut correspondre à un travail en équipe. A chaque niveau de l’arbre, les membres de l’équipe s’entendent sur la décomposition du problème à effectuer, et ils se séparent pour réaliser séparément chacun des sous-problèmes.

Version mono-fichier

void en_tete (void) {….}    /* static void en_tete (void) {….} */

void en_ordre (void) {…}

void insere (void) {

en_ordre () ;

en_tete ()

}

void tri (void) {…}

void cherche (void) {

tri () ;

}

void moins (void) {….}

void divise(void) {….}

void opere (void) {

moins () ;

divise () ;

}

main (..) {

opere () ;

cherche () ;

insere() ;

}

Dans la version mono-fichier, tous les fichiers sont déclarés au même niveau (En C, contrairement à Pascal, on ne peut pas déclarer de fonctions dans une fonction), et on ne traduit donc pas la structure en arbre du programme.

Il conviendrait de mettre le mot réservé static devant chaque sous-fonction pour les rendre confidentielles (invisibles en dehors de ce fichier). Si on ne le fait pas on pollue l’espace des noms gérés par l’éditeur de lien. Cette pollution pourrait s’avérer gênante en cas d’extension futures du programme (la fonction cherche étant par omission une fonction externe, on ne peut pas déclarer une autre fonction externe de même nom).

Version compilation séparée

A chaque définition de fonction correspond un fichier. Dans chaque fichier, on doit placer les déclarations (importations) des fonctions utilisées. La structure en arbre n’est pas non plus traduite. L’espace des noms géré par l’éditeur de liens est là irrémédiablement pollué. On ne peut pas avoir deux fonctions externes de même nom (En Ada, ce phénomène n’intervient pas. opere serait une sous-unité de compilation de la fonction main, et non une unité de compilation à part entière. Le nom opere garderait sont caractère local à main, bien que correspondant à une sous-unité compilée séparément. Le nom véritable de opere en Ada serait main.opere).

Fichier le prog.c

void en_ordre (void) ;

void cherche (void) ;

void opere (void) ;

main (…) {

opere () ;

cherche () ;

insere () ;

}

Fichier operation.c

void moins (void) ;

void divise(void) ;

void opere (void) {

moins () ;

divise () ;

}

Fichier soustrait.c

void moins (void) {….}

Fichier divise.c ;

void divise(void) {….}

Fichier cherche.c

void tri (void) ;

void cherche (void) {

tri () ;

}

Fichier tri.c

void tri (void) {…}

Fichier insertion.c

void en_tete (void) ;

void en_ordre (void) ;

void insere (void){

en_ordre () ;

en_tete ()

}

Fichier ordre.c

void en_ordre (void){…}

Fichier debut.c

void en_tete (void) {…}

Du fait qu’en C ANSI, on doit déclarer dans un fichier les fonctions que l’on utilise, il n’y a aucun ordre de compilation entre les fichiers du programme. Les fichiers sont compilables de façon complètement indépendante. Ceci est même vrai si les différentes fonctions » partagent » des déclarations communes de type (types utilisés pour déclarer les paramètres), car l’équivalence de type (voir chapitre 4) se fait pas par nom, mais par structure. Deux déclarations de type (par typedef) se trouvant dans deux fichiers distincts sont équivalents, s’ils ont la même structure.

Si on veut éviter d’avoir à écrire plusieurs fois, les mêmes déclarations, et éviter de ce fait les erreurs d’incohérences, il faut écrire des fichiers include et utiliser la directive #include du préprocesseur. Un fichier include n’est pas compilé. Le texte qu’il contient est inséré dans les fichiers source qui contiennent une directive #include à ce fichier, avant que la compilation véritable de ces fichiers source débute.

Version avec fichier .h et utilisation de #include

Fichier le prog.h

void en_ordre (void) ;

void cherche (void) ;

void opere (void) ;

Fichier le prog.c

#include leprog.h

main (…) {

opere () ;

cherche () ;

insere() ;

}

Fichier operation.h

void moins (void) ;

void divise(void) ;

Fichier operation.c

#include operation.h

void opere (void) {

moins () ;

divise () ;

}

Fichier soustrait.c

#include operation.h

void moins (void) {….}

Fichier divise.c ;

#include operation.h

void divise(void) {….}

Fichier cherche.h

#include leprog.h

#include leprog.h

void tri (void) ;

Fichier cherche.c

#include cherche.h

void cherche (void) {

tri () ;

}

Fichier tri.c

#include cherche.h

void tri (void) {…}

Fichier insertion..h

#include leprog.h

void en_tete (void) ;

void en_ordre (void) ;

Fichier insertion.c

#include insertion.h

void insere (void){

en_ordre () ;

en_tete () ;

}

Fichier ordre.c

#include insertion.h

void en_ordre (void){…}

Fichier debut.c

#include insertion.h

void en_tete (void) {…}

La compilation séparée en C est mal adaptée à l’approche descendante de la programmation, puisque les noms des sous-unités polluent l’éditeur de lien. Il est vrai que c’est surtout pour l’approche ascendante que la compilation séparée est intéressante (« programming in the large »).

La programmation ascendante ou par interface

Lors de l’analyse d’un problème, on peut s’apercevoir que l’on a besoin d’un composant logiciel (module) n’existant pas déjà (une bibliothèque mathématique spéciale, une structure de données, un couche de protocole réseau, …).

Le premier travail est de spécifier ce module, en particulier les services qu’il rend. A la fin de ce travail, on doit disposer d’un fichier de déclarations constituant l’interface du module.

Ce fichier interface est un fichier include (de suffixe .h) comportant :

Pour pouvoir utiliser ce composant que l’’on espère le plus réutilisable possible, l’application cliente devra insérer dans les fichiers source une directive #include composant.h. Il pourra alors déclarer des objets des types déclarés dans composant.h, il pourra appeler les fonctions fournies par l’interface.

Le fournisseur de service placera la réalisation des services dans le fichier source realisation.c qui devra comporter :

Ce fichier pourra comporter de plus toutes les réalisations et définitions nécessaires pour la réalisation, mais non utiles et donc cachées aux utilisateurs du composant (types, variables et fonctions confidentielles, clause #include pour utiliser d’autres modules).

Cette architecture logicielle permet aux clients et au réalisateur du module de travailler séparément. Les compilations de client1.C et et de réalisation peuvent se faire de façon indépendante.

Il est à noter cependant un défaut de C. Les noms de l’interface polluent l’éditeur de lien et empêchent d’utiliser ailleurs des fonctions de même nom. Il suffirait pour pallier ce défaut que le module ait un vrai statut dans le langage C (paquetage en Ada, paquetages et classes en Java, …). Le nom d’un objet ou d’une fonction de l’interface serait alors précédé par le nom du module.

» Glossaire du langage C