Подтвердить что ты не робот

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

Мой вопрос, как указано в названии, очевиден, и я описываю сценарий в деталях. Существует класс с именем singleton, реализованный singleton pattern, как указано ниже, в файле 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_ */

то есть плагин с именем 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;
}

вы можете видеть, что плагин вызывает singleton и меняет атрибут num в singleton.

last, существует основная функция, использующая singleton и плагин следующим образом:

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

и make файл:

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

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

Почему?

4b9b3361

Ответ 1

Во-первых, вы должны использовать флаг -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'

Ответ 2

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

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

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

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

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

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

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

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

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

void init(singleton ** p)
{
    singleton::ppInsance = 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();

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

Ответ 3

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

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

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

Использование надлежащего дизайна, например объявление статического члена в основном файле и использование -rdynamic/fpic и использование директив "компилятора" сделают трюк для вас.

Пример инструкции makefile:

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

Надеюсь, что это сработает!

Ответ 4

Спасибо всем за ваши ответы!

В качестве продолжения для Linux вы также можете использовать RTLD_GLOBAL с dlopen(...), за man dlopen (и примеры, которые у него есть). Я сделал вариант примера OP в этом каталоге: дерево github Пример вывода: output.txt

Быстрая и грязная:

  • Если вы не хотите, чтобы вручную привязывались в каждом символе к вашему main, поддерживайте общие объекты. (например, если вы сделали объекты *.so для импорта в Python)
  • Вы можете сначала загрузить в глобальную таблицу символов или повторно открыть NOLOAD + GLOBAL.

код:

#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

Режимы:

  • Режим 0: Номинальная ленивая загрузка (не будет работать)
  • Режим 1: включить файл для добавления в статическую таблицу символов.
  • Режим 2: загрузка сначала с использованием RTLD_GLOBAL
  • Режим 3: перезагрузка с использованием RTLD_NOLOAD | RTLD_GLOBAL