Несколько экземпляров синглтона в разделяемых библиотеках в Linux - PullRequest
36 голосов
/ 24 декабря 2011

Мой вопрос, как упоминалось в названии, очевиден, и я подробно описываю сценарий.В файле 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;
}

Вы можете видеть, что плагин вызывает 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

Это доказывает, что есть два экземпляра класса singleton.

Почему?

Ответы [ 4 ]

54 голосов
/ 25 декабря 2011

Во-первых, вы обычно должны использовать флаг -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необходимо знать о том, как динамический компоновщик разрешает символы, и о таблице символов dynamic .

Сначала давайте рассмотрим таблицу динамических символов для 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'
5 голосов
/ 24 декабря 2011

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

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

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

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

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

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

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

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

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

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, что и остальная часть программы.

2 голосов
/ 26 июля 2012

Я думаю, что простой ответ здесь: 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) 

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

0 голосов
/ 03 августа 2017

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

В качестве продолжения для Linux вы также можете использовать RTLD_GLOBAL с dlopen(...) для man dlopen (и примерами, которые он имеет). Я сделал вариант примера OP в этом каталоге: github tree Пример вывода: 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
...