Glibc
Dans cette page, je vais évoquer la rédaction, la compilation et l'utilisation des bibliothèque standard C. Je traiterai avant tout de développement sous linux bien que la majorité des explications soient applicables à d'autres systèmes d'exploitation, y compris ceux qu'on voudrait voir passer par la fenêtre ;-).
Introduction
Intérêt ?
Imaginez : Vous avez développé une série de fonctions très utiles pour un projet, prenons l'exemple d'un tri par bulles. Un jour, vous devez effectuer une nouvelle série de tris. Vous ouvrez le projet et passez la journée à faire du copier coller ?!?! NON !!! car vous aviez pensé à créer un librairie contenant la fonction principale du tri, n'est ce pas ?
Les librairies permettent de se constituer des bibliothèques de fonctions réutilisables et distribuables.
Vous voulez un autre avantage des librairies ?
On les compile à part ! Si vous avez comme moi un pc qui se fait vieux, vous savez que la compilation peut prendre pas mal de temps. La bonne nouvelle c'est qu'une fois les librairies compilées, vous ne les recompilez pas à chaque fois que vous recompilez votre projet. Tout au plus, vous les liez, s'il s'agit de bibliothèques statiques.
Qu'est ce qu'il y a dedans ?
Une librairie est composée d'un fichier d'en têtes (headers en anglais, d'ou le .h), ce sont les fichiers .h qui doivent vous dire quelque chose, non ? Et d'une partie binaire.
Le fichier d'en têtes, permet de "déclarer" les fonctions présentes dans la librairie, le type de valeurs qu'elles renvoient et les paramètres à leur passer.
La partie binaire est constituée d'un ou plusieurs fichiers c compilés. Ces fichiers c contiennent l'implémentation des prototypes de fonctions déclarées dans le fichier d'en têtes.
Vous pouvez modifier le code de la librarie autant que vous le désirez, du moment que vous respectez les prototypes définis dans le fichier .h.
Statique, Dynamique ?
Ouais, entrons dans le vif du sujet. Qu'est ce qu'une librairie statique ? et une librairie dynamique ? Quelles sont les avantages et inconvénients de chacune ?
- Quand vous utilisez une librairie statique (généralement .a sous linux, .lib sous ouinouin) la librairie est liée à l'exécutable généré. C'est à dire que vous obtenez un fichier exécutable relativement "lourd".
D'autre part, si vous apportez des modifications à la librairie, vous devez recompiler la librairie ET l'exécutable.
Bon coté : lorsque vous distribuez votre application, l'exécutable contient tout ce qui lui est nécessaire pour fonctionner, ce qui évite les problèmes de dépendances. - L'utilisation des librairies dynamiques apporte plusieurs avantages :
- La librairie est liée au programme qui l'utilise lors de l'exécution, l'exécutable distribué est ainsi plus léger que dans le cas d'une librairie statique.
- La librarie n'est chargée qu'une fois en mémoire, même si elle est utilisée par plusieurs exécutables.
- Lors de modifications apportées à la librarie, l'application l'utilisant n'a pas besoin d'être recompilée, à la condition toutefois que ces changements respectent les prototypes définis dans le fichier d'en têtes.
Mais il y a aussi des inconvénients :
- Pour exécuter une application faisant appel à une librarie dynamique, vous devez posséder ces librairies et elles doivent pouvoir être trouver par le système. Nous verrons plus loin quelques façons de vérifier tout celà.
- Le temps de lancement peut être plus long, étant donné que la liaison se fait lors de l'exécution.
Nota Bene : les liens avec les librairies dynamiques ne se font pas toujours lors de l'exécution, un programme peut en effet charger lui même les librairies dont il a besoin quand il en a besoin (voir les fonctions dlopen() et dlsym()), Celà est utile pour construire l'application au fur et à mesure de son exécution. Imaginez par exemple que vous écriviez un programme de retouche d'image, pourquoi charger les libraries de traitement de fichier jpeg si pendant une "session" l'utilisateur ne se sert que de fichiers .png ?
Plus d'infos sur cette partie ?
Consultez les pages man de dlopen et dlsym et celles qui y sont conseillées et tapant dans la console
man dlopen man dlsym
Ces fonctions permettent notamment de réaliser des "plugins", c'est à dire des bout de codes qui sont chargés durant l'exécution, pour permettre par exemple d'étendre les fonctionalités d'un programme sans le modifier (à condition que ça été prévu). En pratique, un programme peut charger par dlopen beaucoup (des milliers au moins) de "plugins" ensemble.
Une autre possibilité est même de méta-programmer, c'est à dire d'écrire un programme qui génère le code pour le traitement approprié des données. Il suffit pour ça (par exemple) de générer à l'exécution du code C dans un fichier gen.c, de lancer depuis le programme sa compilation (avec la fonction system du C par exemple) en exécutant la commande gcc -O -fPIC -shared gen.c -o gen.so, puis de charger le code obtenu par dlopen
Rédaction : remarques
Quelques généralités
Si vous voulez que votre librairie soit réutilisable et efficace, vous devez respecter quelques notions fondamentales de la programmation.
- Limitez au minimum les variables globales. Préférez, lorsque c'est possible, de passer des paramètres au fonctions plutôt que d'utiliser les variables globales. C'est plus facile de s'y retrouver et ça occuppe moins de mémoire.
Par exemple, si vous avez une fonction qui traite les données d'un tableau dont vous devez connaitre le nombre d'éléments :
Pas Bien ! |
Cette solution fait appel à une variable globale pour connaitre la taille du tableau. C'est déconseillé ! |
/* * ... */ int tailleTab; long * traiteTab (long * tab) { /* * * Traitement des données * */ } /* * ... */ |
Bien ! |
Ici, la taille du tableau est passée en paramètre à la fonction. C'est mieux ! |
/* * ... */ long * traiteTab (long * tab, int tailleTab) { /* * * Traitement des données * */ } /* * ... */ |
- Commentez vos sources ! Comment voulez vous vous y retrouver rapidement dans le code de quelqu'un d'autre ou même le vôtre si il n'y a aucun commentaire ? Celà est surtout vrai pour les fichiers d'en tête, expliquez ce que font les fonctions et ce que sont les arguments. (Je sais, c'est moins chiant à dire qu'à faire)
- Evitez le plus possible les structures spécifiques à une application particulière. Par exemple, si vous utilisez des chaines de moins de 50 caractères dans votre application, ne limitez pas vos variables à cette valeur. Qui sait si votre patron ne va pas décider dans deux jours que les chaînes doivent pouvoir comporter 1000 caractères ? (Je vous jure que ça arrive ! ;-) ). Préférez toujours, quand c'est possible, une allocation dynamique de mémoire à vos variables. (voir malloc(), realloc(); calloc() et free())
- Lorsqu'on utilise des fichiers d'en têtes abondamment, il y a un risque d'en inclure un plusieurs fois, ce qui allonge le temps de compilation et produit éventuellement des erreurs. pour pallier à ce problème, on utilise la clause #ifndef __NOM_FICHIER_H.
Par exemple, si votre fichier s'appelle monTest.h, placez ceslignes au début et à la fin du fichier:
Si le fichier à été inclus, _MON_TEST_H est défini et il ne le sera pas à nouveau. |
#ifndef _MON_TEST_H #define _MON_TEST_H 1 /* * ... *ici, le contenu normal du fichier monTest.h * ... */ #endif |
Les "chemins"
La directive #include : pour inclure un fichier dans un autre en c, on utilise la directive #include.
On spécifie le chemin relatif entre le fichier incluant et le fichier inclus, par exemple :
#include "mesH/monTest.h"
ou bien on place le fichier .h dans le répertoire ou le compilateur s'attend à trouver les fichiers .h. (regardez dans /usr/include par exemple) et on donne le chemin relatif par rapport à ce répertoire, par exemple :
#include <orb/orbit.h>
Nota Bene : On peut également spécifier d'autres chemins de recherche grâce à l'option -I de gcc, par exemple, pour y inclure le répertoire courant :
gcc -I. etc...
Pour devenir incollables sur ces amusantes petites choses, apprenez par coeur les man pages de gcc et ld. ;-)
Plus d'infos sur cette partie ?
Consultez les pages man des fonctions d'allocation dynamique de mémoire, du compilateur, du linker et celles qui y sont conseillées et tapant dans la console :
man malloc man realloc man calloc man free man gcc man ld
On rentre enfin dans le vif du sujet avec cette partie. On va en effet maintenant suivre un exemple pas à pas pour construire une librarie, la compiler, l'utiliser dans un exécutable sous ses formes statiques et dynamiques.
On va prendre l'exemple d'un librairie comportant une fonction permettant de trier un tableau contenant des entiers longs. La méthode de tri sera le tri par bulles. Je ne suis pas vraiment sûr que ce soit cet algorithme là mais ça marche alors ... Roule !
Rédaction : le fichier d'en têtes
Bien entrons dans le dedans du vif du sujet ! ouvrez un éditeur de texte et tapez ça dans un fichier tri_a_bulles.h :
tri_a_bulles.h /* * tri_a_bulles.h * * Quelques fonctions pour opérations basiques sur un tableau de longs. * * auteur: Xavier GARREAU : xgarreau@club-internet.fr * * web : http://perso.club-internet.fr/xgarreau/ * * dmodif: 13.03.2000 * */ #ifndef _TRI_A_BULLES_H #define _TRI_A_BULLES_H 1
/* * test_case_tableau : Dans le tableau pointé par addr_tableau, * Vérifie que la case case case_tableau est bien placée par rapport à celle qui la précède * Si ce n'est pas le cas, permute les cases et se rappelle sur la case précédente. * La récurrence s'arrête quand * la case case case_tableau est bien placée par rapport à celle qui la précède * ou bien si case_tableau vaut prem_case_tableau. * Ne renvoie rien. * */ void test_case_tableau ( long * addr_tableau, int taille_tableau, int case_tableau, int prem_case_tableau );
/* * permut_cases : * permute les valeurs des cases case_1 et case_2 du tableau pointé par addr_tableau * Ne renvoie rien. * */ void permut_cases ( long * addr_tableau, int case_1, int case_2 );
/* * test_tableau : * renvoie le tableau pointé par addr_tableau trié (ordre croissant) * de la case prem_case_tableau * à la case taille_tableau-1 * */ long * test_tableau ( long * addr_tableau, int prem_case_tableau, int taille_tableau );
/* * nb_cases_tableau : * Renvoie le nombre de cases du tableau pointé par addr_tableau * càd nombre de cases allouées * ou première case contenant (long)NULL si le tableau en contient une. * */ int nb_cases_tableau ( long * addr_tableau );
#endif
Si vous êtes familier avec les fichiers .h, pas de problème. Sinon, disons qu'on se contente de définir les "prototypes" des fonctions, on écrira leur corps dans un fichier .c.
Les fichiers .h permettent au compilateur de connaitre les prototypes des fonctions qu'il rencontre dans les différents fichiers.c qui les utilisent.
Je m'explique ! Les fonctions définies dans un fichier .c peuvent être utilisées dans un autre, ça vous le savez ! (hein ? vous le savez ?) Il suffit de préciser au compilateur tous les fichiers à compiler (gcc fic1.c fic2.c ... ficn.c). Si par hasard vous commettez une erreur en appelant une fonction, vous verrez que la compilation se passera sans problème, MAIS, lors de l'exécution vous obtiendrez des résultats inattendus ou pire, une erreur. Brrrrrrrrrrrr ... Flippant non ?
Bon, si on programmait ? Un petit peu de récursivité maintenant ? GO ! BANZAI !
Rédaction : le fichier c
Ouvrez un éditeur de texte et tapez ça dans un fichier tri_a_bulles.c :
tri_a_bulles.c /* * tri_a_bulles.c * * Quelques fonctions pour opérations basiques sur un tableau de longs. * * auteur: Xavier GARREAU : xgarreau@club-internet.fr * * web : http://perso.club-internet.fr/xgarreau/ * * dmodif: 13.03.2000 * */ #include "tri_a_bulles.h"
void test_case_tableau ( long * addr_tableau, int taille_tableau, int case_tableau, int prem_case_tableau ) { if ( case_tableau > prem_case_tableau ) if (addr_tableau[case_tableau] < addr_tableau[case_tableau-1]) { permut_cases ( addr_tableau, case_tableau, case_tableau-1 ); test_case_tableau ( addr_tableau, taille_tableau, case_tableau-1, prem_case_tableau ); } }
void permut_cases ( long * addr_tableau, int case_1, int case_2 ) { long tempo;
tempo = addr_tableau[case_1]; addr_tableau[case_1] = addr_tableau[case_2]; addr_tableau[case_2] = tempo; }
long * test_tableau ( long * addr_tableau, int prem_case_tableau, int taille_tableau ) { int i;
for (i=prem_case_tableau ; i<taille_tableau ; i++) test_case_tableau (addr_tableau, taille_tableau, i, prem_case_tableau);
return addr_tableau; }
int nb_cases_tableau ( long * addr_tableau ) { int nb_cases;
nb_cases=0; while (addr_tableau[nb_cases]) nb_cases++; return (nb_cases); }
Finalement ça s'est bien passé non ? Pas si compliqué !
Bon si on veut utiliser ça il va falloir créer une application qui en a besoin ! On y va ?
Rédaction : le fichier de l'application
Maintenant que nous avons les sources de notre librairie, prêtes à être compilées et liées, il va falloir penser à construire une application pour utiliser les fonctions que l'on y a mis ! Ce sera bientôt chose faite si vous voulez bien vous prêter encore un peu au jeu de cette dernière fastidieuse saisie.
Ouvrez un éditeur de texte et tapez ça dans un fichier test.c :
test.c /* * test.c * * Une application qui utilise les fonctions de la librairie tri_a_bulles * * auteur: Xavier GARREAU : xgarreau@club-internet.fr * * web : http://perso.club-internet.fr/xgarreau/ * * dmodif: 14.03.2000 * */ #include <stdio.h> #include <stdlib.h> #include "tri_a_bulles.h"
int main (int argc, char * argv[]) { long * tab; int i;
/* créée un tableau de 10 longs */ tab = (long *)malloc( 10 * sizeof (long) );
/* Initialise le générateur de nombre aléatoires avec ... * L'heure de lancement ... * voir man random ou man rand pour le pourquoi de la chose !!! * voir man time pour le comment !!! */ srandom((int)time((time_t *)NULL));
/* remplit le tableau avec une suite de nombres pseudo-aléatoires * et affiche le contenu. */ for (i=0; i<10; i++) { tab[i] = random(); printf ("tableau[%d] = %ld\n", i, tab[i]); }
/* Affiche le nombre de cases su tableau retourné par la librairie */ printf ("\nTaille du tableau : %d\n", nb_cases_tableau (tab));
/* Permute 2 cases et affiche le tableau résultant */ permut_cases (tab, 2, 8); printf ("\nTableau après permutation des cases 2 et 8.\n"); for (i=0; i<10; i++) printf ("tableau[%d] = %ld\n", i, tab[i]);
/* Trie le tableau et affiche le résultat */ printf ("\nTableau trié par la fonction de la librairie\n"); test_tableau ( tab, 0, nb_cases_tableau (tab) ); for (i=0; i<10; i++) printf ("tableau[%d] = %ld\n", i, tab[i]);
return 0; }
Bien ! Maintenant qu'on a tous les bouts, on va pouvoir compiler, lier, exécuter, etc...
Compilation de tous les binious
Bon, et bien, nous y voilà, on a tout ! Il ne nous reste plus qu'à tout mettre nesemble selon différentes méthodes. On va commencer par la méthode du projet unique, sans librairies.
Après ça, on va se faire une petite librairie statique, ensuite, on va mettre en place une librarie dynamique (ou shared object, .so chez les pingouins, .dll chez les défenestrés !)
On n'abordera toutefois pas le cas de la construction des dll car franchement, ce serait perdre du temps pour rien. Je suis pour laisser les gens qui n'ont que ça pour occupper leurs tristes journées générer et utiliser (de façon HYPER galère) les librairies dynamiques en envirronnement ouinouin. Ici, je pense que nous sommes entre gens sérieux, on développe donc sous linux, FreeBSD, solaris ou autres unix. Franchement, je m'excuse de sembler aussi méchant vis à vis de wintruc mais quand vous aurez comparé les qualités des deux systèmes (au moins en matière de support de développement), je suis presque sûr que vous penserez comme moi.
A la fin de cette série d'infos, je vous renverrai sur les bons coins pour utiliser les makefiles, ainsi que les merveilleux outils GNU que sont autoconf, autoscan, automake et leurs copains.
Sans librairies
Placez vous dans le répertoire du projet puis tapez ça dans la console : gcc -o test test.c tri_a_bulles.c Ceci génère un exécutable test que l'on lance, toujours en étant placé dans le répertoire de projet, en tapant : ./test |
Avec librairies statiques
Placez vous dans le répertoire du projet puis tapez ça dans la console : gcc -c test.c Vous obtenez un objet binaire test.o. C'est une compilation sans édition de liens. Compilez de même la librairie en tapant : gcc -c tri_a_bulles.c Ce qui donne tri_a_bulles.o. Comme je l'ai déjà dit, si plusieurs fichiers composent la bibliothèque, on aurait tapé gcc -c fic1.c fic2.c ... Il faut ensuite créer la bibliothèque. Pour celà, tapez : ar -q tri_a_bulles.a tri_a_bulles.o S'il y avait eu plusieurs fichiers ... référez vous au pages man de ar. Regardez surtout les options a, q et c ! Ok, maintenant on lie le tout : gcc -o test test.c tri_a_bulles.a Petite précision, normalement, l'éditeur de liens GNU de linux c'est ld. Ceci dit, ça marche avec gcc parce qu'il "l'appelle" alors, on ne vient pas se plaindre. Toutefois, dans le doute et pour en savoir plus, tapez man ld
On ne se laisse pas décourager pour autant et on exécute : ./test C'est bizzare non ? Ca marche pareil. Bienvenue dans le monde du codage efficace ... |
Avec librairies dynamiques
Placez vous dans le répertoire du projet puis tapez ça dans la console : gcc -c test.c Puis : gcc -c tri_a_bulles.c Jusque là, vous n'êtes pas perdus, c'est pareil ! Oui, mais, maintenant on génère la librairie dynamique : gcc -o tri_a_bulles.so -shared tri_a_bulles.o Puis on génère l'exécutable en lui disant qu'il fera appel à la librarie dynamique tri_a_bulles.so : gcc -o test test.o tri_a_bulles.so Puis content qu'on est, on exécute : ./test ./test: error in loading shared libraries: tri_a_bulles.so: cannot open shared object file: No such file or directory Et oui, le bon des librairies partagées c'est qu'elles sont liées au moment de l'exécution. Or, les librairies partagées, le système va les chercher dans un répertoire contenu dans la variable d'environnement LD_LIBRARY_PATH. Et bien, par défaut le répertoire courant n'en fait pas partie ... C'est con ? ldd ./test tri_a_bulles.so => not found libc.so.6 => /lib/libc.so.6 (0x4001a000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) Visiblement, il y a une couille ! ./test |
En conclusion
Avec les libraries statiques :
- Vous fournissez l'interface et maintenez le binaire.
- Vous pouvez apportez les modifications (à condition de respecter l'interface) que vous voulez à la librarie MAIS :
- Toute modification de librairie nécessite une réédition de lien, d'ou recompilation de l'exécutable !
Avec les libraries dynamiques :
- Vous fournissez l'interface et maintenez le binaire.
- Vous pouvez apportez les modifications (à condition de respecter l'interface) que vous voulez à la librarie.
- En cas de modification, vous n'avez qu'à remplacer l'ancienne librairie par la nouvelle.
Notons que :
- L'option -o nom sert à préciser le nom de l'exécutable résultant, ici test. Sinon, on obtient un binaire exécutable a.out.
- L'option -c sert à compiler sans édition de liens.
- Les extensions .a et .so n'ont rien de formel. Si vous voulez, dans un élan nostalgique, utiliser .lib et .dll ou .bite et .cul, vous avez le droit, vous êtes libres, vous êtes sous linux ...
- Il est bon de taper dans sa console bien aimée : man gcc
- Il est bon de taper dans sa console bien aimée : man ar
- Il est bon de taper dans sa console bien aimée : man ld
- ldd sert à connaître les dépendances dynamiques d'un exécutable.
- On peut ajouter des chemins de recherche de librairies dynamiques dans la vairiable d'environnement LD_LIBRARY_PATH ou dans le fichier /etc/ld.so.conf.
Le résultat
J'allais oublier !!! |
Normalement, le programme affiche un truc comme ça : xavier@Rooty Rep: tabLong $ ./test tableau[0] = 1613489603 tableau[1] = 866884903 tableau[2] = 295298324 tableau[3] = 1614953842 tableau[4] = 1111079167 tableau[5] = 950260573 tableau[6] = 901366332 tableau[7] = 745370511 tableau[8] = 2063084132 tableau[9] = 374329280 Taille du tableau : 10 Tableau après permutation des cases 2 et 8. tableau[0] = 1613489603 tableau[1] = 866884903 tableau[2] = 2063084132 tableau[3] = 1614953842 tableau[4] = 1111079167 tableau[5] = 950260573 tableau[6] = 901366332 tableau[7] = 745370511 tableau[8] = 295298324 tableau[9] = 374329280 Tableau trié par la fonction de la librairie tableau[0] = 295298324 tableau[1] = 374329280 tableau[2] = 745370511 tableau[3] = 866884903 tableau[4] = 901366332 tableau[5] = 950260573 tableau[6] = 1111079167 tableau[7] = 1613489603 tableau[8] = 1614953842 tableau[9] = 2063084132 En scoop, vous apprenez que mon pc s'appelle Rooty (ce qui vient des Régulateurs de Bachman), que je me connecte sous le nom de xavier et que ce projet se trouve dans le répertoire tabLong. En outre le symbole $ précise que je ne suis qu'un simple utilisateur, le root ayant droit à un superbe #. Voilà ! |
Pour ce qui est du renvoi sur la doc sur autoscan, autoconf, automake et leurs copains ce que j'ai trouvé de plus sympa, ce sont les infos pages du gnome-help-browser. Si vous ne l'avez pas installé, vous pouvez les consulter dans la console en tapant info automake ou info autoscan, etc ... !
Pour trouver moults docs de developpement, voyez http://developer.gnome.org/. Il existe la même chose avec les libs kde sur http://developer.kde.org/. Il existe bien d'autres sources d'infos mais ce sont celles que je préfère.
Un petit conseil, si vous aimez le c, installez les librairies gtk+/gdk/glib/imlib/ORBit C'est le top.
Vous cherchez un environnement de développement ? Choisissez gIDE et glade si vous avez des affinités avec le projet et les librairies du projet GNOME ou kdevelop pour affinités avec KDE.
Comme débogueur je dois dire que kdbg est génial, d'autant qu'il s'intègre via DCOP dans kdevelop. Mais xemacs est pas mal non plus et gdb tout seul aussi, le tout c'est de connaitre !
En bref, prenons ce qui existe de meilleur partout. Bienvenue dans linux et à bientôt pour de nouvelles aventures ...
@ Retour à la rubrique Développement
Copyright
Copyright © 03/01/2001, Xavier GARREAU (alaide)
Ce document est publié sous licence Creative Commons Attribution, Partage à l'identique, Contexte non commercial 2.0 : http://creativecommons.org/licenses/by-nc-sa/2.0/fr/ |