1 Introduction

Précédemment, nous avions monté un petit projet tout simple à base d'autotools pour allumer et éteindre une luciole. Nous avions une bibliothèque toute simple, et un exécutable de test.

Nous allons mettre en place l'introspection GObject pour ce code d'exemple.

2 Mais qu'est-ce que l'introspection GObject ?

L'introspection GObject est une méthode permettant de scanner du code C pour déduire la structure d'orientation objet du code que l'on a écrit. Ce n'est pas une chose facile : en C, il n'y a pas de standard pour l'orientation objet. Il n'y a que des pointeurs et des fonctions. L'introspection suit donc une liste d'heuristiques, à savoir celles utilisées par GObject.

2.1 Mais qu'est-ce que GObject ?

GObject est une bibliothèque qui permet de définir des classes en C. Cette bibliothèque est très puissante : on peut définir des classes, des interfaces, et des objets dynamiquement dans le code. C'est-à-dire que l'on peut à tout moment rajouter des classes et les modifier au cours de l'exécution du programme, un peu comme en java. C'est plus souple qu'en C++, où l'ajout de classes ne peut se faire qu'à la compilation, mais il manque certaines choses, comme l'héritage multiple.

2.2 Mais qu'est-ce que l'introspection ?

Pour effectuer l'analyse du code, on est face à une difficulté : on ne peut pas savoir quelles classes seront disponibles lors de l'exécution du programme ! Il y a donc un deuxième volet de l'introspection GObject : la création d'un exécutable pour initialiser le code et voir quelles classes ont été créées par cette opération. On comprend bien pourquoi cela s'appelle de l'introspection.

Dans cet article, nous allons commencer tout doucement, en utilisant une classe très simple, correspondant à notre Luciole, sans chargement complexe de classes, sans énumération.

3 Plan d'action

Nous allons reprendre exactement la même interface et la même implémentation, simplement nous allons modifier l'environnement de compilation pour effectuer l'introspection. Nous allons faire quelque chose de simple pour commencer.

4 Modification du projet Luciole

Nous nous basons sur les instructions officielles.

4.1 Fichiers standards

Les fichiers standards sont presque inchangés.

Now introducing the GObject Introspection!
See https://blog.planete-kraus.eu/2017/09/09/introspection.html
2017-09-09  Vivien Kraus  <vivien@planete-kraus.eu>

	* NEWS: Happy message!
	* README: update site URL.
	* configure.ac: Require GObject introspection.
	* Makefile.am: Scan and build GObject introspection.
	* luciole_glib.h: Add the luciole type.
Fénix Kraus <vivien@planete-kraus.eu>

4.2 Fichier du système de compilation

4.2.1 Code shell

Pour mettre à jour le script configure, il faut faire deux choses :

  1. ajouter le code M4 pour chercher les outils d'introspection GObject ;
  2. modifier le fichier configure.ac.

La première étape consiste à trouver le fichier introspection.m4 et à le placer dans le sous-dossier m4.

Pour Debian, il est avec les autres fichiers m4.

mkdir -p m4
cp /usr/share/aclocal/introspection.m4 m4/

La modification du fichier configure.ac se fait ligne 12 pour rechercher les outils d'introspection, et ligne 15 pour trouver les dépendances à GObject.

 1: #                                               -*- Autoconf -*-
 2: # Process this file with autoconf to produce a configure script.
 3: 
 4: AC_PREREQ([2.69])
 5: AC_INIT([luciole], [0.1], [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: GOBJECT_INTROSPECTION_CHECK([1.30.0]) #
13: 
14: # Checks for libraries.
15: PKG_CHECK_MODULES([GOBJECT],[gobject-2.0],,) #
16: 
17: # Checks for header files.
18: 
19: # Checks for typedefs, structures, and compiler characteristics.
20: 
21: # Checks for library functions.
22: 
23: AC_CONFIG_FILES([Makefile])
24: 
25: AC_OUTPUT

4.2.2 Makefile

Le fichier Makefile.am est légèrement modifié :

  • ligne 1, on spécifie que l'on doit disposer de l'introspection pour le bon fonctionnement du paquet logiciel ;
  • ligne 48, on spécifie que le fichier introspection.m4 doit être distribué avec les sources du paquet ;
  • ligne 3, on charge le Makefile spécial pour lancer les outils d'introspection ;
  • lignes 5-13, on ajoute des options de configuration générales pour l'introspection, qui comprennent notamment le code à rajouter pour charger la bibliothèque ;
  • lignes 27-44, on met en place les règles de compilation du fichier .gir qui contient les informations d'introspection qui nous intéressent.
 1: DISTCHECK_CONFIGURE_FLAGS = --enable-introspection #
 2: 
 3: -include $(INTROSPECTION_MAKEFILE) #
 4: 
 5: INTROSPECTION_GIRS = #
 6: INTROSPECTION_SCANNER_ARGS = \
 7: 	--add-include-path=$(srcdir) \
 8: 	--libtool=$(top_abs_builddir)/libtool \
 9: 	--add-init-section="if (luciole_init () != 0) exit (1);" \
10: 	--c-include="luciole_glib.h" \
11: 	--include-first-in-src="luciole_glib.h" \
12: 	--accept-unprefixed \
13: 	--warn-all #
14: 
15: bin_PROGRAMS = luciole
16: lib_LTLIBRARIES = libluciole.la
17: 
18: include_HEADERS = luciole.h luciole_glib.h
19: libluciole_la_SOURCES = luciole.h luciole_glib.h luciole.c luciole_glib.c
20: libluciole_la_CPPFLAGS = @GOBJECT_CFLAGS@
21: 
22: libluciole_la_LDFLAGS = -no-undefined -version-info 1:0:1 @GOBJECT_LIBS@
23: 
24: luciole_SOURCES = main.c
25: luciole_LDADD = libluciole.la
26: 
27: if HAVE_INTROSPECTION #
28:   introspection_sources = $(libluciole_la_SOURCES)
29: 
30:   Luciole-0.1.gir: libluciole.la
31:   Luciole_0_1_gir_INCLUDES = GObject-2.0
32:   Luciole_0_1_gir_CFLAGS = $(INCLUDES) -I$(srcdir)
33:   Luciole_0_1_gir_LIBS = libluciole.la
34:   Luciole_0_1_gir_FILES = $(introspection_sources)
35:   INTROSPECTION_GIRS += Luciole-0.1.gir
36: 
37:   girdir = $(datadir)/gir-1.0
38:   gir_DATA = $(INTROSPECTION_GIRS)
39: 
40:   typelibdir = $(libdir)/girepository-1.0
41:   typelib_DATA = $(INTROSPECTION_GIRS:.gir=.typelib)
42: 
43:   CLEANFILES = $(gir_DATA) $(typelib_DATA)
44: endif #
45: 
46: ACLOCAL_AMFLAGS = -I m4
47: 
48: EXTRA_DIST = m4/introspection.m4 #

4.3 Refactoration du code source existant

Les exigences de l'introspection sont très précises. Il faut que les classes aient pour nom PrefixeNom, et les méthodes prefixe_nom_methode. Dans notre cas, on prendra Luciole comme préfixe, et T comme nom de classe principale.

À ma connaissance, il n'est pas possible de forcer la main à GObject pour prendre un préfixe vide.

4.3.1 Interface

Nous allons réécrire les commentaires de documentation pour être plus dans l'esprit Gtk-Doc. En plus, on peut mettre des annotations dans les commentaires, qui pourront aider l'introspection sur des points précis. Il y en a toute une flopée, mais nous n'en utiliserons qu'un petit sous-ensemble.

  1: #ifndef H_LUCIOLE_INCLUDED
  2: #define H_LUCIOLE_INCLUDED
  3: 
  4: #ifdef __cplusplus
  5: extern "C"
  6: {
  7: #endif /* __cplusplus */
  8: 
  9:   /**
 10:    * LucioleT:
 11:    * 
 12:    * The luciole type.  It has a name and a state (on / off).  Build
 13:    * it with luciole_t_malloc and free it with luciole_t_free.
 14:    *
 15:    * Before playing with the lucioles, you have to call
 16:    * #luciole_init().  When done, call #luciole_quit() to avoid memory
 17:    * leaks.
 18:    */
 19:   struct LucioleT;
 20:   typedef struct LucioleT LucioleT;
 21: 
 22:   /**
 23:    * luciole_init:
 24:    *
 25:    * Initializes the luciole library.  Call #luciole_quit() when done
 26:    * playing with the lucioles.
 27:    *
 28:    * Returns: 0 on success, something else on failure.
 29:    */
 30:   int luciole_init ();
 31: 
 32:   /**
 33:    * luciole_quit:
 34:    *
 35:    * Dispose of the resources.
 36:    */
 37:   void luciole_quit ();
 38: 
 39:   /**
 40:    * luciole_t_malloc: (constructor)
 41:    * @name: (in) (transfer none): the name, a constant.
 42:    *
 43:    * Allocates a new luciole.  Call #luciole_t_free() when done with it.
 44:    *
 45:    * Returns: (transfer full): a fresh luciole.
 46:    */
 47:   LucioleT *luciole_t_malloc (const char *name);
 48: 
 49:   /**
 50:    * luciole_t_free: (skip)
 51:    * @lucy: (out) (transfer none): what to destroy.
 52:    *
 53:    * Free the memory allocated by #luciole_t_malloc().  This function
 54:    * is not exported in the GObject introspection, since the memory is
 55:    * managed by the class.
 56:    */
 57:   void luciole_t_free (LucioleT *lucy);
 58: 
 59:   /**
 60:    * luciole_t_get_state:
 61:    * @lucy: (in) (transfer none)
 62:    *
 63:    * Return the state of @lucy (whether it is switched on or off).
 64:    *
 65:    * Returns: 1 if @lucy is on, 0 if it is off.
 66:    */
 67:   int luciole_t_get_state (const LucioleT *lucy);
 68: 
 69:   /**
 70:    * luciole_t_get_strstate:
 71:    * @lucy: (in) (transfer none)
 72:    *
 73:    * Return a string representing the state of @lucy.
 74:    *
 75:    * Returns: (transfer none): the representation.
 76:    */
 77:   const char *luciole_t_get_strstate (const LucioleT *lucy);
 78: 
 79:   /**
 80:    * luciole_t_get_name:
 81:    * @lucy: (in) (transfer none)
 82:    *
 83:    * Get the name of @lucy.
 84:    *
 85:    * Returns: (transfer none): the name.
 86:    */
 87:   const char *luciole_t_get_name (const LucioleT *lucy);
 88: 
 89:   /**
 90:    * luciole_t_switch_on:
 91:    * @lucy: (out) (transfer none)
 92:    *
 93:    * Switch @lucy on.
 94:    */
 95:   void luciole_t_switch_on (LucioleT *lucy);
 96: 
 97:   /**
 98:    * luciole_t_switch_off:
 99:    * @lucy: (out) (transfer none)
100:    *
101:    * Switch @lucy off.
102:    */
103:   void luciole_t_switch_off (LucioleT *lucy);
104: 
105: #ifdef __cplusplus
106: }
107: #endif /* __cplusplus */
108: 
109: #endif /* not H_LUCIOLE_INCLUDED */

4.3.2 Programme principal

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

4.3.3 Implémentation

 1: #include "luciole.h"
 2: #include <string.h>
 3: #include <stdlib.h>
 4: 
 5: struct LucioleT
 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: LucioleT *
34: luciole_t_malloc (const char *name)
35: {
36:   LucioleT *ret = NULL;
37:   ret = malloc (sizeof (LucioleT));
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_t_free (LucioleT *lucy)
53: {
54:   free (lucy->name);
55:   free (lucy);
56: }
57: 
58: int
59: luciole_t_get_state (const LucioleT *lucy)
60: {
61:   return lucy->state;
62: }
63: 
64: const char *
65: luciole_t_get_strstate (const LucioleT *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_t_get_name (const LucioleT *lucy)
81: {
82:   return lucy->name;
83: }
84: 
85: void
86: luciole_t_switch_on (LucioleT *lucy)
87: {
88:   lucy->state = 1;
89: }
90: 
91: void
92: luciole_t_switch_off (LucioleT *lucy)
93: {
94:   lucy->state = 0;
95: }

4.4 Code source supplémentaire

Il va malheureusement falloir rajouter du code source aux fichiers déjà présents. Ce sont les bindings pour notre classe de luciole ; comme celle-ci est très simple il s'agit juste de définir la fonction qui va enregistrer notre classe au moment du lancement du programme d'introspection.

 1: #ifndef H_LUCIOLE_GLIB_INCLUDED
 2: #define H_LUCIOLE_GLIB_INCLUDED
 3: 
 4: #include <glib-2.0/glib.h>
 5: #include <glib-2.0/glib-object.h>
 6: 
 7: G_BEGIN_DECLS
 8: 
 9: #include <luciole.h>
10: 
11: GType luciole_t_get_type ();
12: 
13: G_END_DECLS
14: 
15: #endif /* not H_LUCIOLE_GLIB_INCLUDED */
 1: #include "luciole_glib.h"
 2: #include <assert.h>
 3: 
 4: /* This function will copy a luciole to a new place in the heap */
 5: static gpointer
 6: copy_luciole (gpointer existing)
 7: {
 8:   LucioleT *ex = (LucioleT *) existing;
 9:   LucioleT *lucy = luciole_t_malloc (luciole_t_get_name (ex));
10:   assert (lucy != NULL);
11:   luciole_t_switch_off (lucy);
12:   if (luciole_t_get_state (existing))
13:     {
14:       luciole_t_switch_on (lucy);
15:     }
16:   return lucy;
17: }
18: 
19: GType
20: luciole_t_get_type ()
21: {
22:   static volatile gsize g_define_type_id__volatile = 0;
23:   /* Register the LucioleT type when calling the get_type () function
24:      for the first time */
25:   if (g_once_init_enter (&g_define_type_id__volatile))
26:     {
27:       GType g_define_type_id =
28: 	g_boxed_type_register_static ("LucioleT",
29: 				      copy_luciole,
30: 				      (GBoxedFreeFunc) luciole_t_free);
31:       g_once_init_leave (&g_define_type_id__volatile, g_define_type_id);
32:     }
33:   return g_define_type_id__volatile;  
34: }

5 Vérifions si tout fonctionne bien

L'ensemble des fichiers est disponible ici.

Compilons :

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

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

Et nous avons un magnifique fichier XML qui rassemble toute l'information connue.

<?xml version="1.0"?>
<!-- This file was automatically generated from C sources - DO NOT EDIT!
To affect the contents of this file, edit the original C definitions,
and/or use gtk-doc annotations.  -->
<repository version="1.2"
            xmlns="http://www.gtk.org/introspection/core/1.0"
            xmlns:c="http://www.gtk.org/introspection/c/1.0"
            xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
  <include name="GObject" version="2.0"/>
  <c:include name="luciole_glib.h"/>
  <namespace name="Luciole"
             version="0.1"
             shared-library="libluciole.so.0"
             c:identifier-prefixes="Luciole"
             c:symbol-prefixes="luciole">
    <record name="T"
            c:type="LucioleT"
            glib:type-name="LucioleT"
            glib:get-type="luciole_t_get_type"
            c:symbol-prefix="t">
      <doc xml:space="preserve">The luciole type.  It has a name and a state (on / off).  Build
it with luciole_t_malloc and free it with luciole_t_free.

Before playing with the lucioles, you have to call
#luciole_init().  When done, call #luciole_quit() to avoid memory
leaks.</doc>
      <constructor name="malloc" c:identifier="luciole_t_malloc">
        <doc xml:space="preserve">Allocates a new luciole.  Call #luciole_t_free() when done with it.</doc>
        <return-value transfer-ownership="full">
          <doc xml:space="preserve">a fresh luciole.</doc>
          <type name="T" c:type="LucioleT*"/>
        </return-value>
        <parameters>
          <parameter name="name" transfer-ownership="none">
            <doc xml:space="preserve">the name, a constant.</doc>
            <type name="utf8" c:type="const char*"/>
          </parameter>
        </parameters>
      </constructor>
      <method name="get_name" c:identifier="luciole_t_get_name">
        <doc xml:space="preserve">Get the name of @lucy.</doc>
        <return-value transfer-ownership="none">
          <doc xml:space="preserve">the name.</doc>
          <type name="utf8" c:type="const char*"/>
        </return-value>
        <parameters>
          <instance-parameter name="lucy" transfer-ownership="none">
            <type name="T" c:type="const LucioleT*"/>
          </instance-parameter>
        </parameters>
      </method>
      <method name="get_state" c:identifier="luciole_t_get_state">
        <doc xml:space="preserve">Return the state of @lucy (whether it is switched on or off).</doc>
        <return-value transfer-ownership="none">
          <doc xml:space="preserve">1 if @lucy is on, 0 if it is off.</doc>
          <type name="gint" c:type="int"/>
        </return-value>
        <parameters>
          <instance-parameter name="lucy" transfer-ownership="none">
            <type name="T" c:type="const LucioleT*"/>
          </instance-parameter>
        </parameters>
      </method>
      <method name="get_strstate" c:identifier="luciole_t_get_strstate">
        <doc xml:space="preserve">Return a string representing the state of @lucy.</doc>
        <return-value transfer-ownership="none">
          <doc xml:space="preserve">the representation.</doc>
          <type name="utf8" c:type="const char*"/>
        </return-value>
        <parameters>
          <instance-parameter name="lucy" transfer-ownership="none">
            <type name="T" c:type="const LucioleT*"/>
          </instance-parameter>
        </parameters>
      </method>
      <function name="free" c:identifier="luciole_t_free" introspectable="0">
        <doc xml:space="preserve">Free the memory allocated by #luciole_t_malloc().  This function
is not exported in the GObject introspection, since the memory is
managed by the class.</doc>
        <return-value transfer-ownership="none">
          <type name="none" c:type="void"/>
        </return-value>
        <parameters>
          <parameter name="lucy"
                     direction="out"
                     caller-allocates="1"
                     transfer-ownership="none">
            <doc xml:space="preserve">what to destroy.</doc>
            <type name="T" c:type="LucioleT*"/>
          </parameter>
        </parameters>
      </function>
      <function name="switch_off" c:identifier="luciole_t_switch_off">
        <doc xml:space="preserve">Switch @lucy off.</doc>
        <return-value transfer-ownership="none">
          <type name="none" c:type="void"/>
        </return-value>
        <parameters>
          <parameter name="lucy"
                     direction="out"
                     caller-allocates="1"
                     transfer-ownership="none">
            <type name="T" c:type="LucioleT*"/>
          </parameter>
        </parameters>
      </function>
      <function name="switch_on" c:identifier="luciole_t_switch_on">
        <doc xml:space="preserve">Switch @lucy on.</doc>
        <return-value transfer-ownership="none">
          <type name="none" c:type="void"/>
        </return-value>
        <parameters>
          <parameter name="lucy"
                     direction="out"
                     caller-allocates="1"
                     transfer-ownership="none">
            <type name="T" c:type="LucioleT*"/>
          </parameter>
        </parameters>
      </function>
    </record>
    <function name="init" c:identifier="luciole_init">
      <doc xml:space="preserve">Initializes the luciole library.  Call #luciole_quit() when done
playing with the lucioles.</doc>
      <return-value transfer-ownership="none">
        <doc xml:space="preserve">0 on success, something else on failure.</doc>
        <type name="gint" c:type="int"/>
      </return-value>
    </function>
    <function name="quit" c:identifier="luciole_quit">
      <doc xml:space="preserve">Dispose of the resources.</doc>
      <return-value transfer-ownership="none">
        <type name="none" c:type="void"/>
      </return-value>
    </function>
    <function name="t_free"
              c:identifier="luciole_t_free"
              moved-to="T.free"
              introspectable="0">
      <doc xml:space="preserve">Free the memory allocated by #luciole_t_malloc().  This function
is not exported in the GObject introspection, since the memory is
managed by the class.</doc>
      <return-value transfer-ownership="none">
        <type name="none" c:type="void"/>
      </return-value>
      <parameters>
        <parameter name="lucy"
                   direction="out"
                   caller-allocates="1"
                   transfer-ownership="none">
          <doc xml:space="preserve">what to destroy.</doc>
          <type name="T" c:type="LucioleT*"/>
        </parameter>
      </parameters>
    </function>
    <function name="t_switch_off"
              c:identifier="luciole_t_switch_off"
              moved-to="T.switch_off">
      <doc xml:space="preserve">Switch @lucy off.</doc>
      <return-value transfer-ownership="none">
        <type name="none" c:type="void"/>
      </return-value>
      <parameters>
        <parameter name="lucy"
                   direction="out"
                   caller-allocates="1"
                   transfer-ownership="none">
          <type name="T" c:type="LucioleT*"/>
        </parameter>
      </parameters>
    </function>
    <function name="t_switch_on"
              c:identifier="luciole_t_switch_on"
              moved-to="T.switch_on">
      <doc xml:space="preserve">Switch @lucy on.</doc>
      <return-value transfer-ownership="none">
        <type name="none" c:type="void"/>
      </return-value>
      <parameters>
        <parameter name="lucy"
                   direction="out"
                   caller-allocates="1"
                   transfer-ownership="none">
          <type name="T" c:type="LucioleT*"/>
        </parameter>
      </parameters>
    </function>
  </namespace>
</repository>

Comment ça, on s'en moque ?

6 Conclusion

Nous avons un nouveau paquet logiciel ici, avec l'introspection GObject. Le code source utilisateur est disponible ici. Malheureusement, la compilation croisée est cassée : on ne peut pas faire de l'introspection si on ne peut pas lancer les exécutables produits ! Heureusement, nous l'avons réparé par la suite.

Il nous reste à savoir ce qu'on va bien pouvoir faire de ce joli fichier XML…