0

Несколько экземпляров синглтона в общих библиотеках на Linux

11

Проблема с экземплярами синглетона в C++

У меня вопрос, который, как предполагает заголовок, является очевидным. Позвольте мне подробно описать сценарий.

В файле singleton.h у меня реализован класс singleton, использующий паттерн синглтон:

/*
 * 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_ */

Затем есть плагин hello.cpp, содержащий следующий код:

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

Как видно, плагин вызывает синглетон и изменяет атрибут num в синглтоне.

Наконец, вот основной код, который использует синглетон и плагин:

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

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

    singleton::instance().num = 100; // вызов синглетона
    cout << "singleton.num in main : " << singleton::instance().num << endl; // вызов синглетона

    // открытие библиотеки
    void* handle = dlopen("./hello.so", RTLD_LAZY);

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

    // загрузка символа
    typedef void (*hello_t)();

    // сброс ошибок
    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(); // вызов функции плагина hello

    cout << "singleton.num in main : " << singleton::instance().num << endl; // вызов синглетона
    dlclose(handle);
}

А вот и Makefile для сборки:

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

Теперь, что касается вывода программы. Я ожидал, что он будет таким:

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

Однако фактический вывод программы таков:

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

Это доказывает, что существуют два экземпляра класса синглетон.

Почему это происходит?

3 ответ(ов)

0

Во-первых, при сборке динамических библиотек вам следует использовать флаг -fPIC. Хотя отсутствие этого флага может "работать" на 32-битной версии Linux, на 64-битной это приведет к ошибке, подобной следующей:

/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

Во-вторых, ваша программа будет работать так, как вы ожидаете, после добавления -rdynamic в команду линковки для главного исполняемого файла:

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

Чтобы понять, почему требуется -rdynamic, нужно знать, как динамический линкер разрешает символы, а также о динамической таблице символов.

Сначала давайте рассмотрим динамическую таблицу символов для hello.so:

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

Это говорит нам о том, что есть два определения слабых функций и одна глобальная переменная singleton::pInstance, которые видны динамическому линкеру.

Теперь посмотрим на статическую и динамическую таблицы символов для оригинального example1 (связанного без -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
$ 

Верно: даже несмотря на то, что singleton::pInstance присутствует в исполняемом файле как глобальная переменная, этот символ отсутствует в динамической таблице символов и, следовательно, "невидим" для динамического линкера.

Поскольку динамический линкер "не знает", что в example1 уже содержится определение singleton::pInstance, он не связывает эту переменную внутри hello.so с существующим определением (что вы действительно хотите).

Когда мы добавляем -rdynamic в команду линковки:

$ 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()

Теперь определение singleton::pInstance внутри главного исполняемого файла стало видимым для динамического линкера, и он будет "повторно использовать" это определение при загрузке hello.so:

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

При использовании динамически загружаемых разделяемых библиотек необходимо быть особенно внимательным. Эта конструкция не является строго определенной частью стандарта C++, и вам следует тщательно продумать семантику подобных процедур.

Прежде всего, важно понимать, что общая переменная singleton::pInstance в разделяемой библиотеке имеет собственное, отдельное значение. Почему так происходит? Библиотека, загружаемая во время выполнения, фактически является отдельной, независимой программой, которая просто не имеет точки входа. Тем не менее, все остальное рассматривается как отдельная программа, и динамический загрузчик будет обрабатывать ее именно так, например, инициализируя глобальные переменные и т.д.

Динамический загрузчик — это средство выполнения, которое не имеет ничего общего со статическим загрузчиком. Статический загрузчик является частью реализации стандарта C++ и разрешает все символы основной программы до ее старта. Динамический загрузчик, в свою очередь, запускается после начала выполнения основной программы. В частности, все символы основной программы уже должны быть разрешены! Просто нет способа автоматически заменять символы основной программы динамически. Нативные программы не «управляются» таким образом, чтобы позволить систематическую перенастройку. (Может быть, что-то можно будет доработать, но не в систематическом и портируемом ключе.)

Таким образом, настоящий вопрос заключается в том, как решить проектную задачу, с которой вы столкнулись. Решение заключается в передаче указателей на все глобальные переменные в функции плагина. Сделайте так, чтобы ваша основная программа определяла оригинальную (и единственную) копию глобальной переменной, а затем инициализируйте вашу библиотеку с помощью указателя на эту переменную.

Например, ваша разделяемая библиотека может выглядеть следующим образом. Для начала добавьте указатель на указатель в класс singleton:

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

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

Теперь везде используйте *ppInstance вместо pInstance.

В плагине настройте синглтон на указатель из основной программы:

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

А в основной функции вызовите инициализацию плагина:

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

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

Теперь плагин совместно использует тот же указатель на экземпляр синглтона, что и остальная часть программы.

0

Я думаю, что простой ответ можно найти здесь:
http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html

Когда у вас есть статическая переменная, она хранится в объекте (.o, .a и/или .so).

Если финальный исполняемый файл содержит две версии одного и того же объекта, поведение может быть неожиданным, например, при вызове деструктора объекта Singleton.

Правильный дизайн, такой как объявление статического члена в основном файле и использование опций компилятора -rdynamic/fpic, а также директив компилятора поможет вам в этом.

Пример записи в makefile:

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

Надеюсь, это вам поможет!

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь