Bibliothèque partagée dynamique C ++ sous Linux

167

Il s'agit d'un suivi de la compilation Dynamic Shared Library avec g ++ .

J'essaye de créer une bibliothèque de classes partagée en C ++ sur Linux. Je suis capable de faire compiler la bibliothèque, et je peux appeler certaines des fonctions (hors classe) en utilisant les didacticiels que j'ai trouvés ici et ici . Mes problèmes commencent lorsque j'essaye d'utiliser les classes définies dans la bibliothèque. Le deuxième tutoriel auquel j'ai lié montre comment charger les symboles pour créer des objets des classes définies dans la bibliothèque, mais s'arrête avant d' utiliser ces objets pour effectuer le travail.

Quelqu'un connaît-il un didacticiel plus complet pour créer des bibliothèques de classes C ++ partagées qui montre également comment utiliser ces classes dans un exécutable distinct? Un didacticiel très simple qui montre la création d'objets, leur utilisation (de simples getters et setters conviendraient) et leur suppression serait fantastique. Un lien ou une référence à un code open source qui illustre l'utilisation d'une bibliothèque de classes partagée serait tout aussi bien.


Bien que les réponses de codelogic et nimrodm fonctionnent, je voulais juste ajouter que j'ai pris une copie de Beginning Linux Programming depuis que j'ai posé cette question, et son premier chapitre contient un exemple de code C et de bonnes explications pour créer et utiliser des bibliothèques statiques et partagées. . Ces exemples sont disponibles via Google Recherche de Livres dans une ancienne édition de ce livre .

Bill le lézard
la source
Je ne suis pas sûr de comprendre ce que vous entendez par "l'utiliser", une fois qu'un pointeur vers l'objet est renvoyé, vous pouvez l'utiliser comme vous utilisez n'importe quel autre pointeur vers un objet.
codelogic
L'article auquel j'ai lié montre comment créer un pointeur de fonction vers une fonction de fabrique d'objets à l'aide de dlsym. Il ne montre pas la syntaxe pour créer et utiliser des objets de la bibliothèque.
Bill the Lizard
1
Vous aurez besoin du fichier d'en-tête décrivant la classe. Pourquoi pensez-vous que vous devez utiliser "dlsym" au lieu de laisser simplement le système d'exploitation trouver et lier la bibliothèque au moment du chargement? Faites-moi savoir si vous avez besoin d'un exemple simple.
nimrodm le
3
@nimrodm: Quelle est l'alternative à l'utilisation de "dlsym"? Je suis (censé être) en train d'écrire 3 programmes C ++ qui utiliseront tous les classes définies dans la bibliothèque partagée. J'ai aussi 1 script Perl qui l'utilisera, mais c'est un tout autre problème pour la semaine prochaine.
Bill the Lizard

Réponses:

154

myclass.h

#ifndef __MYCLASS_H__
#define __MYCLASS_H__

class MyClass
{
public:
  MyClass();

  /* use virtual otherwise linker will try to perform static linkage */
  virtual void DoSomething();

private:
  int x;
};

#endif

myclass.cc

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

using namespace std;

extern "C" MyClass* create_object()
{
  return new MyClass;
}

extern "C" void destroy_object( MyClass* object )
{
  delete object;
}

MyClass::MyClass()
{
  x = 20;
}

void MyClass::DoSomething()
{
  cout<<x<<endl;
}

class_user.cc

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

using namespace std;

int main(int argc, char **argv)
{
  /* on Linux, use "./myclass.so" */
  void* handle = dlopen("myclass.so", RTLD_LAZY);

  MyClass* (*create)();
  void (*destroy)(MyClass*);

  create = (MyClass* (*)())dlsym(handle, "create_object");
  destroy = (void (*)(MyClass*))dlsym(handle, "destroy_object");

  MyClass* myClass = (MyClass*)create();
  myClass->DoSomething();
  destroy( myClass );
}

Sous Mac OS X, compilez avec:

g++ -dynamiclib -flat_namespace myclass.cc -o myclass.so
g++ class_user.cc -o class_user

Sous Linux, compilez avec:

g++ -fPIC -shared myclass.cc -o myclass.so
g++ class_user.cc -ldl -o class_user

S'il s'agissait d'un système de plugins, vous utiliseriez MyClass comme classe de base et définiriez toutes les fonctions requises virtuelles. L'auteur du plugin dériverait alors de MyClass, remplacerait les virtuels et implémenterait create_objectet destroy_object. Votre application principale n'aurait pas besoin d'être modifiée en aucune façon.

codelogic
la source
6
Je suis en train d'essayer ceci, mais j'ai juste une question. Est-il strictement nécessaire d'utiliser void *, ou la fonction create_object pourrait-elle renvoyer MyClass * à la place? Je ne vous demande pas de changer cela pour moi, j'aimerais juste savoir s'il y a une raison d'utiliser l'un sur l'autre.
Bill the Lizard
1
Merci, j'ai essayé cela et cela a fonctionné tel quel sur Linux à partir de la ligne de commande (une fois que j'ai fait le changement que vous avez suggéré dans les commentaires du code). J'apprécie votre temps.
Bill the Lizard le
1
Y a-t-il une raison pour laquelle vous les déclareriez avec extern "C"? Comme cela est compilé à l'aide d'un compilateur g ++. Pourquoi voudriez-vous utiliser la convention de dénomination c? C ne peut pas appeler c ++. Une interface wrapper écrite en c ++ est le seul moyen d'appeler cela à partir de c.
ant2009
6
@ ant2009 vous en avez besoin extern "C"car la dlsymfonction est une fonction C. Et pour charger dynamiquement la create_objectfonction, elle utilisera une liaison de style C. Si vous n'utilisez pas le extern "C", il n'y aurait aucun moyen de connaître le nom de la create_objectfonction dans le fichier .so, à cause de la modification des noms dans le compilateur C ++.
kokx
1
Belle méthode, c'est très similaire à ce que quelqu'un ferait sur un compilateur Microsoft. avec un peu de travail #if #else, vous pouvez obtenir un joli système indépendant de la plate-forme
Ha11owed
52

Ce qui suit montre un exemple de bibliothèque de classes partagée shared. [H, cpp] et un module main.cpp utilisant la bibliothèque. C'est un exemple très simple et le makefile pourrait être amélioré. Mais cela fonctionne et peut vous aider:

shared.h définit la classe:

class myclass {
   int myx;

  public:

    myclass() { myx=0; }
    void setx(int newx);
    int  getx();
};

shared.cpp définit les fonctions getx / setx:

#include "shared.h"

void myclass::setx(int newx) { myx = newx; }
int  myclass::getx() { return myx; }

main.cpp utilise la classe,

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

using namespace std;

int main(int argc, char *argv[])
{
  myclass m;

  cout << m.getx() << endl;
  m.setx(10);
  cout << m.getx() << endl;
}

et le makefile qui génère libshared.so et relie main à la bibliothèque partagée:

main: libshared.so main.o
    $(CXX) -o main  main.o -L. -lshared

libshared.so: shared.cpp
    $(CXX) -fPIC -c shared.cpp -o shared.o
    $(CXX) -shared  -Wl,-soname,libshared.so -o libshared.so shared.o

clean:
    $rm *.o *.so

Pour exécuter «main» et créer un lien avec libshared.so, vous devrez probablement spécifier le chemin de chargement (ou le mettre dans / usr / local / lib ou similaire).

Ce qui suit spécifie le répertoire actuel comme chemin de recherche des bibliothèques et exécute main (syntaxe bash):

export LD_LIBRARY_PATH=.
./main

Pour voir que le programme est lié à libshared.so, vous pouvez essayer ldd:

LD_LIBRARY_PATH=. ldd main

Imprime sur ma machine:

  ~/prj/test/shared$ LD_LIBRARY_PATH=. ldd main
    linux-gate.so.1 =>  (0xb7f88000)
    libshared.so => ./libshared.so (0xb7f85000)
    libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xb7e74000)
    libm.so.6 => /lib/libm.so.6 (0xb7e4e000)
    libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0xb7e41000)
    libc.so.6 => /lib/libc.so.6 (0xb7cfa000)
    /lib/ld-linux.so.2 (0xb7f89000)
nimrodm
la source
1
Cela semble (à mon œil très inexpérimenté) relier statiquement libshared.so à votre exécutable, plutôt que d'utiliser une liaison dynamique au moment de l'exécution. Ai-je raison?
Bill the Lizard
10
Non. Il s'agit d'un lien dynamique Unix (Linux) standard. Une bibliothèque dynamique a l'extension ".so" (objet partagé) et est liée à l'exécutable (main dans ce cas) au moment du chargement - chaque fois que main est chargé. La liaison statique se produit au moment de la liaison et utilise des bibliothèques avec l'extension ".a" (archive).
nimrodm
9
Ceci est lié dynamiquement au moment de la construction . En d'autres termes, vous devez avoir une connaissance préalable de la bibliothèque avec laquelle vous liez (par exemple, créer un lien avec «dl» pour dlopen). Ceci est différent du chargement dynamique d' une bibliothèque, basé, par exemple, sur un nom de fichier spécifié par l'utilisateur, où une connaissance préalable n'est pas nécessaire.
codelogic
10
Ce que j'essayais d'expliquer (mal), c'est que dans ce cas, vous devez connaître le nom de la bibliothèque au moment de la construction (vous devez passer -lshared à gcc). Habituellement, on utilise dlopen () lorsque cette information n'est pas disponible, c'est-à-dire que le nom de la bibliothèque est découvert au moment de l'exécution (ex: énumération du plugin).
codelogic
3
Utilisez-le -L. -lshared -Wl,-rpath=$$(ORIGIN)lors de la liaison et déposez-le LD_LIBRARY_PATH=..
Maxim Egorushkin
9

Fondamentalement, vous devez inclure le fichier d'en-tête de la classe dans le code où vous souhaitez utiliser la classe dans la bibliothèque partagée. Ensuite, lorsque vous créez un lien, utilisez l'indicateur «-l» pour lier votre code à la bibliothèque partagée. Bien sûr, cela nécessite que le .so soit là où le système d'exploitation peut le trouver. Voir 3.5. Installation et utilisation d'une bibliothèque partagée

L'utilisation de dlsym est utilisée lorsque vous ne savez pas au moment de la compilation quelle bibliothèque vous souhaitez utiliser. Cela ne semble pas être le cas ici. Peut-être que la confusion est que Windows appelle les bibliothèques chargées dynamiquement que vous fassiez la liaison à la compilation ou à l'exécution (avec des méthodes analogues)? Si tel est le cas, vous pouvez considérer dlsym comme l'équivalent de LoadLibrary.

Si vous avez vraiment besoin de charger dynamiquement les bibliothèques (c'est-à-dire, ce sont des plug-ins), alors cette FAQ devrait vous aider.

Matt Lewis
la source
1
La raison pour laquelle j'ai besoin d'une bibliothèque partagée dynamique est que je l'appellerai également à partir du code Perl. C'est peut-être une idée fausse de ma part que je doive également l'appeler dynamiquement à partir d'autres programmes C ++ que je développe.
Bill the Lizard
Je n'ai jamais essayé intégré perl et C ++, mais je pense que vous devez utiliser XS: johnkeiser.com/perl-xs-c++.html
Matt Lewis
5

En plus des réponses précédentes, j'aimerais attirer l'attention sur le fait que vous devriez utiliser l' idiome RAII (Resource Acquisition Is Initialisation) pour être sûr de la destruction des gestionnaires.

Voici un exemple de travail complet:

Déclaration d'interface Interface.hpp::

class Base {
public:
    virtual ~Base() {}
    virtual void foo() const = 0;
};

using Base_creator_t = Base *(*)();

Contenu de la bibliothèque partagée:

#include "Interface.hpp"

class Derived: public Base {
public:
    void foo() const override {}
};

extern "C" {
Base * create() {
    return new Derived;
}
}

Gestionnaire de bibliothèque partagée dynamique Derived_factory.hpp::

#include "Interface.hpp"
#include <dlfcn.h>

class Derived_factory {
public:
    Derived_factory() {
        handler = dlopen("libderived.so", RTLD_NOW);
        if (! handler) {
            throw std::runtime_error(dlerror());
        }
        Reset_dlerror();
        creator = reinterpret_cast<Base_creator_t>(dlsym(handler, "create"));
        Check_dlerror();
    }

    std::unique_ptr<Base> create() const {
        return std::unique_ptr<Base>(creator());
    }

    ~Derived_factory() {
        if (handler) {
            dlclose(handler);
        }
    }

private:
    void * handler = nullptr;
    Base_creator_t creator = nullptr;

    static void Reset_dlerror() {
        dlerror();
    }

    static void Check_dlerror() {
        const char * dlsym_error = dlerror();
        if (dlsym_error) {
            throw std::runtime_error(dlsym_error);
        }
    }
};

Code client:

#include "Derived_factory.hpp"

{
    Derived_factory factory;
    std::unique_ptr<Base> base = factory.create();
    base->foo();
}

Remarque:

  • J'ai tout mis dans des fichiers d'en-tête pour plus de concision. Dans la vraie vie, vous devez bien sûr diviser votre code entre .hppet .cppfichiers.
  • Pour simplifier, j'ai ignoré le cas où vous souhaitez gérer une surcharge new/ delete.

Deux articles clairs pour obtenir plus de détails:

Xavier Lamorlette
la source
C'est un excellent exemple. RAII est définitivement la voie à suivre.
David Steinhauer