1 Introduction

J'aimerais bien pouvoir vous montrer tout plein de choses que l'on peut faire avec un compilateur et de l'audace. Cependant, il faut pour cela que je m'appuie sur du code source. Je vais donc créer spécialement le projet Luciole pour pouvoir faire des expérimentations.

1.1 Autotools

Les autotools sont un ensemble d'outils (principalement autoconf, automake et libtool) qui facilitent la vie du programmeur. Dans leur ensemble, ils peuvent être comparés à CMake pour ce qui est des fonctionnalités.

La différence par rapport à CMake réside -à mon sens- dans l'idée qu'il y a deux notions de code source : le code source du développeur et le code source de l'utilisateur.

  • Le code source du développeur comprend tous les fichiers que le développeur a écrit à la main, plus la partie légale.
  • Le code source de l'utilisateur comprend tous les fichiers nécessaires pour que l'utilisateur qui ne dispose que d'un shell, d'un programme make, des bibliothèques et d'un compilateur installe le programme en trois commandes :
./configure
make
make install

Ces deux notions regroupent en général à peu près les mêmes fichiers, mais il existe plusieurs exceptions, en particulier (et c'est ce qui va nous intéresser) pour le code source généré automatiquement. En effet, la génération de code ne dépend pas du système hôte, et nécessite un outil de développement qui n'est pas un compilateur natif.

Par exemple, lorsque l'on écrit du code Vala, un langage de programmation qui ressemble à du C# mais qui est traduit en code source C pour utiliser le système GObject, on ne considère comme code source pour le développeur que le fichier .vala. En revanche, le code C généré doit être distribué à l'utilisateur en tant que code source.

2 Projet Luciole

Nous allons créer un petit projet C qui nous permettra de manipuler des lucioles. Il se composera d'une bibliothèque, libluciole, et d'un exécutable, luciole.

2.1 Interface

L'interface que je vous propose d'implémenter pour libluciole (fichier 1) est la suivante :

 1: #ifndef H_LUCIOLE_INCLUDED
 2: #define H_LUCIOLE_INCLUDED
 3: 
 4: #ifdef __cplusplus
 5: extern "C"
 6: {
 7: #endif /* __cplusplus */
 8: 
 9:   /* Le type principal, caché pour faire de l'orientation objet.
10:    */
11:   struct Luciole;
12:   typedef struct Luciole Luciole;
13: 
14:   /* Initialise la bibliothèque, retourne 0 si tout s'est bien passé
15:    */
16:   int luciole_init ();
17: 
18:   /* Libère les ressources
19:    */
20:   void luciole_quit ();
21: 
22:   /* Alloue une nouvelle luciole ayant pour nom name
23:    */
24:   Luciole *luciole_malloc (const char *name);
25: 
26:   /* Libère la mémoire précédemment allouée
27:    */
28:   void luciole_free (Luciole *lucy);
29: 
30:   /* Retourne 1 si la luciole est allumée, 0 sinon.
31:    */
32:   int luciole_get_state (const Luciole *lucy);
33: 
34:   /* Retourne une description de l'état de la luciole
35:    */
36:   const char *luciole_get_strstate (const Luciole *lucy);
37: 
38:   /* Retourne le nom de la luciole
39:    */
40:   const char *luciole_get_name (const Luciole *lucy);
41: 
42:   /* Allume la luciole
43:    */
44:   void luciole_switch_on (Luciole *lucy);
45: 
46:   /* Éteint la luciole
47:    */
48:   void luciole_switch_off (Luciole *lucy);
49: 
50: #ifdef __cplusplus
51: }
52: #endif /* __cplusplus */
53: 
54: #endif /* not H_LUCIOLE_INCLUDED */

Notre bibliothèque déclare dans le fichier 1 les éléments suivants :

  • un type abstrait (Luciole, ligne 9) ;
  • des fonctions pour charger et quitter la bibliothèque (lignes 14, 18) ;
  • des fonctions pour l'allocation de mémoire (lignes 22, 26) ;
  • des accesseurs (lignes 30, 38, 42, 46) ;
  • enfin, une fonction luciole_get_strstate (ligne 34), qui retournera la représentation textuelle de l'état. Nous aurons besoin de construire et garder en mémoire la représentation des deux états, c'est pourquoi nous avons besoin d'une fonction d'initialisation.

Voici le code de l'exécutable que je vous propose :

 1: #include "luciole.h"
 2: #include <stdio.h>
 3: #include <assert.h>
 4: 
 5: int
 6: main ()
 7: {
 8:   Luciole *lucie;
 9:   assert (luciole_init () == 0);
10:   lucie = luciole_malloc ("Lucie");
11:   assert (lucie != NULL);
12:   luciole_switch_on (lucie);
13:   printf ("Je vous présente %s.  %s\n",
14: 	  luciole_get_name (lucie),
15: 	  luciole_get_strstate (lucie));
16:   luciole_free (lucie);
17:   luciole_quit ();
18:   return 0;
19: }

Il s'agit tout simplement :

  1. d'initialiser la bibliothèque, ligne 9 ;
  2. de créer une luciole, ligne 10 ;
  3. de l'allumer, ligne 12 ;
  4. de la présenter, ligne 13 ;
  5. de libérer les ressources, lignes 16 et 17.

2.2 Implémentation

L'implémentation est relativement simple. Pour la fonction luciole_get_strstate, on souhaite allouer les chaînes dynamiquement sur le tas lors de l'initialisation. On utilise donc deux variables globales constantes.

 1: #include "luciole.h"
 2: #include <string.h>
 3: #include <stdlib.h>
 4: 
 5: struct Luciole
 6: {
 7:   char *name;
 8:   int state;
 9: };
10: 
11: static char *state_0;
12: static char *state_1;
13: 
14: int
15: luciole_init ()
16: {
17:   state_0 = strdup ("La luciole est éteinte.");
18:   state_1 = strdup ("La luciole est allumée.");
19:   if (state_0 == NULL || state_1 == NULL)
20:     return 1;
21:   return 0;
22: }
23: 
24: void
25: luciole_quit ()
26: {
27:   free (state_0);
28:   free (state_1);
29: }
30: 
31: /* Il faut vérifier la valeur de retour des fonctions qui allouent de
32:    la mémoire. */
33: Luciole *
34: luciole_malloc (const char *name)
35: {
36:   Luciole *ret = NULL;
37:   ret = malloc (sizeof (Luciole));
38:   if (ret != NULL)
39:     {
40:       ret->name = strdup (name);
41:       ret->state = 0;
42:       if (ret->name == NULL)
43: 	{
44: 	  free (ret);
45: 	  ret = NULL;
46: 	}
47:     }
48:   return ret;
49: }
50: 
51: void
52: luciole_free (Luciole *lucy)
53: {
54:   free (lucy->name);
55:   free (lucy);
56: }
57: 
58: int
59: luciole_get_state (const Luciole *lucy)
60: {
61:   return lucy->state;
62: }
63: 
64: const char *
65: luciole_get_strstate (const Luciole *lucy)
66: {
67:   switch (lucy->state)
68:     {
69:     case 0:
70:       return state_0;
71:     case 1:
72:       return state_1;
73:     default: ;/* rien */
74:     }
75:   /* Impossible : l'état est soit 0 soit 1. */
76:   exit (1);
77: }
78: 
79: const char *
80: luciole_get_name (const Luciole *lucy)
81: {
82:   return lucy->name;
83: }
84: 
85: void
86: luciole_switch_on (Luciole *lucy)
87: {
88:   lucy->state = 1;
89: }
90: 
91: void
92: luciole_switch_off (Luciole *lucy)
93: {
94:   lucy->state = 0;
95: }

2.3 Mise en place des autotools

Pour mettre en place le système de compilation, on a besoin d'un certain nombre de fichiers.

No news.
See http://planete-kraus.eu/2017/09/02/projet-luciole.html

Fénix Kraus <vivien@planete-kraus.eu>
 1: #                                               -*- Autoconf -*-
 2: # Process this file with autoconf to produce a configure script.
 3: 
 4: AC_PREREQ([2.69]) #
 5: AC_INIT([luciole], [0.0], [vivien@planete-kraus.eu]) #
 6: AM_INIT_AUTOMAKE([subdir-objects]) #
 7: AC_CONFIG_MACRO_DIR([m4])
 8: 
 9: # Checks for programs.
10: AC_PROG_CC_STDC #
11: LT_INIT([win32-dll]) #
12: 
13: # Checks for libraries.
14: 
15: # Checks for header files.
16: 
17: # Checks for typedefs, structures, and compiler characteristics.
18: 
19: # Checks for library functions.
20: 
21: AC_CONFIG_FILES([Makefile]) #
22: 
23: AC_OUTPUT

Le fichier 8 comprend notamment :

  • ligne 4, une version minimale d'autoconf ;
  • ligne 5, l'initialisation de notre projet luciole, version 0.0 ;
  • ligne 10, une vérification de la disponibilité du compilateur C ;
  • ligne 21, la création d'un Makefile.
 1: bin_PROGRAMS = luciole #
 2: lib_LTLIBRARIES = libluciole.la #
 3: 
 4: include_HEADERS = luciole.h #
 5: libluciole_la_SOURCES = luciole.h luciole.c
 6: libluciole_la_LDFLAGS = -no-undefined -version-info 0:0:0 #
 7: 
 8: luciole_SOURCES = main.c #
 9: luciole_LDADD = libluciole.la #
10: 
11: ACLOCAL_AMFLAGS = -I m4

Le fichier 9 déclare un programme, luciole ligne 1, et une librarie, libluciole.so, ligne 2 (en utilisant libtool). Ensuite, on définit la bibliothèque, lignes 4-6, qui comprend un en-tête à installer dans le dossier /include, deux fichiers sources, et un numéro de version à la libtool. Enfin, on définit le programme, lignes 8-9, qui comprend le fichier source principal et qui lie la bibliothèque.

3 Vérifions si tout fonctionne bien

L'ensemble des fichiers est disponible à l'adresse ../../../code/luciole-maintainer.tar.gz.

Compilons (avec le plugin Address Sanitation, pour éviter certaines fuites) :

autoreconf --install
./configure CPPFLAGS="-fsanitize=address" LDFLAGS="-fsanitize=address"
make
./luciole

Je vous présente Lucie. La luciole est allumée.

4 Conclusion

Nous avons un petit projet, disponible ici. Le code source utilisateur est disponible ici. Nous nous en servirons pour nos expérimentations futures !

Suite de la saga : ici.