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 le produit de deux nombres entiers en séparant le programme et les fonctions (somme et produit) dans deux fichiers différents.
#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
;
}
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 objet). Placé devant une définition, il permet d'indiquer que la variable ou la fonction est visible dans tout le projet, ce qui est déjà le comportement par défaut.
I-C. Le mot-clé static▲
Le mot-clé static permet de restreindre 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. Par exemple :
#include <stdio.h>
static
int
id
(
int
x); /* Declaration de la fonction "privee" id() (utilisee dans main()). */
int
main
(
)
{
int
x =
0
;
printf
(
"
id(%d) = %d
\n
"
, x, id
(
x));
return
0
;
}
int
id
(
int
x)
{
return
x;
}
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.) communs à 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.
#ifndef H_SOMME_H
#define H_SOMME_H
int
somme
(
int
a, int
b);
int
produit
(
int
a, int
b);
#endif
Le fait d'avoir choisi H_SOMME_H 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 réservés du langage. Par exemple, les identifiants commençant par E, LC_, SIG, etc. sont réservé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 :
#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 (c'est-à-dire sa définition) 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
#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
#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. Comme la définition de la structure n'apparaît pas plus bas, La structure est alors opaque.
Voici un exemple d'utilisation de integer.h :
#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 :
#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ées. 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 EDI.
Évidemment 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. À 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 DLL (.dll) pour Dynamic-Link Library. Sous UNIX on les appelle Shared Objects (.so). Dans ce tutoriel, nous nous intéresserons aux DLL.
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. Ce qui ne sont pas exportées sont privées.
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 reprise 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 :
__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. À 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 :
#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 (ce fichier se trouve dans le répertoire système). Les compilateurs utilisent généralement cette bibliothèque comme corps de la bibliothèque standard, mais rien n'empêche un compilateur particulier de disposer de sa propre bibliothèque d'exécution. Visual C++ 2005 (Visual C++ 8.0) par exemple utilise msvcr80.dll (msvcr80d.dll en mode Debug) au lieu de msvcrt.dll.
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 API du système sont appelés appels système.
Les DLL sont très utilisées sous Windows et le système lui-même expose son API via de nombreuses DLL. Les programmes conçus spécifiquement pour Windows se lient donc à un ou plusieurs DLL de l'API Windows.
III. Conclusion▲
La compilation séparée permet de mieux organiser ses projets et de développer des bibliothèques de fonctions. C'est donc une technique qu'il faut absolument maîtriser si on veut développer sérieusement en C.