Avant de commencer ce TP, téléchargez l'archive TP3.tgz et extrayez la.
Travail du préprocesseur
Préprocesseur
Pour commencer cet exercice, placez vous dans le répertoire 1-preprocesseur.
Compilez le fichier hello.c avec la commande gcc -c hello.c. Vous devriez obtenir des erreurs de compilation.
On souhaite maintenant que N ait la même valeur que M si M est défini. Si M n'est pas défini, N conserve sa valeur.
Modifiez le programme condition.c et testez votre implémentation
Étudiez le programme tab.c, puis essayez de générer le fichier objet tab.o.
Quelle est la cause de l'erreur ?
À l'aide de directives #ifndef et #define, implémentez une solution à ce problème :
- Ajoutez les lignes suivantes au début du fichier types.h:
#ifndef _TYPES_H #define _TYPES_H
- Mettez la ligne suivante à la fin de votre fichier types.h:
#endif /* _TYPES_H */
- Reproduisez ces deux opérations pour le fichier tab.h en utilisant _TAB_H
La solution consiste à ajouter des "#include guards" évitant les inclusions multiples de fichiers .h.
Il s'agit d'une construction qu'habituellement, on ajoute à tous les fichiers .h.
Compilation de modules
Portée des variables
Pour cet exercice, placez vous dans le répertoire 3-portee_variables.
Dans le fichier foo.c, écrivez la fonction int main(int argc, char**argv). Complétez cette fonction pour qu'elle déclare une variable entière locale et une variable entière locale et statique. Lors de leur déclaration, initialisez ces variables (à 0 par exemple).
Complétez la fonction pour qu'elle affiche la somme des deux variables.
Générez, à partir de foo.c, l'objet foo.o. Affichez la liste des symboles de l'objet. Que remarquez vous ?
- la variable locale (var_locale) n'apparaît pas.
- la variable statique locale (var_locale_statique) apparaît dans la section des données non initialisées (b) et est locale à l'objet (car b est en minuscule).
- Une variable globale nommée var_globale
- Une variable globale statique nommée var_globale_static
- Une variable globale externe nommée var_globale_extern
- var_globale est stockée dans la section des données non initialisées et est globale (B majuscule)
- var_globale_static est également stockée dans la section des données non initialisées, mais est locale (b minuscule)
- var_globale_extern est "undefined"
Créez le fichier bar.c, et déclarez-y une variable globale de type int nommée var_globale_extern.
Bibliothèques
On souhaite créer la bibliothèque libtab.so à partir de tab.c (situé dans le répertoire 4-bibliotheque/tab/).
Commencez par générer tab.o avec la commande gcc -c tab.c. Utilisez ensuite le fichier objet créé pour générer libtab.so. Que signifie le message d'erreur que vous obtenez ?
Il faut ajouter l'option -ltab qui indique à l'éditeur de liens que la bibliothèque libtab.so (ou libtab.a) est nécessaire.
Il est également nécessaire d'ajouter l'option -L../tab qui indique à l'éditeur de liens où chercher les bibliothèques.
Modifiez maintenant la variable d'environnement LD_LIBRARY_PATH afin de pouvoir exécuter le programme. Vous pouvez également vérifier avec ldd que libtab.so est bien trouvée.
Rappelez vous vos cours de CSC3102 (Introduction aux systèmes d'exploitation), et exportez la variable LD_LIBRARY_PATH pour que votre modification soit prise en compte par les programmes que vous lancez:NB: le sujet de cet exercice est prévu pour un système Linux. Si vous êtes utilisateur d'un Mac (personne n'est parfait), modifiez la variable d'environnement DYLD_LIBRARY_PATH, qui est l'équivalent de LD_LIBRARY_PATH sous Mac (attention, les répertoires sont séparés par , et non par :).
Makefile
Pour cet exercice, placez vous dans le répertoire 5-makefile.
On souhaite, dans un premier temps, automatiser la compilation du programme main.c situé dans le répertoire src/.
Écrivez le fichier src/Makefile pour qu'il permette de générer l'exécutable main à partir de main.c et foo.c.
Afin de contrôler facilement les options passées au compilateur lors de la création des fichiers objets, créez une variable CFLAGS et initialisez la à -Wall -Werror.
Adaptez votre Makefile pour qu'il utilise cette variable.
Dans src, modifiez main.c pour qu'il utilise la fonction void saisir_tab(int tab[], int size) définie dans libtab/tab.c.
Modifiez ensuite le Makefile pour permettre de générer l'exécutable main.
La solution de compilation actuelle a pour inconvénient que si l'on modifie tab.c (ou tab.h), il est nécessaire de lancer la commande make dans libtab, puis dans src.
Modifiez le Makefile afin de corriger ce problème. Vous pouvez pour cela utilise make -C <directory> qui lance récursivement la commande make dans le répertoire directory.
Balade
Warning et #include
Avant de commencer ces exercices, créez-vous un répertoire balade dans lequel vous travaillerez.
Compilation avec warning, mais le programme fonctionne
Créez un fichier main.c avec le code suivant :
cc -Wall -o executable main.c # On fait exprès de ne pas mettre -Werror pour permettre la génération d'un exécutable
Ce code compile avec warning, mais ne cause pas d'erreur à l'exécution.
Morale de cette question : un code peut compiler avec warning et pourtant fonctionner.
Compilation avec warning, mais le programme ne fonctionne pas
Dans main.c, remplacez "Hello World !" par 42. Puis, compilez et exécutez : même warning à la compilation, sauf qu'on a une erreur de segmentation à l'exécution.
Morale de cette question : un code peut compiler avec le même warning que précédemment et pourtant ne plus fonctionner.
Correction du warning
Corrigeons le warning pour voir si ça n'aiderait pas à comprendre l'erreur à l'exécution. man puts permet de voir la déclaration de puts que nous ajoutons au début du fichier main.c
À la compilation, nous obtenons un warning plus précis : il permet de comprendre que nous avons un souci de type au niveau de puts. Nous avons toujours une erreur à l'exécution (car nous n'écoutons pas le warning !)
Correction du warning en faisant un cast
Pour corriger le warning, castez l'entier 42 en char * de sorte que puts a effectivement une donnée de type char * en paramètre.
Le warning est corrigé, mais erreur à l'exécution !
Morale de cette question : faire un cast est une opération toujours délicate. Réfléchissez avant de le faire et surtout vérifiez que vous comprenez ce que vous faites.
Correction du warning de la bonne manière
Au lieu de caster 42 en char *, remplacez l'entier 42 par la chaîne de caractères "42".
Le programme fonctionne !
Morale de cette question : les warnings, c'est mal. Ne laissez aucun warning dans votre code, sous aucun prétexte. C'est d'ailleurs pour ça que nous vous demandons d'ajouter systématiquement l'option de compilation -Werror pour que vous soyez sûr de ne pas générer un exécutable en cas de warning.
Intérêt des #include
La déclaration int puts(const char *s); améliore la qualité du code développé jusqu'à maintenant, c'est-à-dire qu'elle lui rajoute des informations pour que le travail de développement se fasse avec plus de garde-fous (ce qui est bien). Mais, en même temps, elle "pollue" notre code en le compliquant (ce qui est mal). En effet, elle rajoute une ligne qui ne sert pas au fonctionnement de notre code.
Vous trouvez que cette histoire de "pollution" est exagérée ? Hé bien, imaginez que votre code ait besoin d'utiliser également putchar (cf. man puts). Vous auriez alors besoin de rajouter la ligne int putchar(int c); pour ajouter un garde-fou à votre code.
Pour alléger votre code et le rendre plus lisible, remplacez int puts(const char *s); par #include <stdio.h>. Ainsi, au moment de la compilation, le compilateur (plus précisément le préprocesseur) remplace ce #include par le contenu du fichier stdio.h qui contient la déclaration int puts(const char *s);
Vérifiez que ça marche en compilant et en exécutant votre code.
Ensuite, pour voir ce remplacement en action, tapez la commande : cc -Wall -o resultatPreprocessing -E main.c
La compilation s'arrête à la phase de préprocessing dont le résultat (output) est stocké dans le fichier resultatPreprocessing qui est un source C. Faire more resultatPreporcessing, puis tapez /puts pour retrouver la déclaration de puts (notez le mot-clé extern dont nous parlerons plus tard dans cet exercice). Appuyez plusieurs fois sur la barre d'espace pour afficher la fin de votre fichier.
Conclusion : l'utilisation de #include rend le code plus lisible, tout en ajoutant des garde–fous.
Digressions autour du préprocesseur
Compilation conditionnelle
Revenons au message "Hello world!" que nous affichions initialement. Comment faire pour que notre programme affiche ce message soit en anglais, soit en français ? Une solution possible est d'utiliser la compilation conditionnelle pour générer un code avec messages en anglais ou un code avec messages en français.
Modifiez l'instruction puts de la manière suivante :
Puis, compilez (sans l'option -E). Enfin, exécutez pour vérifier que l'affichage est en anglais.
Compilez maintenant avec cc -Wall -o executable -DFRANCAIS main.c et exécutez pour vérifier que l'affichage est en français.
Arrêtez-vous à l'étape du préprocessing avec -E (en pensant à mettre -o resultatPreprocessing et non -o executable) et faites tail resultatPreprocessing. Voyez comment toutes les lignes commençant par # ont été remplacées par des lignes vides.
Morale de cette question : la compilation conditionnelle permet d'obtenir des exécutables différents à partir d'un même source. Cette technique est très utilisée pour assurer la portabilité d'un code entre différentes plateformes matérielles. Notez que pour la traduction de messages, les développeurs préfèrent d'autres techniques qui sortent du contexte de cette balade.
Stockage de sections de code dans la commande de compilation
Le code précédent n'est pas très lisible avec son #ifdef. Voici une piste d'amélioration :
- modifiez le code en écrivant simplement puts(MESSAGE);
- compilez avec la commande cc -Wall -o executable -DMESSAGE='"Bonjour le monde !"' main.c (notez les quotes et les double quotes).
Compilez sans -E pour vérifier que ce code affiche le résultat attendu, puis avec -E pour voir le fruit du travail du préprocesseur.
Modularité
Positionnons le problème
Lors de nos digressions autour du préprocesseur, nous avons généré plusieurs exécutables : un pour avoir des messages en anglais, et l'autre pour avoir des messages en français. Voyons comment avoir un seul et même exécutable capable d'afficher de l'anglais ou du français.
Déclarez une variable message :
et écrivons puts(message[maisQueMettreIci]);
Au niveau de maisQueMettreIci, nous pourrions coder un #define. Mais alors, nous obtiendrions à nouveau 2 exécutables.
Sur votre shell, tapez la commande env | grep LANG
Votre ordinateur devrait afficher au moins la ligne
Dit autrement, la variable d'environnement LANG vous permet de savoir en quelle langue affiche votre ordinateur. Exploitons cela en écrivant :
et puts(message[quelleEstMaLangue()]);
et enfin les 2 #include (stdlib.h et string.h) pour que tout cela compile sans warning.
Intérêt de modulariser un code
Avec la question précédente, le code de main.c est complexifié par la présence du code pour le multi-langue. Aussi, nous allons mettre ce code à part.
Créez un fichier message.c
main.c ne contient alors plus que :
La ligne de compilation devient cc -Wall -o executable main.c message.c
Elle génère 1 warning, mais l'exécutable donne le résultat attendu.
Déclaration d'extern dans un module utilisant du code d'un autre module
Puisque nous avons vu précédemment que les warnings c'est le mal, supprimons le warning de la question précédente.
Ajoutez au début de main.c la ligne extern int quelleEstMaLangue();
NB : il n'y a rien à faire sur le fichier message.c.
Vérifiez qu'il n'y a plus de warning à la compilation (et que l'exécution est correcte).
Possibilité d'avoir des bugs si les extern de variable ou de code n'apparaissent pas dans le module qui les définit
Nous sommes maintenant en présence d'une erreur potentielle difficile à détecter : imaginons que la personne en charge de coder la fonction quelleEstMaLangue() tape, dans message.c, float quelleEstMaLangue() { au lieu de int quelleEstMaLangue() {
cc -Wall -o executable main.c message.c : il n'y a aucun problème à la compilation.
Mais, à l'exécution, le message est en anglais au lieu d'être en français ! En effet, la valeur 1 est renvoyée par quelleEstMaLangue en tant que float dans message.c, mais cette valeur est interprétée comme l'entier de valeur 0 au niveau de main.c
Création d'un fichier d'interfaces pour repérer ces problèmes dès la compilation
Pour éviter ce problème d'incohérence entre déclarations et implémentations, nous ajoutons à message.c les lignes :
Ainsi, à la compilation de message.c, nous obtenons un warning : nous sommes en mesure de détecter qu'il faut remplacer float quelleEstMaLangue() { par int quelleEstMaLangue() {. Corrigez votre code.
Pour ne pas avoir ces déclarations extern répétées (et donc source d'incohérences et donc d'erreurs potentielles), nous les centralisons dans message.h :
Vérifiez que la compilation se passe bien et que l'exécution est correcte.
Pour être propre au niveau de la structure des fichiers, nous mettons message.h dans un répertoire include qui est un sous-répertoire du répertoire courant.
La commande de compilation cc -Wall -o executable main.c message.c donne lieu à une erreur : il faut ajouter l'option -Iinclude pour indiquer au compilateur que des fichiers d'include se trouvent dans le répertoire include.cc -Wall -o executable main.c message.c -Iinclude compile correctement.
Compilation séparée
Chronométrez la durée d'exécution de votre compilation en tapant la commande :
L'elapsed est la durée de compilation.
Sur une machine de salle TP, ce temps est de 0.06 secondes. Il faut donc approximativement 0.03 secondes par fichier (nous verrons à la question suivante que cette approximation est inexacte, car elle ne distingue pas compilation et édition de lien).
0.03 secondes de compilation pour un fichier d'en moyenne 10 lignes. Sur un projet industriel, il est courant d'avoir 1000 fichiers d'environ 1000 lignes. Si nous devions compiler un tel projet, le temps de compilation serait de 0.03*(1000 lignes/10 lignes pour 0.03 secondes) * 1000 fichiers = 3000 secondes. Il est donc souhaitable de trouver une solution pour que notre compilation dure moins longtemps !
C'est pourquoi nous allons compiler nos fichiers séparément. Puis, nous mettrons le résultat de ces compilations ensemble.
cc -Wall -c -Iinclude main.c # Notez que pas d'option -o
cc -Wall -c -Iinclude message.c # Notez que pas d'option -o
cc main.o message.o -o executable # Notez que ni option -Wall, ni option -Iinclude
Les 2 premières commandes sont des commandes de compilation (elle convertissent chaque fichier source en fichier object). La dernière commande réalise l'édition de liens (elle regroupe les objets pour constituer un exécutable).
Pour recompiler juste ce qu'il faut, il est bienvenu de disposer d'un outil qui, en cas de changement, ne compile que ce qu'il y a besoin de compiler. C'est le rôle de make !
Besoin de bibliothèques
Mesurons le temps d'édition de liens : /usr/bin/time cc main.o message.o -o executable
Sur une machine de salle TP, l'édition de lien prend 0.02 secondes pour 2 fichiers. Pour 1000 fichiers de 1000 lignes, une approximation de ce temps est 0.01*(1000/10)*1000 fichiers = 1000 secondes.
C'est pourquoi les informaticiens ont créé la notion de bibliothèque pour réduire le temps d'édition de lien. En fait, vous utilisez des bibliothèques depuis que vous compilez des programmes C. Pour vous en rendre compte :
qui fait apparaître la libc (à travers la chaîne de caractères -lc).
Pour aller plus loin: CMake
Pour cet exercice, créez le répertoire 6-cmake et déplacez vous y.
Pour cet exercice, nous allons étudier un projet "de la vraie vie", mais qui reste petit. Il s'agit de LiTL (Lightweight Trace Library), un outil permettant d'enregistrer des traces de manière efficace.
Commencez par récupérer le code source du projet, puis affichez la liste des fichiers à la racine du projet
Etude des fichiers CMake
Parmi les fichiers, on remarque un fichier CMakeLists.txt.
Commencez par ouvrir le fichier CMakeLists.txt, qui est utilisé par cmake pour configurer le projet. Le fichier décrit le projet (avec la commande project), liste les fonctionnalités optionnelles (commandes option), détecte les dépendances (CHECK_LIBRARY_EXISTS), et génère un fichier pour pkg-config (avec configure_file). Enfin, le fichier liste les sous-répertoires qui contiennent le code du projet (add_subdirectory).
Ouvrez maintenant le fichier src/CMakeLists.txt. Ce fichier décrit comment compiler la bibliothèque liblitl:
- configure_file transforme le header litl_config.h.in en litl_config.h. Pour cela, il substitue les macros @TRUC@ par leurs valeurs telles que définies par cmake;
- add_library liste les fichiers nécessaires pour créer la bibliothèque litl
- install liste les objets qui doivent être installés lors de la phase d'installation.
Compilation du projet
Créez le sous-répertoire build et déplacez vous y. Lancez ensuite la commande:
Cette commande demande à cmake de charger le fichier ../CMakeLists.txt et de l'exécuter. L'option CMAKE_INSTALL_PREFIX définit le répertoire où le projet doit être installé (par défaut, le projet est installé dans /usr/local, ce qui nécessite d'être administrateur de la machine).
Observez les fichiers qui ont été générés dans le répertoire courant (et ses sous répertoires
Compilez maintenant le projet avec la commande make, puis installez le avec make install. Observez les fichiers qui ont été installés dans le répertoire $PWD/../install.
Configuration du projet
Vous pouvez passer des options à cmake avec -DVARIABLE=VALEUR
Vous pouvez également configurer le projet avec une interface graphique en lançant: ccmake .. (vous aurez peut-être besoin d'installer le paquet cmake-curses-gui)
Contribution à un projet
Dans le projet LiTL, certaines fonctionnalités sont manquantes. Par exemple:
- La documentation (dans le sous-répertoire doc) n'est pas générée. Ajoutez une option GENERATE_DOCUMENTATION qui, lorsqu'elle est activée vérifie si les dépendances (doxygen, pdflatex, latexmk) sont installées
- Les tests (dans le sous-répertoire tests) ne sont pas compilé ni exécutés. Complétez le projet pour permettre l'exécution des tests avec make test
Si vous implémentez l'une où l'autre de ces fonctionnalités manquantes, n'hésitez pas à soumettre une merge requests pour contribuer au projet !