c++ - Varias instancias de singleton en bibliotecas compartidas en Linux
dlopen (4)
Mi pregunta, como el título mencionado, es obvia, y describo el escenario en detalle. Hay una clase llamada singleton implementada por el patrón singleton como sigue, en el archivo 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_ */
luego, hay un complemento llamado hello.cpp de la siguiente manera:
#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;
}
puede ver que el complemento llama al singleton y cambia el número de atributo en el singleton.
Por último, hay una función principal que utiliza el singleton y el complemento de la siguiente manera:
#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);
}
y el makefile esta siguiendo:
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
Entonces, ¿cuál es la salida? Pensé que hay seguidores:
singleton.num in main : 100
singleton.num in hello.so : 100
singleton.num in hello.so after ++ : 101
singleton.num in main : 101
sin embargo, la salida real es la siguiente:
singleton.num in main : 100
singleton.num in hello.so : -1
singleton.num in hello.so after ++ : 0
singleton.num in main : 100
Demuestra que hay dos instancias de la clase singleton.
¿Por qué?
¡Gracias a todos por sus respuestas o comentarios!
Como seguimiento para Linux, también puede usar RTLD_GLOBAL
con dlopen(...)
, por man dlopen
(y los ejemplos que tiene). He hecho una variante del ejemplo de OP en este directorio: github tree Ejemplo de salida: output.txt
Rápido y sucio:
- Si no desea tener que vincular manualmente cada símbolo a su
main
, mantenga los objetos compartidos alrededor. (por ejemplo, si creó*.so
objetos para importar a Python) - Inicialmente puede cargar en la tabla de símbolos global, o hacer una
NOLOAD
+GLOBAL
.
Código:
#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
Modos:
- Modo 0: carga perezosa nominal (no funcionará)
- Modo 1: Incluir archivo para agregar a la tabla de símbolos estáticos.
- Modo 2: Cargar inicialmente usando RTLD_GLOBAL
- Modo 3: recargar utilizando RTLD_NOLOAD | RTLD_GLOBAL
Creo que la respuesta simple está aquí: http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html
Cuando tienes una variable estática, se almacena en el objeto (.o, .a y / o .so)
Si el objeto final que se ejecutará contiene dos versiones del objeto, el comportamiento es inesperado, como por ejemplo, llamar al Destructor de un objeto Singleton.
El uso del diseño adecuado, como declarar el miembro estático en el archivo principal y el uso de las etiquetas -rdynamic / fpic y "" compilador, hará el truco por usted.
Ejemplo de declaración makefile:
$ g++ -rdynamic -o appexe $(OBJ) $(LINKFLAGS) -Wl,--whole-archive -L./Singleton/ -lsingleton -Wl,--no-whole-archive $(LIBS)
¡Espero que esto funcione!
Debe tener cuidado al usar bibliotecas compartidas cargadas en tiempo de ejecución. Dicha construcción no es estrictamente parte del estándar C ++, y debe considerar cuidadosamente lo que resulta ser la semántica de tal procedimiento.
En primer lugar, lo que sucede es que la biblioteca compartida ve su propia variable global separada singleton::pInstance
. ¿Porqué es eso? Una biblioteca que se carga en tiempo de ejecución es esencialmente un programa separado e independiente que simplemente no tiene un punto de entrada. Pero todo lo demás es realmente como un programa separado, y el cargador dinámico lo tratará así, por ejemplo, inicializar variables globales, etc.
El cargador dinámico es una función de tiempo de ejecución que no tiene nada que ver con el cargador estático. El cargador estático es parte de la implementación estándar de C ++ y resuelve todos los símbolos del programa principal antes de que comience el programa principal. El cargador dinámico, por otro lado, solo se ejecuta después de que el programa principal ya haya comenzado. ¡En particular, todos los símbolos del programa principal ya deben ser resueltos! Simplemente no hay manera de reemplazar dinámicamente los símbolos del programa principal. Los programas nativos no se "administran" de ninguna manera que permita una reenlazada sistemática. (Tal vez algo puede ser pirateado, pero no de manera sistemática y portátil).
Entonces, la verdadera pregunta es cómo resolver el problema de diseño que estás intentando. La solución aquí es pasar identificadores a todas las variables globales a las funciones del complemento. Haga que su programa principal defina la copia original (y única) de la variable global, y que inicialice su biblioteca con un puntero a eso.
Por ejemplo, su biblioteca compartida podría verse así. Primero, agregue un puntero a puntero a la clase singleton:
class singleton
{
static singleton * pInstance;
public:
static singleton ** ppinstance;
// ...
};
singleton ** singleton::ppInstance(&singleton::pInstance);
Ahora use *ppInstance
lugar de pInstance
todas partes.
En el complemento, configure el singleton al puntero desde el programa principal:
void init(singleton ** p)
{
singleton::ppInsance = p;
}
Y la función principal, llame a la inicialización del complemento:
init_fn init;
hello_fn hello;
*reinterpret_cast<void**>(&init) = dlsym(lib, "init");
*reinterpret_cast<void**>(&hello) = dlsym(lib, "hello");
init(singleton::ppInstance);
hello();
Ahora el complemento comparte el mismo puntero a la instancia de singleton que el resto del programa.
En primer lugar, generalmente debe utilizar el indicador -fPIC
al -fPIC
bibliotecas compartidas.
No usarlo "funciona" en Linux de 32 bits, pero fallaría en uno de 64 bits con un error similar al siguiente:
/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
En segundo lugar, su programa funcionará como espera después de agregar -rdynamic
a la línea de enlace para el ejecutable principal:
singleton.num in main : 100
singleton.num in hello.so : 100
singleton.num in hello.so after ++ : 101
singleton.num in main : 101
Para comprender por -rdynamic
se requiere -rdynamic
, debe conocer la forma en que el enlazador dinámico resuelve los símbolos y la tabla de símbolos dinámicos .
Primero, veamos la tabla de símbolos dinámicos para hello.so
:
$ nm -C -D hello.so | grep singleton
0000000000000b8c W singleton::instance()
0000000000201068 B singleton::pInstance
0000000000000b78 W singleton::singleton()
Esto nos dice que hay dos definiciones de funciones débiles y una variable global singleton::pInstance
que son visibles para el enlazador dinámico.
Ahora veamos la tabla de símbolos estáticos y dinámicos para el example1
original1 (vinculado sin -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
$
Así es: aunque singleton::pInstance
está presente en el ejecutable como una variable global, ese símbolo no está presente en la tabla de símbolos dinámicos y, por lo tanto, es "invisible" para el vinculador dinámico.
Debido a que el enlazador dinámico "no sabe" que example1
ya contiene una definición de singleton::pInstance
, no vincula esa variable dentro de hello.so
a la definición existente (que es lo que realmente desea).
Cuando agregamos -rdynamic
a la línea de enlace:
$ 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()
Ahora la definición de singleton::pInstance
dentro del ejecutable principal es visible para el enlazador dinámico, por lo que "reutilizará" esa definición al cargar hello.so
:
LD_DEBUG=bindings ./example1-rdynamic |& grep pInstance
31972: binding file ./hello.so [0] to ./example1-rdynamic [0]: normal symbol `_ZN9singleton9pInstanceE''