web-dev-qa-db-fra.com

Plusieurs instances de singleton dans des bibliothèques partagées sous Linux

Ma question, comme le titre l'indique, est évidente et je décris le scénario en détail. Il existe une classe nommée singleton implémentée par singleton pattern comme suit, dans le fichier singleton.h:

/*
 * singleton.h
 *
 *  Created on: 2011-12-24
 *      Author: bourneli
 */

#ifndef SINGLETON_H_
#define SINGLETON_H_

class singleton
{
private:
    singleton() {num = -1;}
    static singleton* pInstance;
public:
    static singleton& instance()
    {
        if (NULL == pInstance)
        {
            pInstance = new singleton();
        }
        return *pInstance;
    }
public:
    int num;
};

singleton* singleton::pInstance = NULL;

#endif /* SINGLETON_H_ */

ensuite, il existe un plugin appelé hello.cpp comme suit:

#include <iostream>
#include "singleton.h"

extern "C" void hello() {
    std::cout << "singleton.num in hello.so : " << singleton::instance().num << std::endl;
    ++singleton::instance().num;
    std::cout << "singleton.num in hello.so after ++ : " << singleton::instance().num << std::endl;
}

vous pouvez voir que le plugin appelle le singleton et change l'attribut num dans le singleton.

enfin, il existe une fonction principale qui utilise le singleton et le plugin comme suit:

#include <iostream>
#include <dlfcn.h>
#include "singleton.h"

int main() {
    using std::cout;
    using std::cerr;
    using std::endl;

    singleton::instance().num = 100; // call singleton
    cout << "singleton.num in main : " << singleton::instance().num << endl;// call singleton

    // open the library
    void* handle = dlopen("./hello.so", RTLD_LAZY);

    if (!handle) {
        cerr << "Cannot open library: " << dlerror() << '\n';
        return 1;
    }

    // load the symbol
    typedef void (*hello_t)();

    // reset errors
    dlerror();
    hello_t hello = (hello_t) dlsym(handle, "hello");
    const char *dlsym_error = dlerror();
    if (dlsym_error) {
        cerr << "Cannot load symbol 'hello': " << dlerror() << '\n';
        dlclose(handle);
        return 1;
    }

    hello(); // call plugin function hello

    cout << "singleton.num in main : " << singleton::instance().num << endl;// call singleton
    dlclose(handle);
}

et le makefile est le suivant:

example1: main.cpp hello.so
    $(CXX) $(CXXFLAGS)  -o example1 main.cpp -ldl

hello.so: hello.cpp
    $(CXX) $(CXXFLAGS)  -shared -o hello.so hello.cpp

clean:
    rm -f example1 hello.so

.PHONY: clean

alors, quelle est la sortie? Je pensais qu'il y avait:

singleton.num in main : 100
singleton.num in hello.so : 100
singleton.num in hello.so after ++ : 101
singleton.num in main : 101

cependant, la sortie réelle est la suivante:

singleton.num in main : 100
singleton.num in hello.so : -1
singleton.num in hello.so after ++ : 0
singleton.num in main : 100

Cela prouve qu'il existe deux instances de la classe singleton.

Pourquoi?

35
bourneli

Tout d'abord, vous devez généralement utiliser l'indicateur -fPIC lors de la création de bibliothèques partagées.

Ne l'utilisez pas "fonctionne" sous Linux 32 bits, mais échouera sous 64 bits avec une erreur similaire à:

/usr/bin/ld: /tmp/ccUUrz9c.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC

Deuxièmement, votre programme fonctionnera comme prévu après avoir ajouté -rdynamic à la ligne de liaison pour le fichier exécutable principal:

singleton.num in main : 100
singleton.num in hello.so : 100
singleton.num in hello.so after ++ : 101
singleton.num in main : 101

Afin de comprendre pourquoi -rdynamic est requis, vous devez connaître la façon dont l’éditeur de liens dynamique résout les symboles et la table dynamic symbol.

Commençons par examiner la table des symboles dynamiques pour hello.so:

$ nm -C -D hello.so | grep singleton
0000000000000b8c W singleton::instance()
0000000000201068 B singleton::pInstance
0000000000000b78 W singleton::singleton()

Cela nous indique qu'il existe deux définitions de fonction faibles et une variable globale singleton::pInstance visibles par l'éditeur de liens dynamique.

Examinons maintenant la table des symboles statiques et dynamiques pour le example1 d'origine (lié sans -rdynamic):

$ nm -C  example1 | grep singleton
0000000000400d0f t global constructors keyed to singleton::pInstance
0000000000400d38 W singleton::instance()
00000000006022e0 B singleton::pInstance
0000000000400d24 W singleton::singleton()

$ nm -C -D example1 | grep singleton
$ 

C’est vrai: même si le singleton::pInstance est présent dans l’exécutable en tant que variable globale, ce symbole n’est pas présent dans la table dynamic symbol et est donc "invisible" pour l’éditeur de liens dynamique.

Parce que l'éditeur de liens dynamique "ne sait pas" que example1 contient déjà une définition de singleton::pInstance, il ne lie pas cette variable à l'intérieur de hello.so à la définition existante (ce que vous voulez réellement).

Lorsque nous ajoutons -rdynamic à la ligne de lien:

$ nm -C  example1-rdynamic | grep singleton
0000000000400fdf t global constructors keyed to singleton::pInstance
0000000000401008 W singleton::instance()
00000000006022e0 B singleton::pInstance
0000000000400ff4 W singleton::singleton()

$ nm -C -D  example1-rdynamic | grep singleton
0000000000401008 W singleton::instance()
00000000006022e0 B singleton::pInstance
0000000000400ff4 W singleton::singleton()

Désormais, la définition de singleton::pInstance à l'intérieur du fichier exécutable principal est visible pour l'éditeur de liens dynamique. Elle sera donc "réutilisée" lors du chargement de hello.so:

LD_DEBUG=bindings ./example1-rdynamic |& grep pInstance
     31972: binding file ./hello.so [0] to ./example1-rdynamic [0]: normal symbol `_ZN9singleton9pInstanceE'
53
Employed Russian

Vous devez faire attention lorsque vous utilisez des bibliothèques partagées chargées à l'exécution. Une telle construction ne fait pas strictement partie de la norme C++ et vous devez examiner avec soin la sémantique d'une telle procédure.

Premièrement, la bibliothèque partagée voit sa propre variable globale séparée singleton::pInstance. Pourquoi donc? Une bibliothèque chargée au moment de l'exécution est essentiellement un programme distinct et indépendant qui n'a tout simplement pas de point d'entrée. Mais tout le reste est vraiment comme un programme séparé, et le chargeur dynamique le traitera comme ça, par exemple. initialiser les variables globales, etc.

Le chargeur dynamique est une installation d'exécution qui n'a rien à voir avec le chargeur statique. Le chargeur statique fait partie de l'implémentation standard C++ et résout tous les symboles du programme principal avant le programme principal démarre. D'autre part, le chargeur dynamique n'exécute que après le programme principal a déjà démarré. En particulier, tous les symboles du programme principal doivent déjà être résolus! Il existe simplement non moyen de remplacer automatiquement les symboles du programme principal de manière dynamique. Les programmes natifs ne sont en aucun cas "gérés" de manière à permettre la création de liens systématiques. (Peut-être que quelque chose peut être piraté, mais pas de manière systématique et portable.)

La vraie question est donc de savoir comment résoudre le problème de conception que vous tentez. La solution ici est de transmettre les descripteurs de toutes les variables globales aux fonctions du plug-in. Faites en sorte que votre programme principal définisse la copie originale (et unique) de la variable globale et initialise votre bibliothèque avec un pointeur sur celle-ci.

Par exemple, votre bibliothèque partagée pourrait ressembler à ceci. Tout d'abord, ajoutez un pointeur à pointeur à la classe singleton:

class singleton
{
    static singleton * pInstance;
public:
    static singleton ** ppinstance;
    // ...
};

singleton ** singleton::ppInstance(&singleton::pInstance);

Maintenant, utilisez *ppInstance au lieu de pInstance partout.

Dans le plugin, configurez le singleton sur le pointeur du programme principal:

void init(singleton ** p)
{
    singleton::ppInsance = p;
}

Et la fonction principale, appelez l’initialisation du plugin:

init_fn init;
hello_fn hello;
*reinterpret_cast<void**>(&init) = dlsym(lib, "init");
*reinterpret_cast<void**>(&hello) = dlsym(lib, "hello");

init(singleton::ppInstance);
hello();

Maintenant, le plugin partage le même pointeur sur l'instance singleton que le reste du programme.

5
Kerrek SB

Je pense que la réponse simple est la suivante: http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html

Quand vous avez une variable statique, elle est stockée dans l'objet (.o, .a et/ou .so)

Si le dernier objet à exécuter contient deux versions de l'objet, le comportement est inattendu, comme par exemple l'appel du destructeur d'un objet Singleton.

Utiliser la conception appropriée, telle que déclarer le membre statique dans le fichier principal, utiliser -rdynamic/fpic et utiliser les directives "" du compilateur, fera l'affaire pour vous.

Exemple d'instruction makefile: 

$ g++ -rdynamic -o appexe $(OBJ) $(LINKFLAGS) -Wl,--whole-archive -L./Singleton/ -lsingleton -Wl,--no-whole-archive $(LIBS) 

J'espère que ça marche!

2
Paulo Lellis

Merci à tous pour vos réponses!

Pour le suivi de Linux, vous pouvez également utiliser RTLD_GLOBAL avec dlopen(...), par man dlopen (et les exemples dont il dispose). J'ai créé une variante de l'exemple de l'OP dans ce répertoire: github tree Exemple de sortie: output.txt

Rapide et sale:

  • Si vous ne souhaitez pas lier manuellement chaque symbole à votre main, conservez les objets partagés. (par exemple, si vous avez créé des objets *.so à importer dans Python)
  • Vous pouvez initialement charger dans la table de symboles globale ou effectuer une réouverture NOLOAD + GLOBAL.

Code:

#if MODE == 1
// Add to static symbol table.
#include "producer.h"
#endif
...
    #if MODE == 0 || MODE == 1
        handle = dlopen(lib, RTLD_LAZY);
    #Elif MODE == 2
        handle = dlopen(lib, RTLD_LAZY | RTLD_GLOBAL);
    #Elif MODE == 3
        handle = dlopen(lib, RTLD_LAZY);
        handle = dlopen(lib, RTLD_LAZY | RTLD_NOLOAD | RTLD_GLOBAL);
    #endif

Modes:

  • Mode 0: chargement paresseux nominal (ne fonctionnera pas)
  • Mode 1: Inclure le fichier à ajouter à la table des symboles statiques.
  • Mode 2: chargez initialement avec RTLD_GLOBAL
  • Mode 3: Recharger avec RTLD_NOLOAD | RTLD_GLOBAL
0
Eric Cousineau