Hello,

Il y a beaucoup de choses que l’on apprend pas du tout dans les cours de programmation, et même en suivant une haute école pour une formation en tant qu’ingénieur. Heureusement les logiciels libres permettent de côtoyer des spécialistes qui ont une expérience pratique du logiciel, contrairement à de nombreux professeurs qui ne connaissent que la théorie mais ne semblent pas vraiment pratiquer et passent ainsi à côté de potentiels problèmes importants. Je pense tout particulièrement au C dans le contexte des exécutables ELF utilisés par les OS Unix-like. Bien que beaucoup d’écoles restent encore trop rattachées sur les technologies Microsoft, dans le monde des systèmes embarqués GNU/Linux est particulièrement présent. Néanmoins la formation sur le langage C n’est pas toujours très bonne. Une erreur trop courante concerne les espaces de nom utilisés par les bibliothèques.

Les fonctions “static”

Quand j’étudiais le C++ à l’école d’ingénieur on devait travailler avec un outil que je déteste absolument, c’est Rhapsody. Un générateur de code à partir d’UML. L’UML c’est très bien pour poser les idées et réfléchir sérieusement sur un programme. Mais le fait qu’un langage de programmation ne soit pas objet ou orienté-objet n’empêche pas de se donner des règles reposants sur les concepts objets. Bref, Rhapsody c’est pire que tout, il génère du code (C, C++ ou autre) à partir d’UML. Finalement le programmeur fait du dessin, des carrés, des ronds et les relie avec des flèches. Il presse un bouton et des centaines de lignes de code se génèrent. Wouhaw.. moi qui aime programmer.. Bientôt on devra se former en tant que graphiste avant de réaliser un logiciel :-).

OK vous l’aurez compris, je déteste ça. Mais là où je veux en venir c’est à une petite anecdote. On travaillait donc sur ce logiciel et un des objet était instancié une seule fois (donc en tant que Singleton. Alors je demande à l’assistant qui était présent avec nous pour répondre à nos questions :

Qu’est-ce que c’est concrètement un Singleton ?

Il s’assied et m’explique ce que je sais déjà, c’est à dire que l’objet est instancié qu’une seule fois. Il me montrait via l’interface de Rhapsody comment on paramètre la classe pour être un Singleton. Et me ré-expliquait toujours la même chose. J’essayais de reformuler ma question en vain. C’est finalement en allant observer le code C généré que j’ai vu l’objet instancié en tant que variable globale statique.

5 secondes pour lire le code et comprendre, 15 minutes d’explication dans le monde “merveilleux” de Rhapsody et des formes géométriques.

Mais revenons à nos fonctions statiques

Une chose que tout le monde apprend aux cours c’est qu’une variable déclarée comme étant “static” dans une fonction, garde sa valeur entre chaque appel de fonction. Par contre je n’ai jamais eu un seul professeur qui nous ait expliqué à quoi sert une variable globale statique, ou alors une fonction statique (sauf les fonctions statiques en C++, mais ici je fais référence au C).

En fait, il est très rare de voir un professeur proposer de créer une bibliothèque pendant un cours. En principe on créer des exécutables avec un main. En C je n’ai jamais vu (en dehors des logiciels libres et des professionnels), des fonctions déclarées comme étant statiques. Que se sois du code créé par des professeurs ou par des étudiants. Pourtant l’écriture de bibliothèques demandent quelques considérations supplémentaires qui concernent les espaces de nom et plus particulièrement les ELF.

Mais tout d’abord une fonction “static” est une fonction qui n’est pas “extern” (qui est le qualificateur de type par défaut). En étant “static” la visibilité de la fonction est restreinte au fichier source où elle a été déclarée. Une fonction externe peut être atteignable depuis n’importe où dans les sources. Mais dans le cas des ELF c’est encore plus subtile que ça car elle peut être atteinte depuis n’importe quelles bibliothèques ou applications étant liées avec elle.

Les Shared Object (so) et les DLL

Sans partir dans des explications compliquées et inutiles il faut bien faire la différence entre une DLL (de chez Microsoft) et un Shared Object (qui vient du monde Unix). Les développeurs pour Windows connaissent bien :

__declspec(dllexport)

Qui est un qualificateur de type inventé par Microsoft. Il permet de spécifier les fonctions qui doivent être exportées par la DLL. Vous pouvez aussi utiliser un fichier .def qui donne la liste des fonctions à exporter. Ce principe existe aussi avec les gcc >=4 mais est rarement utilisé à ma connaissance. Quoi qu’il en soit dans tous les cas vous avez une liste de symboles exportés.

La différence qui m’intéresse c’est au runtime, lorsque le lanceur d’application doit charger les bibliothèques. Dans le cas d’une DLL, le programme va rechercher les pointeurs sur les fonctions désirées à l’aide de LoadLibrary. C’est lourd mais ça fonctionne. Dans le cas d’un “so” le fonctionnement est très différent. Le lanceur de programme de Linux va charger les bibliothèques les unes après les autres dans l’ordre où elles ont été liées. Lorsque le programme a besoin d’une fonction, c’est la première occurrence trouvée qui sera utilisée.

Concrètement

Imaginons un programme qui utilise libplayer et libvalhalla. Si je n’étais pas conscient du problème que je viens d’expliquer, dans ces deux bibliothèques j’aurais pu écrire une fonction qui a exactement le même nom (de part et d’autre) comme par exemple :

libplayer :

void foobar (int a, int b);

libvalhalla :

void foobar (int c);

Ces fonctions ne sont pas static car bien entendu j’aimerais les utiliser partout dans les projets. Alors que se passe-t-il lorsqu’on lie l’application sur ces deux bibliothèques ? Notez que ces deux fonctions ne sont pas non plus déclarées dans les en-têtes “publiques” que vous distribuez à vos développeurs. Par exemple vous donnez ceci à un ami :

player.so (que vous avez compilé vous même)

Et un fichier d’en-tête

player.h

/* libplayer header */
void libplayer_is_the_best (void);

De même avec la seconde bibliothèque :

valhalla.so

valhalla.h

/* libvalhalla header */
void libvalhalla_is_the_best (void);

Maintenant vous avez créé une application tel que :

#include <player.h>
#include <valhalla.h>

int
main (void)
{
  libplayer_is_the_best ();
  libvalhalla_is_the_best ();
  return 0;
}

Dans libplayer.so il y a le symbole “foobar”, mais il existe également dans la bibliothèque libvalhalla.so. Lorsque vous liez votre application vous n’avez aucune erreur. Vous utilisez deux fonctions considérées comme publiques et qui n’ont pas du tout le même nom. Alors où est le problème? Et bien c’est très simple. Quand vous liez votre programme vous devez passer les noms des bibliothèques. Par exemple:

cas 1 : gcc -lplayer -lvalhalla main.c -o main

Mais vous auriez aussi pu faire

cas 2 : gcc -lvalhalla -lplayer main.c -o main

Les deux façons sont correctes mais le comportement de l’application main n’est pas du tout le même. Les deux bibliothèques utilisent la fonction foobar. Cette fonction n’a pas le même nombre d’arguments dans libvalhalla que dans libplayer et leurs comportements sont différents. Les fonctions libplayer_is_the_best et libvalhalla_is_the_best utilisent “en théorie” leurs propres fonction foobar. Et bien en réalité ce n’est pas le cas.

Dans le cas 1, le chargeur de programme va commencer par player.so, puis par valhalla.so. Lorsque la fonction libplayer_is_the_best va utiliser foobar, alors le foobar de player.so va être utilisé. Mais lorsque valhalla.so va utiliser foobar, c’est aussi le foobar de player.so qui sera utilisé (aïe). Dans le cas 2 c’est le même principe mais inversé. Les conséquences peuvent être très imprévisibles.

Si vous avez une application qui est liée à des dizaines de bibliothèques il faut espérer que tout le monde ait pris la peine de faire deux choses importantes :

  1. Utilisez toujours un espace de nom pour vos fonctions. Par exemple libvalhalla en utilise trois (libvalhalla_, valhalla_ et vh_). Un espace de nom en C ce n’est rien d’autre qu’un nom identique que vous concaténez au début des noms. Par exemple nos foobar auraient pût se nommer libplayer_foobar et libvalhalla_foobar, ce qui aurait évité la collision.

  2. Déclarez toujours en static toutes les fonctions qui ne sont pas utilisées en dehors du fichier source. Une fonction static n’a pas besoin d’espace de nom, car elle n’est jamais exportée! Et faire ainsi permet d’aider le compilateur à effectuer de meilleurs optimisations.

Comment détecter et debugger les collisions

Une des solution c’est de compiler votre programme entièrement en static. Il doit donc être lié aux .a de toutes les bibliothèques. Dans ce cas de figure, une collision sera forcément détectée par le linker.

Pour debugger commencez par tout compiler en -O0 -g3, puis utilisez valgrind. Vous arriverez à remonter sur l’appel de fonction qui s’est fait de la libA à la libB. Vous pourriez voir le foobar de player.so appelé par valhalla.so.

En pratique…

Il y a de nombreux mois, j’ai eu des problèmes de ce type avec libVLC et libplayer, car les fonctions de getopt étaient exportées par libVLC bien que c’était uniquement pour son propre usage. Ça me provoquait des collisions avec le getopt que j’utilise dans libplayer-test. J’ai bien entendu rapporté le problème qui a été corrigé.

J’ai aussi eu un cas avec GeeXboX et le projet GuPNP. Le développeur principal a aussi été prévenu mais a priori il s’en fiche (ce n’est pas moi qui l’a contacté mais un autre du team). Du coup ce n’est pas possible de lier en static si on utilise deux de ses libs car elles ont les même fonctions non-static pour traiter le XML. Et le pire c’est que le nom de ces fonctions n’a pas d’espace de nom très original. Notez qu’en dynamique il n’y a jamais de problème car heureusement dans les deux bibliothèques, les fonctions sont les mêmes.

Bref, en fouillant bien on doit trouver ce genre d’exemple un peu partout…

J’espère que ce poste vous sera utile pour vos propres développements.

Bon code et à bientôt!