Dynamiczna biblioteka współdzielona C ++ w systemie Linux

167

To jest kontynuacja kompilacji Dynamic Shared Library z g ++ .

Próbuję utworzyć współdzieloną bibliotekę klas w C ++ w systemie Linux. Jestem w stanie skompilować bibliotekę i mogę wywołać niektóre (nieklasowe) funkcje, korzystając z samouczków, które znalazłem tutaj i tutaj . Moje problemy zaczynają się, gdy próbuję użyć klas zdefiniowanych w bibliotece. Drugi samouczek, do którego dołączyłem, pokazuje, jak załadować symbole do tworzenia obiektów klas zdefiniowanych w bibliotece, ale zatrzymuje się na krótko przed użyciem tych obiektów do wykonania jakiejkolwiek pracy.

Czy ktoś zna bardziej kompletny samouczek dotyczący tworzenia współdzielonych bibliotek klas C ++, który pokazuje również, jak używać tych klas w oddzielnym pliku wykonywalnym? Bardzo prosty samouczek pokazujący tworzenie i używanie obiektów (proste metody pobierające i ustawiające byłyby w porządku) i usuwanie byłoby fantastyczne. Równie dobry byłby odsyłacz lub odniesienie do jakiegoś kodu open source, który ilustruje użycie współdzielonej biblioteki klas.


Chociaż odpowiedzi z codelogic i nimrodm działają, chciałem tylko dodać, że wybrałem kopię Beginning Linux Programming od czasu zadania tego pytania, a jej pierwszy rozdział zawiera przykładowy kod C i dobre wyjaśnienia dotyczące tworzenia i używania zarówno bibliotek statycznych, jak i współdzielonych . Te przykłady są dostępne za pośrednictwem Google Book Search w starszym wydaniu tej książki .

Bill the Lizard
źródło
Nie jestem pewien, czy rozumiem, co masz na myśli, mówiąc o „używaniu” go. Gdy wskaźnik do obiektu zostanie zwrócony, możesz go używać tak, jak każdego innego wskaźnika do obiektu.
codelogic
Artykuł, do którego utworzyłem łącze, pokazuje, jak utworzyć wskaźnik funkcji do funkcji fabryki obiektów przy użyciu dlsym. Nie pokazuje składni tworzenia i używania obiektów z biblioteki.
Bill the Lizard
1
Będziesz potrzebował pliku nagłówkowego opisującego klasę. Jak myślisz, dlaczego musisz używać „dlsym” zamiast po prostu pozwalać systemowi operacyjnemu na znajdowanie i łączenie biblioteki w czasie ładowania? Daj mi znać, jeśli potrzebujesz prostego przykładu.
nimrodm
3
@nimrodm: Jaka jest alternatywa dla używania „dlsym”? Mam (powinienem) pisać 3 programy C ++, które będą używać klas zdefiniowanych w bibliotece współdzielonej. Mam też 1 skrypt w Perlu, który go użyje, ale to zupełnie inny problem na następny tydzień.
Bill the Lizard

Odpowiedzi:

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 );
}

W systemie Mac OS X skompiluj z:

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

W systemie Linux skompiluj z:

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

Gdyby to było dla systemu wtyczek, użyłbyś MyClass jako klasy bazowej i zdefiniowałbyś wszystkie wymagane funkcje wirtualne. Autor wtyczki następnie wywodziłby się z MyClass, nadpisałby wirtualne i zaimplementował create_objecti destroy_object. Twoja główna aplikacja nie musiałaby być w żaden sposób zmieniana.

codelogic
źródło
6
Właśnie tego próbuję, ale mam tylko jedno pytanie. Czy użycie void * jest absolutnie konieczne, czy też funkcja create_object może zwrócić MyClass *? Nie proszę, żebyś to dla mnie zmieniał, po prostu chciałbym wiedzieć, czy istnieje powód, aby używać jednego nad drugim.
Bill the Lizard
1
Dzięki, wypróbowałem to i działało tak, jak w Linuksie z wiersza poleceń (kiedy wprowadziłem zmianę, którą zasugerowałeś w komentarzach do kodu). Szanuję Twój czas.
Bill the Lizard
1
Czy jest jakiś powód, dla którego miałbyś zadeklarować je z zewnętrznym „C”? Ponieważ jest to kompilowane przy użyciu kompilatora g ++. Dlaczego miałbyś chcieć używać konwencji nazewnictwa c? C nie może wywołać C ++. Interfejs opakowania napisany w c ++ jest jedynym sposobem wywołania tego z poziomu c.
ant2009
6
@ ant2009 potrzebujesz, extern "C"ponieważ dlsymfunkcja jest funkcją C. Aby dynamicznie załadować create_objectfunkcję, użyje powiązania w stylu C. Gdybyś nie użył rozszerzenia extern "C", nie byłoby możliwości poznania nazwy create_objectfunkcji w pliku .so z powodu zmiany nazw w kompilatorze C ++.
kokx,
1
Niezła metoda, jest bardzo podobna do tego, co ktoś zrobiłby na kompilatorze Microsoftu. przy odrobinie #if #else pracy, możesz uzyskać niezły system niezależny od platformy
Ha11owed
52

Poniżej przedstawiono przykład współużytkowanej biblioteki klas. [H, cpp] i modułu main.cpp korzystającego z tej biblioteki. To bardzo prosty przykład, a plik makefile mógłby być znacznie lepszy. Ale to działa i może ci pomóc:

shared.h definiuje klasę:

class myclass {
   int myx;

  public:

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

shared.cpp definiuje funkcje getx / setx:

#include "shared.h"

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

main.cpp używa klasy,

#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;
}

oraz plik makefile, który generuje libshared.so i łączy main z biblioteką współdzieloną:

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

Aby faktycznie uruchomić 'main' i połączyć się z libshared., prawdopodobnie będziesz musiał określić ścieżkę ładowania (lub umieścić ją w / usr / local / lib lub podobnym).

Poniższy kod określa bieżący katalog jako ścieżkę wyszukiwania bibliotek i uruchamia main (składnia bash):

export LD_LIBRARY_PATH=.
./main

Aby zobaczyć, że program jest powiązany z libshared. więc możesz spróbować ldd:

LD_LIBRARY_PATH=. ldd main

Wydruki na moim komputerze:

  ~/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
źródło
1
Wydaje się to (moim bardzo niewprawnym okiem) statyczne łączenie libshared.so z twoim plikiem wykonywalnym, a nie używanie dynamicznego linkowania w czasie wykonywania. Mam rację?
Bill the Lizard
10
Nie. To jest standardowe łączenie dynamiczne w systemie Unix (Linux). Biblioteka dynamiczna ma rozszerzenie „.so” (obiekt współdzielony) i jest połączona z plikiem wykonywalnym (w tym przypadku głównym) w czasie ładowania - za każdym razem, gdy ładowany jest plik main. Łączenie statyczne następuje w czasie łączenia i korzysta z bibliotek z rozszerzeniem „.a” (archiwum).
nimrodm
9
Jest to dynamicznie łączone w czasie kompilacji . Innymi słowy, potrzebujesz wcześniejszej wiedzy na temat biblioteki, z którą tworzysz linki (np. Tworzenie linków z „dl” dla dlopen). Różni się to od dynamicznego ładowania biblioteki, na przykład na podstawie nazwy pliku określonej przez użytkownika, gdzie wcześniejsza wiedza nie jest potrzebna.
codelogic
10
To, co próbowałem wyjaśnić (źle), to to, że w tym przypadku musisz znać nazwę biblioteki w czasie kompilacji (musisz przekazać -lshared do gcc). Zwykle używa się dlopen (), gdy ta informacja nie jest dostępna, tj. Nazwa biblioteki jest odkrywana w czasie wykonywania (np. Wyliczenie wtyczki).
codelogic
3
Użyj -L. -lshared -Wl,-rpath=$$(ORIGIN)podczas łączenia i upuść to LD_LIBRARY_PATH=..
Maxim Egorushkin
9

Zasadniczo powinieneś dołączyć plik nagłówkowy klasy do kodu, w którym chcesz użyć klasy w bibliotece współdzielonej. Następnie podczas tworzenia linku użyj flagi „-l”, aby połączyć swój kod z udostępnioną biblioteką. Oczywiście wymaga to, aby plik .so był tam, gdzie system operacyjny może go znaleźć. Patrz 3.5. Instalowanie i używanie udostępnionej biblioteki

Korzystanie z dlsym jest przydatne, gdy w czasie kompilacji nie wiesz, której biblioteki chcesz użyć. To nie brzmi, jakby tak było w tym przypadku. Może zamieszanie polega na tym, że system Windows wywołuje dynamicznie ładowane biblioteki, niezależnie od tego, czy łączysz się w czasie kompilacji, czy w czasie wykonywania (za pomocą analogicznych metod)? Jeśli tak, możesz myśleć o dlsym jako odpowiedniku LoadLibrary.

Jeśli naprawdę potrzebujesz dynamicznie ładować biblioteki (tj. Są to wtyczki), to ten FAQ powinien pomóc.

Matt Lewis
źródło
1
Powodem, dla którego potrzebuję dynamicznej biblioteki współdzielonej, jest to, że będę ją również wywoływał z kodu Perla. Z mojej własnej strony może to być kompletne nieporozumienie, że muszę również wywoływać to dynamicznie w innych programach C ++, które rozwijam.
Bill the Lizard
Nigdy nie próbowałem zintegrowanego Perla i C ++, ale myślę, że musisz użyć XS: johnkeiser.com/perl-xs-c++.html
Matt Lewis,
5

Oprócz poprzednich odpowiedzi chciałbym zwrócić uwagę na fakt, że należy używać idiomu RAII (pozyskiwanie zasobów to inicjalizacja) aby być bezpiecznym w przypadku zniszczenia obsługi.

Oto kompletny przykład roboczy:

Deklaracja interfejsu Interface.hpp::

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

using Base_creator_t = Base *(*)();

Zawartość biblioteki współdzielonej:

#include "Interface.hpp"

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

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

Dynamiczna procedura obsługi bibliotek współdzielonych 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);
        }
    }
};

Kod klienta:

#include "Derived_factory.hpp"

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

Uwaga:

  • Dla zwięzłości umieszczam wszystko w plikach nagłówkowych. W prawdziwym życiu powinieneś oczywiście podzielić swój kod pomiędzy .hppi.cpp pliki .
  • Aby uprościć, zignorowałem przypadek, w którym chcesz obsłużyć new/ deleteprzeciążenie.

Dwa jasne artykuły, aby uzyskać więcej informacji:

Xavier Lamorlette
źródło
To doskonały przykład. RAII jest zdecydowanie właściwą drogą.
David Steinhauer