compilation de prog multi-modules (bis)
Bonjour,
Je reviens différemment sur ce thème, déjà évoqué dans un précédent fil: http:[i]http://www.developpez.net/forums/d12...multi-modules/. Désolé pour ce message très long, mais j'essaie de faire le tour de la question en procédant logiquement, dans la mesure de mes moyens, et en abordant les divers aspects souvent laissés implicites du sujet, ainsi que les liens (sic!) entre ces aspects.
J'essaie de comprendre:
1. Les rôles respectifs des fichiers sources .c et .h .
2. Les rôles respectifs de l'inclusion (#include) et du "liage" explicite.
Je veux par ce dernier point l'indication explicite d'un nom de fichier (compilé) sur la ligne de commande d'édition de lien. Ce que je voudrais, au-delà de "comment faire?", arriver à comprendre assez clairement comment ça marche.
Pour ça j'utilise le programme exemple suivant:
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| // prog.c
#include "salut.c" [i] cette ligne-là change *******
int main () {
salut () ;
return 1 ;
}
// salut.c
#include <stdio.h>
void salut () {
puts ("Salut au monde !") ;
}
// salut.h
void salut () ; |
Exploration:
* Sans la ligne Astérix (;-), pas de compil.
* En incluant l'un ou l'autre salut.*, la compil roule.
* En incluant salut.h seul, le liage plante.
* En incluant salut.c seul, le liage et l'exécution roulent.
* Inclure .c et .h à la fois ne change rien: ça roule.
Alors l'édition de lien, là je la fais sans indication d'autre fichier que prog.o:
gcc -Wall -o prog prog.c
Déjà, je viens de réaliser réellement que l'incluson "physique" (sic!) des fichiers fait que le compilateur sait déjà presque tout sur la totalité de l'appli (sauf s'il y a incohérence entre une déclaration dans un fichier et la définiton correspondante ailleurs, ou son absence). Mais alors, que rajoute la distinction entre .c et .h? et celle entre compilation et édition de liens? Là, je pige plus trop.
J'ai dans l'idée que les deux marchent ensemble, et qu'ils ne servent qu'à faire de la compilation séparée. Les .h auraient pour but d'empêcher le compilateur de râler lorsqu'on compile séparément un module qui utilise des fonctionnalités externes (tiens, à propos, extern, encore un sujet à comprendre), en lui disant en gros "fais moi confiance, ce truc-là existe et il est comme j'te dis".
Très bien mais pour moi, sur une machine moderne, tant que le projet ne dépasse pas une certaine échelle, ça ne sert pas à grand chose. (Surtout que le compilo ne va pas recompiler les fichiers inchangés.) Et encore moins en phase de développement où l'optimisation (ce qui bouffe l'essentiel du temps de compil) est plus que secondaire.
Le truc, donc, ce serait de mettre les déclarations uniquement dans le .h, et de compiler en incluant seulement cela. Ce qui allège et accélère le processus. Ensuite, au liage, au lieu d'inclure les .c (ce qui imposerait une recompilation), on les indique à l'éditeur de lien explicitement. Est-ce que mon raisonnement est correct?
[HS: y a pas des mots français pour link, linker, linking? Pourquoi lier, lieur, liage (ou liaison ;-) ne sont-ils pas utilisés? On pourrait aussi dire re-lieur, re-lier rel-liage.]
Pour en revenir à mes problèmes, il me semble qu'il n'y a pas de différence fonctionnelle entre l'inclusion à la compilation et le "reliage" a posteriori. Je veux dire par là que mon prog fonctionnera strictement pareil si j'inclue le .c (avec ou sans .h) ou si j'inclue le .h seul et "relie" ensuite le .c. Est-ce que j'ai raison?
Je vérifie que ça marche, au moins (compil avec .h seul):
Code:
1 2 3
| spir@ospir:~/prog/C/comp$ gcc -Wall -o prog salut.o prog.o
spir@ospir:~/prog/C/comp$ ./prog
Salut au monde ! |
Alors est-ce que cette ligne de raisonnement est valable quelle que soit l'architecture du projet: je peux toujours soit inclure les .c (les fichiers qui contiennent les définitions explicites) (avec les .h si les déclarations sont exportées) à la compilation, soit inclure les .h (les fichiers qui contiennent les définitions explicites) uniquement et relier les .c à l'édition de lien seulement. Correct?
J'ai encore un petit problème. Je modifie mon programme ainsi, ce qui exporte la définition de 'salutation':
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| // prog.c
#include "salut.h"
int main () {
salut () ;
return 1 ;
}
// salut.c
#include <stdio.h>
#include "salut.h"
void salut () {
puts (salutation) ;
}
// salut.h
#ifndef H_SALUT
#define H_SALUT
const char * salutation = "Salut au monde !";
void salut () ;
#endif |
Là, malgré le #ifndef dans salut.h, j'ai une erreur à l'édition de lien (qui comme plus haut indique salut.o explicitement):
Code:
1 2 3 4
|
spir@ospir:~/prog/C/comp$ gcc -Wall -o prog salut.o prog.o
prog.o:(.data+0x0): multiple definition of `salutation'
salut.o:(.data+0x0): first defined here |
C'est tout-à-fait logique si je comprends bien: du fait de la compilation séparée, prog.c et salut.c incluent tous deux salut.h, malgré la précaution du #ifndef. Correct? Mais je suis bien obligé de l'inclure dans chacun, pour compiler, non? Je peux aussi dans prog.c inclure salut.c au lieu de salut .h, mais c'est pas ça le but du jeu, car là je compile avec prog.c la totalité du programme, ce n'est plus de la compilation séparée en fait. Alors quelle est la méthode que je n'ai pas trouvée tout seul?
Le but à terme pour moi est de définir la manière la plus pratique d'utiliser les outils C avec mon style de programmation "exploratoire". Ce style, en gros, du point de vue compilation, c'est que chaque fichier source peut être lancé à tout moment (en fonction de là où j'en suis dans mon développement) pour tester ses fonctionnalités ou son comportement exact, ou encore pour diagnostic. Typiquement (c'est une vision schématique), chacun de mes fichiers se termine par qq ch du genre:
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void test_xxx () { ... }
void test_yyy () { ... }
void test_zzz () { ... }
void test () {
// "hors-commenter" ce que je ne teste pas là maintenant
test_xxx () ;
test_yyy () ;
test_zzz :
}
// "hors-commenter" pour inclusion par un autre fichier source
void main () {
test () ;
exit (1) ;
} |
Il me semble que les outils genre makefile sont faits pour tout autre chose. En gros, pour moi, ils serviraient plutôt à distribuer le code en préparant la compilation par l'usager (utilisateur final ou autre programmeur) de l'application complète (ou du composant logiciel complet). Mais je me trompe peut-être totalement, peut-être que les makefiles sont tout-à-fait capables de permettre de programmer à ma façon.
Sinon, je suis prêt à écrire des outils moi-même. Par exemple, un bout de code qui lirait les #include dans les en-têtes de mes fichiers source, récursivement, en les cherchant dans une arborescence locale, pour éditer un fichier batch de compilation / édition de liens / exécution test.
Quelle méthode recommanderiez-vous?
2 autres points que j'ai en-tête d'explorer sont:
* Les différents type de fichiers à inclure prédéfinis par C ou par les outils C, et notamment pourquoi/comment l'éditeur de liens trouve les libs standards, et le sens concret de cette différence de syntaxe d'inclusion (<...> ou "...").
* Le(s) bon(s) usage(s) d'un makefile.
PS: J'ai dans l'idée de retracer ce fil de discussion pour un tutoriel sur le(s) sujet(s). Qu'en pensez-vous?
Merci à tous,
Denis
double inclusion de fichier header
Merci pour toutes vos réponses, ça clarifie bien les choses.
Alors, j'ai toujours pas compris comment éviter d'inclure plusieurs fois un fichier .h. Exemple:
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| // prog.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "circ.h"
int main () {
const float MAX_R = 999.9 ;
srand (time (NULL)) ;
float r = rand () * MAX_R / RAND_MAX ;
float c = circ (r) ;
printf ("rayon %.3f --> circonférence %.3f\n", r, c) ;
return 1 ;
}
//salut.c
#include <stdio.h>
#include "circ.h"
float circ (float r) {
return TAU * r ;
}
// salut.h
#ifndef H_SALUT
#define H_SALUT
const float TAU = 6.28 ; // voir http://tauday.com/tau-manifesto ;-)
float circ (float rayon) ;
#endif |
Une fois les 3 fichiers compilés séparément, prog.c et circ.c incluent tous deux le code de circ.h. Il me semble que c'est nécessaire, puisque prog.c doit avoir la déclaration de la fonction circ() et circ.c doit avoir celle de TAU. Correct?
Donc forcément, lorsque l'on relie tout ça, ça donne des erreurs. Voilà ma commande et son résultat:
Code:
1 2 3 4
| spir@ospir:~/prog/C/comp$ gcc -Wall -o prog prog.o circ.o
circ.o:(.rodata+0x0): multiple definition of `TAU'
prog.o:(.rodata+0x0): first defined here
collect2: ld returned 1 exit status |
Bon, je concède que l'exemple est un peu artificiel, car ici TAU n'a pas besoin d'être exporté, donc il pourrait être dans circ.c. Mais imaginons... De toute façon, le cas général n'est-il pas que la code client (ici prog) inclue le header de son "fournisseur" (circ.h), et le fichier source .c de ce module (circ.c) inclue aussi ce même header ?
Il y a un point qui m'échappe.
EDIT: Les tutoriels sur la compil séparée et sur makefile ne m'aide, car dans aucun des deux le fichier source .c du "module" n'inclue son header.
denis
programmation exploratoire
Bon, je réponds moi-même à la partie de ma question sur ce sujet. J'ai trouvé un moyen très pratique avec make.
Mon objectif était de pouvoir compiler en exécutable tout fichier source d'un projet, indépendamment et évidemment sans avoir à taper la commande avec toutes les dépendances à chaque (ou réécrire un batch, ou quoi que ce soit).
Le truc, c'est juste d'ajouter dans le makefile une "cible" de build pour chaque fichier source (ou au fur et mesure qu'ils commencent à être testable. Dans le cas de mon petit projet test, ça donne donc un makefile avec une seule entrée supplémentaire, celle nommée "circ":
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| CFLAGS=-Wall -ftabstop=3
LDFLAGS=
# CC=gcc
# EXEC=prog
all: prog
prog: circ.o prog.o
$(CC) circ.o prog.o -o prog $(LDFLAGS)
circ: circ.o
$(CC) circ.o -o circ $(LDFLAGS)
prog.o: prog.c
$(CC) -c prog.c -o prog.o $(CFLAGS)
circ.o: circ.c circ.h
$(CC) -c circ.c -o circ.o $(CFLAGS)
clean:
rm *.o
mrproper: clean
rm prog |
J'ai remplacé dans mon éditeur (geany) les commandes standard de compil et build (qui appellent directement gcc) par un usage de make
où %e représente le nom du fichier (sans extension) : ça appelle donc la "cible" correspondante du makefile. Et voilà! :D Pour tester le fichier courant, 2 ou 3 touches (compil -> build -> exec).
Tout commentaire ou astuce bienvenu(e).
Merci à vous,
Denis
[B][I]bug de compilation lié à fichier ;h;gch[/I][/B]
Voilà, je n'arrivais plus à compiler quelque chose qui fonctionnait auapravant (un "module", avec .c et .h correspondant). Le problème était dû à l'existence d'un fichier .h.ghc. J'imagine que ces fichiers servent peut-être à gcc à éviter de re-compiler les .h, susceptibles d'être inclus plusieurs fois; le contenu d'un tel fichier est du binaire.
Bon, je me doutais qu'il y avait un binz avec le header, pour ainsi dire "gelé"; vu que l'erreur était une incompatibilité de déclararions (que j'avais pourtant bien harmonisées dans les 2 sources), puis lorsque je l'ai à nouveau modifié (pour tenter de provoqier une re-compilation) les n° de lignes ne correspondaient plus. Dès que j'ai viré ce .h.gch, bingo! ça roule.
Alors ce qui est bizarre, c'est pourquoi make ne prenait pas en compte la version plus récente du header? (bien cité dans les dépendances, et d'ailleurs ça a re-marché sans que je change le makefile)
Ce que je voudrais, aussi, c'est pouvoir compiler à part ces header pour vérifier leur validité (en "autonomie"), avant de les inclure en compilant le(s) fichier(s) qui les citent. Il n' y a pas de problème de compile proprement dit, la commande est identique (et d'ailleurs il n'y a même pas besoin de préciser un nom de sortie) Le problème, c'est d'avoir une commande de compilation qui appelle une règle (générique ou spécifique) du makefile. Or, ces règles portent des noms de cibles et non pas de sources; ce qui fait que si je compilais ordinairement un .h, je produirais un .o, le nom de la cible correspondant; ce qui ne me semble pas sain du tout.
C'est peut-être ça un fichier .h.gch, d'ailleurs. PS: *** je viens de vérifier: oui, si je compile un .h, gcc fait un .h.gch ***
Denis