Avec la notion de préprocesseur, la compilation modulaire est un mécanisme fondamental du langage C. Le but de la compilation modulaire et de concevoir un programme comme l’assemblage de plusieurs «modules».
Chaque module est le fruit de la compilation séparée d’un fichier source (fichier .c) différent. Les modules sont autant de fichiers objets (fichiers .o) différents. Pour produire un fichiers objets à partir d’un fichier source il faut utiliser l’option -c du compilateur gcc.
L’assemblage des différents modules est appelé linkage, liaison, ou encore édition de liens. Lors de cette opération de liaison les différents fichiers objets sont réunis dans un seul fichier exécutable. Pour que cette opération aboutisse il faut cependant qu’un et un seul des fichiers liés contienne la fonction main() qui sera lepont d’entrée de l’exécution du programme produit.
.
Vous avez réalisé un fichier numbers.c qui compile toute une série de fonctions d’affichage des entiers sous leurs formes décimale, hexadécimale ou binaire. Cet ensemble de fonctions est utile dans de nombreux programmes. Selon les canons de la compilation modulaire il est donc pertinent de compiler une fois cet ensemble de fonctions dans un fichier .o puis de réutiliser ces fonctions dans tous les programmes que vous produirez, sans les recompiler.
Vous allez travailler dans le répertoire module/ de votre dépôt.
Produisez un module put_numbers.o qui contiendra l’ensemble des fonctions que vous avez réalisées. Comment réaliser cette ligne de compilation ?
Ce fichier ne doit pas contenir les fonctions testées, putdec(), puthex()...). Produisez un module test_numbers.o à partir de votre programme. Comment compiler ce programme ? La compilation provoque des warning. Pourquoi ? Que faut-il ajouter pour éviter ces warning de compilation ?
Proposez une solution générique qui repose sur l’usage du préprocesseur et des fichiers de prototypes vus précédemment.
Finalement, votre programme test_numbers est composé de 3 fichiers sources et 3 fichiers contenant du code machine :
La compilation modulaire nous a permis (i) de structurer notre programme en différents fichiers servant des objectifs différents et (i) de produire une librairie de code put_numbers.o que l’on pourra réutiliser dans d’autres programmes grace au fichier put_numbers.h (en liant le .o avec les programmes qui l’utilise).
Cependant lorsque vous modifiez le fichier put_numbers.c la recompilation de l’exécutable test_numbers devient fastidieuse...
Ce script effectue systématiquement toutes les phases de compilation pour tous les fichiers. L’un des intérêts de la compilation modulaire est justement de pouvoir ne recompiler que ce qui est nécessaire. Voyons comment améliorer cette première version du script.
Dans un script bash, il est possible de tester si un fichier a été modifié avant un autre fichier, en utilisant l’opérateur -nt, newer than :
if [ "file1" -nt "file2" ]; then echo "file1 has been changed after file2" else echo "file1 has been changed before file2" fi
Une fois votre script mis au point, assurez-vous d’avoir tester ces différents cas de figures de recompilation.
L’utilisation de fichiers de script permet de produire efficacement un programme (ou un ensemble programmes) en ne recompilant que les choses nécessaires. La mise au point de tels scripts est néanmoins délicate. De plus lorsque le projet évolue, il faut modifier le script en conséquence en intégrant les bonnes lignes de compilations aux bons endroits et en faisant évoluer les conditions de compilation.
Finalement, ce qu’exprime ce script de compilation, c’est que le fichier test_numbers «dépend» des fichiers test_numbers.o et put_numbers.o. Si l’un de ces deux fichiers a changé depuis la dernière production de test_numbers, il faut simplement exécuter la commande gcc put_numbers.o test_numbers.o -o test_numbers.
Dans un fichier Makefile on exprime cela par une règle comme suit :
test_numbers: test_numbers.o put_numbers.o gcc test_numbers.o put_numbers.o -o test_numbers
Le fichier Makefile peut contenir un série de règles. Chaque règle débute par une ligne de dépendances qui pour une cible donnée (test_numbers), liste ses prérequis (test_numbers.o put_numbers.o). Cette ligne de dépendances est suivie de lignes de commandes qui seront exécutée si un des prérequis est plus récent que la cible. Ces lignes de commandes shell doivent être précédées d’une tabulation en début de ligne.
Pour déclencher la recompilation conditionnelle d’un fichier, il suffit ensuite de lancer la commande make. Cette commande va essayer de reconstruire, si nécessaire, la cible de la première règle trouvée dans le fichier Makefile. Si les prérequis de cette règle sont eux-mêmes l’objet d’autres règles de compilation, ils seront préalableent reconstruits si nécessaire, et ce récursivement.
Ainsi un fichier Makefile permet d’exprimer beaucoup plus simplement qu’un fichier de script ce qui doit être fait pour recompiler un programme.
Réalisez un fichier Makefile qui réalise la même tâche que votre fichier de script compile_test_numbers.sh.
Testez votre Makefile de la même manière que vous aviez testé votre script de compilation.
Notez que dans ce Makefile, contrairement au script shell, l’ordre dans lequel les règles sont données n’a pas d’importance. Tout au plus, vous pouvez notez que lorsque vous lancez la commande make sans paramètre, c’est la première règle trouvée dans le fichier qui est considérée.
L’usage (ou une bonne pratique) consiste à s’assurer que la première règle, la règle évaluée par défaut, assure la compilation de l’ensemble du projet. Pour assurer cela, on place généralement, en première règle du Makefile règle blanche (sans commande associée) qui s’écrit :
all: ...
Les ... sont remplacés par la liste des règles qui doivent être évaluées lors du lancement du make. Donc votre cas, make doit produire une chose : test_numbers.
Il est aussi d’usage qu’un Makefile propose une règle clean qui supprime tous les fichiers compilés (.o et exécutables). Cette règle ne dépendant de rien, et elle consiste en l’exécution d’une commande rm avec les paramètres appropriés.
Enfin, une bonne pratique consiste à définir un certains nombre de variables au début du Makefile qui pourront être changées, au besoin et qui définissent :
Il est ensuite possible d’utiliser ces variables dans le Makefile lors de la description des commandes shell à lancer en écrivant $(VAR) la ou elles doivent être utilisées.
De la même façon il est possible de faire référence au fichier cible
de la règle courante avec $@
, à l’ensemble des fichiers
sources de la règle courante avec $<
et à l’ensemble des
fichiers sources avec $^
.
Quel est le sens de cette règle ?$(BLD_DIR)/test_numbers: $(BLD_DIR)/test_numbers.o $(BLD_DIR)/put_numbers.o $(CC) $^ -o $@
Quelle sera la commande exécutée si cette règle est considérée ?
Modifiez votre Makefile pour qu’il utilise cette règle. Ré-écrivez les autres règles sur le même principe. Proposez aussi une règle par défaut all et une règle de nettoyage clean.
Dans un répertoire de projet, il est d’usage de placer les fichiers sources dans un répertoire src/ les fichiers compilés dans un répertoire build/, et de proposé à la racine un fichier Makefile et un fichier README.md.