Biblioteca compartida dinámica de C ++ en Linux

167

Este es un seguimiento de la compilación de Dynamic Shared Library con g ++ .

Estoy tratando de crear una biblioteca de clase compartida en C ++ en Linux. Puedo hacer que la biblioteca se compile, y puedo llamar a algunas de las funciones (que no son de clase) usando los tutoriales que encontré aquí y aquí . Mis problemas comienzan cuando trato de usar las clases que están definidas en la biblioteca. El segundo tutorial al que me vinculé muestra cómo cargar los símbolos para crear objetos de las clases definidas en la biblioteca, pero no utiliza esos objetos para realizar ningún trabajo.

¿Alguien sabe de un tutorial más completo para crear bibliotecas de clases compartidas de C ++ que también muestre cómo usar esas clases en un ejecutable separado? Un tutorial muy simple que muestra la creación, el uso de objetos (los captadores y establecedores simples estarían bien), y la eliminación sería fantástica. Un enlace o una referencia a algún código fuente abierto que ilustre el uso de una biblioteca de clase compartida sería igualmente bueno.


Aunque las respuestas de codelogic y nimrodm funcionan, solo quería agregar que recogí una copia de Beginning Linux Programming desde que hice esta pregunta, y su primer capítulo tiene un código de ejemplo C y buenas explicaciones para crear y usar bibliotecas estáticas y compartidas . Estos ejemplos están disponibles a través de la Búsqueda de libros de Google en una edición anterior de ese libro .

Bill el lagarto
fuente
No estoy seguro de entender lo que quiere decir con "usarlo", una vez que se devuelve un puntero al objeto, puede usarlo como si usara cualquier otro puntero a un objeto.
codelogic
El artículo al que enlacé muestra cómo crear un puntero de función a una función de fábrica de objetos usando dlsym. No muestra la sintaxis para crear y usar objetos de la biblioteca.
Bill the Lizard
1
Necesitará el archivo de encabezado que describe la clase. ¿Por qué cree que debe usar "dlsym" en lugar de dejar que el sistema operativo encuentre y vincule la biblioteca en el momento de la carga? Avísame si necesitas un ejemplo simple.
nimrodm
3
@nimrodm: ¿Cuál es la alternativa al uso de "dlsym"? Estoy (se supone que estoy) escribiendo 3 programas C ++ que usarán todas las clases definidas en la biblioteca compartida. También tengo 1 script Perl que lo usará, pero ese es otro problema para la próxima semana.
Bill the Lizard

Respuestas:

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

En Mac OS X, compile con:

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

En Linux, compile con:

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

Si esto fuera para un sistema de complemento, usaría MyClass como clase base y definiría todas las funciones virtuales requeridas. El autor del complemento derivaría de MyClass, anularía los virtuales e implementaría create_objecty destroy_object. Su aplicación principal no necesitaría ser cambiada de ninguna manera.

codelogic
fuente
66
Estoy en el proceso de intentar esto, pero solo tengo una pregunta. ¿Es estrictamente necesario usar void *, o podría la función create_object devolver MyClass * en su lugar? No te estoy pidiendo que cambies esto por mí, solo me gustaría saber si hay una razón para usar uno sobre el otro.
Bill the Lizard
1
Gracias, probé esto y funcionó como está en Linux desde la línea de comandos (una vez que hice el cambio que sugirió en los comentarios del código). Aprecio tu tiempo.
Bill the Lizard
1
¿Hay alguna razón por la que declararías esto con una "C" externa? Como esto se compila usando un compilador de g ++. ¿Por qué querrías usar la convención de nomenclatura de c? C no puede llamar a c ++. Una interfaz de contenedor escrita en c ++ es la única forma de llamar a esto desde c.
ant2009
66
@ ant2009 necesita el extern "C"porque la dlsymfunción es una función C. Y para cargar dinámicamente la create_objectfunción, utilizará la vinculación de estilo C. Si no usara el extern "C", no habría forma de saber el nombre de la create_objectfunción en el archivo .so, debido a la alteración del nombre en el compilador de C ++.
kokx
1
Buen método, es muy similar a lo que alguien haría en un compilador de Microsoft. con un poco de #si #else trabajo, puede obtener un buen sistema independiente de plataforma
Ha11owed
52

A continuación se muestra un ejemplo de una biblioteca de clase compartida compartida. [H, cpp] y un módulo main.cpp que usa la biblioteca. Es un ejemplo muy simple y el archivo MAKE podría mejorarse mucho. Pero funciona y puede ayudarte:

shared.h define la clase:

class myclass {
   int myx;

  public:

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

shared.cpp define las funciones getx / setx:

#include "shared.h"

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

main.cpp usa la clase,

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

y el archivo MAKE que genera libshared.so y enlaza main con la biblioteca compartida:

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

Para ejecutar 'main' y vincular con libshared.so probablemente necesitará especificar la ruta de carga (o ponerla en / usr / local / lib o similar).

A continuación se especifica el directorio actual como la ruta de búsqueda para bibliotecas y se ejecuta main (sintaxis bash):

export LD_LIBRARY_PATH=.
./main

Para ver que el programa está vinculado con libshared.so puede probar ldd:

LD_LIBRARY_PATH=. ldd main

Impresiones en mi máquina:

  ~/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
fuente
1
Esto parece (para mi ojo muy inexperto) estar vinculando estáticamente libshared.so a su ejecutable, en lugar de utilizar la vinculación dinámica en tiempo de ejecución. ¿Estoy en lo correcto?
Bill the Lizard
10
No. Este es un enlace dinámico estándar de Unix (Linux). Una biblioteca dinámica tiene la extensión ".so" (Objeto compartido) y está vinculada con el ejecutable (principal en este caso) en el momento de la carga, cada vez que se carga el principal. El enlace estático se produce en el momento del enlace y utiliza bibliotecas con la extensión ".a" (archivo).
nimrodm
9
Esto está vinculado dinámicamente en el momento de la compilación . En otras palabras, necesita un conocimiento previo de la biblioteca con la que está vinculando (por ejemplo, vincular contra 'dl' para dlopen). Esto es diferente de cargar dinámicamente una biblioteca, basado en, por ejemplo, un nombre de archivo especificado por el usuario, donde no se necesita conocimiento previo.
codelogic el
10
Lo que estaba tratando de explicar (mal) es que en este caso, necesita saber el nombre de la biblioteca en el momento de la compilación (debe pasar -compartido a gcc). Por lo general, uno usa dlopen () cuando esa información no está disponible, es decir, el nombre de la biblioteca se descubre en tiempo de ejecución (por ejemplo: enumeración de complementos).
codelogic
3
Úselo -L. -lshared -Wl,-rpath=$$(ORIGIN)al vincular y soltar eso LD_LIBRARY_PATH=..
Maxim Egorushkin
9

Básicamente, debe incluir el archivo de encabezado de la clase en el código donde desea usar la clase en la biblioteca compartida. Luego, cuando enlace, use la bandera '-l' para vincular su código con la biblioteca compartida. Por supuesto, esto requiere que .so esté donde el sistema operativo pueda encontrarlo. Ver 3.5. Instalación y uso de una biblioteca compartida

El uso de dlsym es para cuando no sabe en tiempo de compilación qué biblioteca desea usar. Eso no parece ser el caso aquí. ¿Quizás la confusión es que Windows llama a las bibliotecas cargadas dinámicamente si hace el enlace en la compilación o en tiempo de ejecución (con métodos análogos)? Si es así, puede pensar en dlsym como el equivalente de LoadLibrary.

Si realmente necesita cargar dinámicamente las bibliotecas (es decir, son complementos), estas preguntas frecuentes deberían ser útiles.

Matt Lewis
fuente
1
La razón por la que necesito una biblioteca compartida dinámica es que también la llamaré desde el código Perl. Puede ser una idea errónea completa por mi parte que también necesito llamarlo dinámicamente desde otros programas de C ++ que estoy desarrollando.
Bill the Lizard
Nunca he probado Perl y C ++ integrados, pero creo que necesitas usar XS: johnkeiser.com/perl-xs-c++.html
Matt Lewis
5

Además de las respuestas anteriores, me gustaría crear conciencia sobre el hecho de que debe usar el lenguaje RAII (La adquisición de recursos es la inicialización) para estar seguro sobre la destrucción del controlador.

Aquí hay un ejemplo de trabajo completo:

Declaración de interfaz Interface.hpp:

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

using Base_creator_t = Base *(*)();

Contenido de la biblioteca compartida:

#include "Interface.hpp"

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

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

Controlador dinámico de biblioteca compartida 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);
        }
    }
};

Codigo del cliente:

#include "Derived_factory.hpp"

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

Nota:

  • Puse todo en los archivos de encabezado por concisión. En la vida real, por supuesto, debe dividir su código entre .hppy .cpparchivos.
  • Para simplificar, ignoré el caso en el que desea manejar una new/ deletesobrecarga.

Dos artículos claros para obtener más detalles:

Xavier Lamorlette
fuente
Este es un excelente ejemplo. RAII es definitivamente el camino a seguir.
David Steinhauer