Использование статических переменных и статическая линковка исполняемых модулей друг в друга

от автора

Всем доброго пятничного вечера!

Сегодня я хочу рассказать о некоторых коварных особенностях статических переменных при неправильной линковке исполняемых модулей. Я покажу проблему из моей реальной практики, которая может возникнуть у каждого.
Разжевываю все довольно детально, поэтому у «бывалых» и красноглазиков может возникнуть ощущение, что я «колупаюсь в песочнице», но это статья не только для них.

Давайте представим ситуацию: есть некоторый класс, реализованный в статической библиотеке (lib). Эту библиотеку статически привязывает модуль реализации (dll). Далее эту dll также статически привязывает исполняемый модуль (exe). Кроме этого Exe-модуль статически линкует статическую библиотеку (lib).
Примерно так:

image

Например, здесь есть следующая логика: в lib’е реализован некоторый инструмент для чего-либо. В dll’е реализована некоторая функциональность на основе данного инструмента. В exe реализован тест на эту функциональность. Dll сама не экспортирует инструментальный класс (который находится в lib’e), поэтому тесту требуется статическая линковка lib’ы.
Пусть инструментальный класс содержит в себе статическую переменную. А в dll есть функция создания данного класса, причем объект возвращается по значению.
Вот дополненная схема:

image
Вот код на С++:

  • lib
    ListAndIter.h

    #pragma once #include <list>  using namespace std;  class ListAndIter {    private:       std::list<int>::iterator iter;       static std::list<int> &getList();    public:       void foo();       ListAndIter();       ListAndIter(ListAndIter& rhs);       ~ListAndIter();       }; 

    ListAndIter.cpp

    #include "ListAndIter.h"  ListAndIter::ListAndIter() {     getList().push_front(0);     iter = getList().begin(); }  ListAndIter::ListAndIter(ListAndIter& rhs) {    this->iter = rhs.iter;    rhs.iter = getList().end(); }  std::list<int> & ListAndIter::getList() {    static std::list<int> MyList;    return MyList; }  ListAndIter::~ListAndIter() {    if (iter != getList().end())        getList().erase(iter); }  void ListAndIter::foo() {  } 

  • dll
    GetStaticObj.h

    #pragma once  #include "ListAndIter.h"  #ifdef _DLL_EXPORTS    #define _DLL_EXP __declspec(dllexport) #else    #define _DLL_EXP __declspec(dllimport) #endif  _DLL_EXP ListAndIter GetStaticObj();  

    GetStaticObj.cpp

    #include "GetStaticObj.h"  ListAndIter GetStaticObj() {    ListAndIter obj;    obj.foo();    return obj; } 

  • exe
    Main.cpp

    #include "GetStaticObj.h"  int main() {    ListAndIter obj = GetStaticObj();    obj.foo(); } 

Как видно из кода, есть специальная функция foo, которая служит для обхода RVO, чтобы вызывался конструктор копирования. Напомню, что и dll-модуль и exe-модуль собираются независимо друг от друга, поэтому они должны знать о существовании статической переменной в lib’е и поэтому создают их у себя.

Объект класса ListAndIter возвращается через конструктор копирования, поэтому при получении объекта на стороне exe-модуля, все ссылки на статическую переменную станут не валидными. По шагам это выглядит так:

  1. *.exe: Вызов функции GetStaticObj().
  2. Dll.dll: создание временного объекта класса ListAndIter. В список кладется ноль, итератор iter указывает на него. Причем в это время статическая переменная на стороне exe-модуля пустая, соответственно итератор не валидный.
  3. *.exe: Вызывается конструктор копирования для объекта класса ListAndIter. У временного объекта итератор стал не валидным. У нового объекта итератор указывает на список из DLL.dll, хотя сам объект создается на стороне exe-модуля.
  4. Dll.dll: Уничтожается временный объект класса ListAndIter. Так как итератор не валидный никаких действий не происходит.
  5. *.exe: Вызывается деструктор для объекта obj. При попытке сравнения итератора с getList().end() вылезает виндовая ошибка: «Итераторы не совместимы». То есть итератор от «другого списка».

Попробуем исправить такую ситуацию, убрав зависимость exe-модуля от статической библиотеки. Тогда всю функциональность статической библиотеки нужно экспортировать через dll (см. код ниже):

image
Изменения в коде:

  • Создал новый заголовочный файл shared.h. В нем описываем макросы экспорта. Разместил файл в lib’е:
    shared.h

    #pragma once  #ifdef _DLL_EXPORTS    #define _DLL_EXP __declspec(dllexport) #else    #define _DLL_EXP __declspec(dllimport) #endif 

  • В ListAndIter.h добавил директивы экспорта:
    ListAndIter.h

    #pragma once #include <list> #include "shared.h"  using namespace std;  class ListAndIter {    private:       std::list<int>::iterator iter;       _DLL_EXP static std::list<int> &getList();    public:       _DLL_EXP void foo();       _DLL_EXP ListAndIter();       _DLL_EXP ListAndIter(ListAndIter& rhs);       _DLL_EXP ~ListAndIter(); }; 

  • В dll соответственно убрал объявления макросов экспорта:
    GetStaticObj.h

    #pragma once  #include "ListAndIter.h" #include "shared.h"  _DLL_EXP ListAndIter GetStaticObj(); 

Теперь объект будет создаваться и удаляться только на стороне dll. В exe-модуле статической переменной не будет и такой код отработает успешно.

Теперь давайте предположим, что будет, если класс ListAndIter стал шаблонным:

image

Для каждой полной специализации шаблона и всех объектов таких классов должна быть своя статическая переменная.
Во-первых, мы обязаны реализацию шаблонного класса поместить в заголовочный файл, т.к. шаблоны раскрываются на этапе компиляции.
Если статическая переменная является членом класса, то чтобы успешно собрать наш проект, мы вынуждены явно проинициализировать эти переменные во всех используемых модулях. В таком случае мы ЯВНО создаем две статические переменные, что возвращает нас к 1-ому примеру.
Иначе, если статическая переменная не является членом класса, а создается через статический метод, то в этом случае она также создается, но уже неявно для нас. Ошибка повторяется вновь.

Для разрешения такой ситуации необходимо создавать промежуточную lib’у, в которой и размещать эту функциональность. То есть вместо dll делать lib. Тогда снова останется одна статическая переменная.

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

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

Весь код можно взять с github.

P.S. Много всего написал, возможно не идеально… буду рад советам и замечаниям.

ссылка на оригинал статьи http://habrahabr.ru/post/201414/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *