Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUMS C FAQs C TUTORIELS C LIVRES C COMPILATEURS C SOURCES GTK+

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.

               Version hors-ligne



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 :
struct integer_s;
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.



               Version hors-ligne

Valid XHTML 1.1!Valid CSS!

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.

Responsable bénévole de la rubrique C : Arnaud Feltz (buchs) - Contacter par EMail :
Vos questions techniques : forum d'entraide C - Publiez vos articles, tutoriels et cours
et rejoignez-nous dans l'équipe de rédaction du club d'entraide des développeurs francophones
Nous contacter - Copyright © 2000-2008 www.developpez.com - Legal informations.