La compilation séparée
Date de publication : 14 avril 2008
Par
Jessee Edouard (Accueil)
La compilation séparée désigne le fait de compiler plusieurs fichiers sources séparément puis de les lier ensuite pour générer le produit final
qui peut être un exécutable par exemple. Elle comprend plusieurs techniques que nous allons explorer tout au long de ce tutoriel.
I. Généralités
I-A. Compilation d'un projet
I-A-1. Introduction
Un projet d'application en langage C est au moins constitué d'un fichier source qui sera
compilé, ce qui donnera un fichier objet en sortie, puis lié à d'autres fichiers objets pour
générer la sortie finale qui peut être un exécutable par exemple. Dans le cas général, un projet est constitué de plusieurs fichiers
sources.
I-A-2. Exemple avec un projet constitué de deux fichiers sources
I-A-2-a. Le projet
Nous allons créer un programme qui affiche la somme et la différence de deux nombres
entiers en séparant le programme et les fonctions (somme et produit) dans deux fichiers
différents.
| Fichier : exemple.c |
#include <stdio.h>
int somme(int a, int b);
int produit(int a, int b);
int main()
{
int a = 2, b = 5;
printf("%d + %d = %d\n", a, b, somme(a, b));
printf("%d * %d = %d\n", a, b, produit(a, b));
return 0;
}
|
| Fichier : somme.c |
int somme(int a, int b)
{
return a + b;
}
int produit(int a, int b)
{
int prod = 0;
while (b-- > 0)
prod += a;
return prod;
}
|
I-A-2-b. Compilation sous Code::Blocks
Pour compiler le projet, sélectionnez la commande Build > Build (Ctrl + F9). Cette
compilation se fait en deux phases : compilation des fichiers sources (exemple.c et
fonctions.c) puis édition des liens. On peut aussi compiler les fichiers sources
individuellement : Build > Compile Current File (Ctrl + Shift + F9). Dans ce cas
la commande Build (Ctrl + F9) ne fera plus que l'édition des liens.
I-B. Le mot-clé extern
En langage C, tout objet (variable ou fonction) doit toujours avoir été
déclaré avant d'être utilisé. Nous avons déjà résolu le problème pour les fonctions, ne reste donc
plus que les variables. Supposons donc que l'on souhaite, depuis un fichier donné, accéder à
une variable globale définie dans un autre fichier. On ne peut pas tout simplement déclarer
une deuxième fois la variable car on aurait alors deux variables de même nom au sein d'un
même projet ce qui conduirait à une erreur lors de l'édition des liens. Le mot-clé extern
permet de résoudre le problème. Placé devant une déclaration, il permet d'indiquer que la
variable ou fonction est définie (plus précisément : peut être définie) dans un autre fichier
(source ou compilé). Evidemment si l'objet déclaré est une fonction, ce mot-clé ne sert qu'à la
déco !
I-C. Le mot-clé static
Devant la déclaration d'une variable globale ou d'une fonction, le mot-clé static restreint la
visibilité de la variable ou de la fonction au fichier source courant. On a alors ce qu'on appelle
une variable ou fonction privée. Uniquement pour les fonctions : si la fonction est définie
avant sa première utilisation auquel cas elle ne nécessite donc pas de déclaration, alors on met
tout simplement le mot-clé static devant la définition. En effet, il est syntaxiquement correct
de placer ce mot-clé devant la déclaration ou la définition d'une fonction.
I-D. Les fichiers d'en-tête
Les fichiers d'en-tête (*.h) permettent de rassembler des « en-têtes » (c'est-à-dire des
déclarations de fonctions, des définitions de macros, de types, etc.) communes à plusieurs
fichiers sources et/ou fichiers d'en-tête. Mais puisqu'ils peuvent justement être inclus par un
grand nombre de fichiers, le risque d'être inclus plus d'une fois dans un même fichier est très
élevé. C'est pour cette raison que les fichiers d'en-têtes doivent impérativement être protégés
contre les inclusions multiples. La technique fait appel au préprocesseur : une macro indique
si le fichier est déjà inclus. Il suffit donc de tester dès le début du fichier si la macro est
définie ou non. Le schéma est donc le suivant :
#ifndef DRAPEAU
#define DRAPEAU
#endif
|
Dans l'exemple de projet précédent, on aurait pu par exemple créé un fichier somme.h
contenant la déclaration des fonctions somme et produit du fichier somme.c.
| Fichier : somme.h |
#ifndef H_SOMME
#define H_SOMME
int somme(int a, int b);
int produit(int a, int b);
#endif
|
Le fait d'avoir choisi H_SOMME plutôt que SOMME_H ou __SOMME_H__ comme drapeau n'est pas du tout
le fruit du hasard. Il permet de ne pas entrer en conflit avec les identifiants reservés du langage.
Par exemple, les identifiants commencants par E, LC_, SIG, etc. sont reservés (E pour les numéros d'erreur
de errno.h, LC_ pour les constantes définies par locale.h et SIG pour le les signaux de signal.h). H_ en début
d'un identifiant est pour l'heure encore libre, alors en profiter.
Il suffit maintenant d'inclure ce fichier partout où on a besoin des fonctions somme et produit. Par
exemple :
| Fichier : exemple.c |
#include <stdio.h>
#include "somme.h"
int main()
{
int a = 2, b = 5;
printf("%d + %d = %d\n", a, b, somme(a, b));
printf("%d * %d = %d\n", a, b, produit(a, b));
return 0;
}
|
Lorsque le nom d'un fichier est mis entre guillemets comme dans cet exemple, le
préprocesseur va d'abord chercher le fichier dans le même répertoire que celui du fichier
source puis s'il ne le trouve pas, va le chercher dans le ou les répertoires par défaut
(spécifiques du compilateur). On peut également spécifier un chemin absolu.
I-E. Les structures opaques
Une structure est dite opaque lorsque son implémentation est cachée. L'accès aux champs de
la structure se fait alors par l'intermédiaire de fonctions dont l'implémentation évidemment
est également cachée. En particulier, l'interface devra au moins fournir une fonction
permettant de créer l'objet (constructeur) et une fonction permettant de le détruire
(destructeur). Par exemple, nous allons encapsuler le type int dans une structure de type
integer_s.
Implémentons la structure dans un fichier integer.c
| Fichier : integer.c |
#include <stdlib.h>
struct integer_s {
int value;
};
struct integer_s * integer_create_object()
{
return malloc(sizeof(struct integer_s));
}
void integer_set_value(struct integer_s * p_object, int value)
{
p_object->value = value;
}
int integer_get_value(struct integer_s * p_object)
{
return p_object->value;
}
void integer_delete_object(struct integer_s * p_object)
{
free(p_object);
}
|
Fournissons maintenant l'interface via un fichier d'en-tête : integer.h
| integer.h |
#ifndef H_INTEGER
#define H_INTEGER
struct integer_s;
struct integer_s * integer_create_object(void);
void integer_set_value(struct integer_s * p_object, int value);
int integer_get_value(struct integer_s * p_object);
void integer_delete_object(struct integer_s * p_object);
#endif
|
La ligne :
déclare la structure (sans la définir). C'est ce qu'on appelle une déclaration incomplète. La
structure est alors opaque.
Voici un exemple d'utilisation de integer.h :
| exemple.c |
#include <stdio.h>
#include "integer.h"
int main()
{
struct integer_s * i = integer_create_object();
if (i != NULL)
{
integer_set_value(i, 100);
printf("The value of i is : %d\n", integer_get_value(i));
integer_delete_object(i);
}
return 0;
}
|
II. Les bibliothèques
II-A. Introduction
Une bibliothèque (library en anglais) est en première approximation un bouquet de fonctions. Chez
certains langages elles sont appelées unités ou paquetages mais le principe reste le même. En langage C, il faut en fait
également fournir le ou les fichiers d'en-tête correspondants (contenant la déclaration des fonctions de la bibliothèque,
des macros et/ou types supplémentaires, etc.) avant de réellement en constituer une.
La manière de créer et d'utiliser une bibliothèque est très dépendante de l'environnement avec lequel
on travaille. Dans ce tutoriel nous allons expliquer essentiellement la procédure pour MS
Visual Studio .NET donc évidemment sous Windows.
Comme nous le savons déjà, la compilation d'un fichier source
ne produit pas un exécutable mais un module objet (.o ou .obj), que l'on peut considérer
comme la version machine du fichier source original. Il faut ensuite lier différents modules
objets pour produire un exécutable.
Un module objet est réutilisable (on peut donc voir un tel fichier comme une véritable « boîte à
outils »). C'est là d'ailleurs toute l'importance de la compilation séparée. Reprenons par
exemple le fichier somme.c contenant le
code des fonctions somme et produit. En compilant ce fichier, on obtient un fichier
somme.obj.
Maintenant, créons un nouveau projet dans lequel nous allons utiliser les fonctions somme et
produit. Voici le programme :
| Fichier : exemple.c |
#include <stdio.h>
int somme(int a, int b);
int produit(int a, int b);
int main()
{
int a = 2, b = 5;
printf("%d + %d = %d\n", a, b, somme(a, b));
printf("%d * %d = %d\n", a, b, produit(a, b));
return 0;
}
|
Si on compile ce fichier, il n'y a aucune erreur puisque tout est syntaxiquement correct,
l'équivalent en langage machine peut donc être généré. Par contre si on tente de générer
l'exécutable, on aura un message d'erreur indiquant que l'édition des liens a échoué car les
fonctions somme et produit n'ont pu être trouvés. Il faut donc dire au linkeur qu'il doit
également chercher dans somme.obj lors de l'édition des liens. La procédure est évidemment
dépendante de l'environnement de développement. Sous Code::Blocks, c'est dans Project >
Build Options > Linker > Link Libraries. Sous Visual Studio .NET c'est Project >
Properties > Configuration Properties > Linker > Input > Additional
Dependencies. Il suffit ensuite d'ajouter somme.obj. Ce n'est pas plus différent non plus
avec les autres EDIs.
Evidemment si vous ne spécifiez pas de chemin complet, le linkeur va supposer que le fichier
se trouve dans le répertoire par défaut pour les libs (généralement un dossier nommé LIB dans
le répertoire d'installation du compilateur) qui est bien entendu spécifique du linkeur.
II-B. Les bibliothèques statiques
Une bibliothèque statique (.lib ou .a) est un fichier qui regroupe un ou plusieurs modules objets.
Elles s'utilisent donc de la même manière que ces derniers. Pour créer une bibliothèque statique
avec Visual Studio .NET, créez un nouveau projet Win32 (Win32 Project) puis dans
Paramètres de l'application (Application settings), choisissez Bibliothèque statique (Static
library). Cochez l'option Projet vide (Empty project) afin qu'aucun fichier source ne soit
automatiquement ajouté au projet. A la fin, compilez le projet à l'aide du menu Générer
(Build).
II-C. Les bibliothèques dynamiques
Une bibliothèque dynamique est un fichier qui ne sera effectivement lié à l'exécutable que
pendant l'exécution. Cela présente plusieurs avantages. Supposez par exemple que vous avez
créé une bibliothèque statique et que vous l'avez ensuite utilisé dans de nombreuses applications.
Si un jour vous la modifiez et que vous voulez également mettre à jour toutes vos
applications, vous devrez les recompiler une par une ! Pourtant si vous avez utilisé
une bibliothèque dynamique, la modification seule de ce fichier aura des répercussions sur toutes
les applications l'utilisant puisque la liaison avec le fichier ne se fait que pendant l'exécution.
De plus, si vous avez bien compris, l'utilisation des bibliothèques dynamiques rend les exécutables
plus petits (en terme de taille) puisque ce dernier même est incomplet. En effet, il a besoin du
code contenu dans la bibliothèque pour fonctionner.
Sous Windows, les bibliothèques dynamiques sont appelées DLLs (.dll) pour Dynamic-Link
Library. Sous UNIX on les appelle Shared Objects (.so). Dans ce tutoriel, nous-nous
intéresserons aux DLLs.
Nous allons donc créer une DLL (dsomme.dll) exportant deux fonctions : somme et produit.
Que signifie exporter ?
Ben c'est très simple : lorsqu'on développe une bibliothèque, on peut spécifier quelles fonctions
(ou variables) seront « publiques » (ou exportées), c'est-à-dire accessibles depuis l'extérieur,
et lesquelles seront « privées », c'est-à-dire réservées à usage interne. Nous avons déjà vu que
le mot-clé static permet de rendre une fonction privée. Cependant dans le cas d'une DLL,
toutes les fonctions sont par défaut privées ! Le mot-clé static ne nous sert donc plus à grandchose.
La question est donc maintenant : comment exporter une fonction. Et ben il y a plusieurs
manières de le faire, par exemple à l'aide du modificateur __declspec(dllexport). Bien
entendu, il s'agit bien d'une extension Microsoft aux langages C et C++ (en ce qui nous
concerne : le langage C) et qui fut ensuite repris par la plupart des implémentations pour
Windows. Donc pas de problème que vous compilez avec MingW ou Borland C++ ...
Sous Visual Studio .NET, créez un nouveau projet Win32 (Win32 Project) puis dans
Paramètres de l'application choisissez DLL. Cochez l'option Projet vide afin qu'aucun
fichier ne soit automatiquement ajouté au projet. Ajoutez ensuite un fichier dsomme.c puis
saisissez le code suivant :
| Fichier : dsomme.c |
__declspec(dllexport) int somme(int a, int b)
{
return a + b;
}
__declspec(dllexport) int produit(int a, int b)
{
int prod = 0;
while (b-- > 0)
prod += a;
return prod;
}
|
Compilez ensuite le projet avec la commande Build du menu Build. Vous obtiendrez entre
autres en sortie deux fichiers : dsomme.dll et dsomme.lib. Ce dernier, bien que portant
l'extension .lib, n'est pas une bibliothèque statique mais une bibliothèque d'importation. C'est lui
qu'il faut passer au linkeur lors de l'édition des liens pour pouvoir compiler du code
dépendant d'une DLL. A l'exécution, le programme doit pouvoir localiser la DLL. Cette dernière
doit donc se trouver soit dans le même répertoire que le programme, soit dans le répertoire
courant du programme, ou encore dans un répertoire « connu » du système par exemple le répertoire system32.
La déclaration de fonctions à importer depuis une DLL (via la bibliothèque d'importation) peut se
faire comme la déclaration d'une fonction « normale », cependant le modificateur
__declspec(dllimport) permet d'indiquer au compilateur (je dis bien le compilateur, pas le
linkeur) que la fonction en question se trouve dans une bibliothèque dynamique, ce qui lui
permettra de générer du code plus efficace (plus « direct »). Sans cela, le compilateur va tout
simplement convertir chaque appel de la fonction en appel de celle qui se trouve dans le .lib,
qui ne fait rien de plus qu'un appel à la fonction dans la DLL ce qui fait donc finalement deux
appels, ce qui est évidemment plus long qu'un appel direct. Notre programme sera donc :
| Fichier : exemple.c |
#include <stdio.h>
__declspec(dllimport) int somme(int a, int b);
__declspec(dllimport) int produit(int a, int b);
int main()
{
int a = 2, b = 5;
printf("%d + %d = %d\n", a, b, somme(a, b));
printf("%d * %d = %d\n", a, b, produit(a, b));
return 0;
}
|
Et n'oubliez pas : nous devons nous lier avec dsomme.lib.
II-D. Applications. Exemples
II-D-1. La bibliothèque standard du langage C
Sous Windows, la bibliothèque « standard » du langage C, c'est-à-dire celle qui contient entre autres
le code des fonctions standard du C est implémentée en tant que bibliothèque
dynamique connue sous le nom de Microsoft C Run-Time Library (CRT), et qui
correspond au fichier MSVCRT.DLL. On peut toujours bien sur se lier statiquement
avec cette bibliothèque (il suffit de faire les réglages nécessaires, ça dépend du compilateur)
mais dans ce cas les exécutables seront considérablement plus gros.
II-D-2. Le concept d'API
Une API ou Application Programming Interface (Interface de Programmation
d'Applications) est un ensemble de fonctions exposées par un système ou un logiciel pour
permettre à d'autres logiciels d'interagir (c'est-à-dire de communiquer) avec lui. Par
extension, toute fonction d'une API donnée est également appelée : une API. Sous UNIX, les
APIs du système sont appelés appels système.
Les DLLs sont très utilisées sous Windows et le système lui-même expose son API via de nombreuses DLLs.
Les programmes conçues spécifiquement pour Windows se lient donc à un ou plusieurs DLLs de l'API Windows.


Les sources présentées sur cette page sont libres de droits,
et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation
constitue une oeuvre intellectuelle protégée par les droits d'auteurs. Copyright ©
2008 Melem. Aucune reproduction,
même partielle, ne peut être faite de ce site et de l'ensemble de son contenu :
textes, documents, images, etc sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E
de dommages et intérêts.
Cette page est déposée à la
SACD.