Toujours dans le cadre d'un de mes TP de programmation en C, je travail sur la réalisation d'un modèle de skip-list et dans l'élaboration des mes différents fichiers, j'ai pas mal de soucis quant à la dépendance des fichiers .h ainsi que leur compilation.

N'ayant pas encore tout compris sur ce système de dépendance qui me parait particulier encore selon certains cas, j'ai pensé que mon problème ne venait pas seulement de la déclaration des différents #include dans mes fichiers .c ou .h mais de ma conception de mon Makefile et particulièrement l'ordre des dépendances déclaré.

C'est pour cela que je vais tenter d'effectuer une recherche sur les dépendances d'inclusions et de compilation dans ce billet.
Vous êtes de la partie ?

1. Test d'un programme basique

Pour commencer, rien de mieux qu'un petit « Hello world! » standard. Hop, lancez un Terminal et déterminez un répertoire de projet pour notre article :

~/Prog% mkdir rand0  
~/Prog% cd rand0 
~/Prog/rand0% vim main.c Makefile

Code source

Code source du fichier main.c :

#include <stdio.h>
#include <stdlib.h>

void says_hello()  
{
    printf("Hello world!\n");
}

int main ()  
{
    says_hello();
    return EXIT_SUCCESS;
}

Voici rien de bien compliqué, juste une petite fonction qui va afficher un simple « Hello world! » dans la console.
Passons au Makefile.

Makefile

Code source du fichier Makefile :

# Définition du compilateur à utiliser
CC     ?= gcc

# Définition des paramètres de compilation
FLAGS    ?= -W -Wall -ansi -pedantic -ggdb

# Tout notre programme se résumera à rand0 (nom de notre binaire final)
all: rand0

# rand0 dépend de main.o et de hello.o
rand0: main.o  
    $(CC) -o rand0 main.o

# main.o dépend de main.c (c'est sa propre compilation)
main.o: main.c  
    $(CC) -o main.o -c main.c $(FLAGS)

# Commande pour effacer les fichiers compilés
clean:  
    rm -rf *.o *~ rand0

Compilons notre programme et exécutons-le :

~/Prog/rand0% make      
cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb  
cc -o rand0 main.o  
~/Prog/rand0% ./rand0 
Hello world!  
~/Prog/rand0% 

Pas d'erreur ni de warning théoriquement. On constate tout de même que la compilation de main.o est précédée de celle de rand0. Cela paraît logique puisque rand0 a besoin de main.o afin d'être compilé (c'est plus de l'assemblage à ce niveau-là).

Pour vérifier cela, nous allons maintenant procéder à la décomposition de notre programme.

2. Décomposition de notre programme

Pour faire ceci, nous allons transférer tout ce qui concerne notre « Hello world! » dans un jeu de fichier hello.c et hello.h et seulement se contenter d'appeler says_hello() dans main.c.
Aller, hop, c'est parti :

~/Prog/rand0% vim hello.h hello.c

Code source

Code source du fichier hello.h :

#ifndef _HELLO
#define _HELLO

void says_hello();

#endif

Code source du fichier hello.c :

#include <stdio.h>
#include <stdlib.h>
#include "hello.h"

void says_hello()  
{
    printf("Hello world!\n");
}

Code source du fichier main.c :

#include <stdio.h>
#include <stdlib.h>
#include "hello.h"

int main ()  
{
    says_hello();
    return EXIT_SUCCESS;
}

A ce stade, si nous compilons, le compilateur nous envoie une erreur car il ne parvient pas à trouver la fonction says_hello() :

~/Prog/rand0% make
cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb  
cc -o rand0 main.o  
Undefined symbols:  
  "_says_hello", referenced from:
      _main in main.o
ld: symbol(s) not found  
collect2: ld returned 1 exit status  
make: *** [rand0] Error 1  
zsh: exit 2     make  
~/Prog/rand0% 

Pour palier à ce souci de compilation, nous allons ajouter la dépendance dans notre Makefile.

Makefile

Code source du fichier Makefile :

# Définition du compilateur à utiliser
CC     ?= gcc

# Définition des paramètres de compilation
FLAGS    ?= -W -Wall -ansi -pedantic -ggdb

# Tout notre programme se résumera à rand0 (nom de notre binaire final)
all: rand0

# rand0 dépend de main.o et de hello.o
rand0: main.o hello.o  
    $(CC) -o rand0 main.o hello.o

# main.o dépend de main.c (c'est sa propre compilation)
main.o: main.c  
    $(CC) -o main.o -c main.c $(FLAGS)

# hello.o dépend de hello.c (c'est sa propre compilation)
hello.o: hello.c  
    $(CC) -o hello.o -c hello.c $(FLAGS)

# Commande pour effacer les fichiers compilés
clean:  
    rm -rf *.o *~ rand0

On compile et on exécute :

~/Prog/rand0% make      
cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb  
cc -o hello.o -c hello.c -W -Wall -ansi -pedantic -ggdb  
cc -o rand0 main.o hello.o  
~/Prog/rand0% ./rand0      
Hello world!  
~/Prog/rand0% 

On constate que main.o est compilé avant hello.o mais que cela n'affecte pas la compilation globale de notre programme. Seulement, lorsque je compile, j'aime garder les notions de dépendances le plus faible en début de compilation. Et puis cela ne paraît pas logique.
On verra comment inverser l'ordre plus bas...

3. Tests sur les dépendances d'inclusion

Avant de bidouiller avec nos fichiers .h, nous allons les supprimer de nos en-têtes de fichiers .c ainsi :

Code source

Code source du fichier main.c :

#include <stdio.h>
#include <stdlib.h>

int main ()  
{
    says_hello();
    return EXIT_SUCCESS;
}

Code source du fichier hello.c :

#include <stdio.h>
#include <stdlib.h>

void says_hello()  
{
    printf("Hello world!\n");
}

On nettoie, on compile et on exécute :

~/Prog/rand0% make clean
rm -rf *.o *~ rand0  
~/Prog/rand0% make
cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb  
main.c: In function ‘main’:  
main.c:6: warning: implicit declaration of function ‘says_hello’  
cc -o hello.o -c hello.c -W -Wall -ansi -pedantic -ggdb  
cc -o rand0 main.o hello.o  
~/Prog/rand0% ./rand0 
Hello world!  
~/Prog/rand0%

Nous constatons que le compilateur n'a pas besoin du fichier header .h pour compiler, assembler et exécuter le programme. Un simple « warning » est levé donc rien de bloquant.
Étudions un cas bloquant maintenant...

Ce qui va être intéressant, c'est de savoir à quel endroit notre fichier hello.h aura-t'il besoin d'être inclus. Pour cela, nous allons ajouter la définition d'un type Hello qui correspondra à une structure et nous allons l'initialiser et l'utiliser sans notre fonction says_hello() :

Code source

Code source du fichier hello.h :

#ifndef _HELLO
#define _HELLO

typedef struct Hello Hello;  
struct Hello  
{
    int foo;
};

void says_hello();

#endif

Code source du fichier hello.c :

#include <stdio.h>
#include <stdlib.h>

void says_hello()  
{
    Hello entier;
    entier.foo = 3;
    printf("Hello world!\nHere is my number: %d\n", entier.foo);
}

Vous avez déjà deviné ce qui allait se passer... Faisons tout de même le test : compilons :

~/Prog/rand0% make clean
rm -rf *.o *~ rand0  
~/Prog/rand0% make
cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb  
main.c: In function ‘main’:  
main.c:6: warning: implicit declaration of function ‘says_hello’  
cc -o hello.o -c hello.c -W -Wall -ansi -pedantic -ggdb  
hello.c: In function ‘says_hello’:  
hello.c:6: error: ‘Hello’ undeclared (first use in this function)  
hello.c:6: error: (Each undeclared identifier is reported only once  
hello.c:6: error: for each function it appears in.)  
hello.c:6: error: syntax error before ‘entier’  
hello.c:7: error: ‘entier’ undeclared (first use in this function)  
make: *** [hello.o] Error 1  
zsh: exit 2     make  
~/Prog/rand0%

C'est clairement écrit : le type Hello n'est pas déclaré donc impossible de l'utiliser, cela nous lève donc une erreur de syntaxe juste avant « entier » dans la compilation de notre fichier hello.o.

Si maintenant nous ajoutons la ligne nécessaire (#include "hello.h"), et que nous compilons puis exécutons :

~/Prog/rand0% make
cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb  
main.c: In function ‘main’:  
main.c:6: warning: implicit declaration of function ‘says_hello’  
cc -o hello.o -c hello.c -W -Wall -ansi -pedantic -ggdb  
cc -o rand0 main.o hello.o  
~/Prog/rand0% ./rand0 
Hello world!  
Here is my number: 3  
~/Prog/rand0%

Ok, ça marche mais toujours avec le warning donc il faut aussi le rajouter dans main.c !
Il faut comprendre que les fichiers d'en-têtes sont utiles lorsque que nous avons besoin des déclarations de prototypes ou lorsque vous faites appel à une fonction qui elle-même en appelle une autre déclarée plus bas dans le fichier.

4. Tests sur les dépendances de compilation

Comme écrit plus haut, nous allons tenter d'inverser la compilation de nos fichiers .o et comprendre comment cela se fait.

Makefile

Fragment du code source du fichier Makefile :

# rand0 dépend de main.o et de hello.o
rand0: main.o hello.o  
        $(CC) -o rand0 hello.o main.o

Si on compile, (faire un make clean préalablement), on constate que l'ordre de compilation n'a pas changé.
Maintenant, changeons l'ordre des dépendances de compilation de « rand0 » :

Fragment du code source du fichier Makefile :

# rand0 dépend de main.o et de hello.o
rand0: hello.o main.o  
        $(CC) -o rand0 hello.o main.o

Après sauvegarde et compilation, nous obtenons bien un changement d'ordre :

~/Prog/rand0% make clean  
rm -rf *.o *~ rand0  
~/Prog/rand0% make        
cc -o hello.o -c hello.c -W -Wall -ansi -pedantic -ggdb  
cc -o main.o -c main.c -W -Wall -ansi -pedantic -ggdb  
cc -o rand0 hello.o main.o  
~/Prog/rand0% 

Donc on comprend bien que la ligne du dessus (rand0: hello.o main.o) correspond en fait aux dépendances de fichiers .o afin d'assembler notre programme alors que la ligne du dessous ($(CC) -o rand0 hello.o main.o) correspond à la commande qui sera passée au shell afin de d'assembler les fichiers .o.

Dans ce cas de dépendance, quelque soit l'ordre de compilation des objets, cela ne semble pas avoir d'influence sur l'assemblage final de notre programme. Cependant, il se pourrait que cela ait une importance dans certains cas de dépendance extrême mais je n'ai pas encore trouvé de cas alors si vous avez un retour d'expérience dessus, merci de m'en faire part.

5. Conclusion

Pour avoir galéré avec pas plus de 4 paires de fichiers .c et .h contenant des dépendances d'inclusions de types, il a fallu que j'effectue plusieurs tests afin de bien comprendre comment et pourquoi cela ne fonctionnait pas.

La seule chose que vous devez retenir, c'est qu'à chaque fois que vous utilisez une fonction, un type ou quoique ce soit d'autre dans votre fichier et qui n'est pas déclaré dedans ou dans votre en-tête, vous devez inclure son en-tête.


Joris Berthelot