Всем доброго пятничного вечера!
Сегодня я хочу рассказать о некоторых коварных особенностях статических переменных при неправильной линковке исполняемых модулей. Я покажу проблему из моей реальной практики, которая может возникнуть у каждого.
Разжевываю все довольно детально, поэтому у «бывалых» и красноглазиков может возникнуть ощущение, что я «колупаюсь в песочнице», но это статья не только для них.
Давайте представим ситуацию: есть некоторый класс, реализованный в статической библиотеке (lib). Эту библиотеку статически привязывает модуль реализации (dll). Далее эту dll также статически привязывает исполняемый модуль (exe). Кроме этого Exe-модуль статически линкует статическую библиотеку (lib).
Примерно так:
Например, здесь есть следующая логика: в lib’е реализован некоторый инструмент для чего-либо. В dll’е реализована некоторая функциональность на основе данного инструмента. В exe реализован тест на эту функциональность. Dll сама не экспортирует инструментальный класс (который находится в lib’e), поэтому тесту требуется статическая линковка lib’ы.
Пусть инструментальный класс содержит в себе статическую переменную. А в dll есть функция создания данного класса, причем объект возвращается по значению.
Вот дополненная схема:
Вот код на С++:
- 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-модуля, все ссылки на статическую переменную станут не валидными. По шагам это выглядит так:
- *.exe: Вызов функции GetStaticObj().
- Dll.dll: создание временного объекта класса ListAndIter. В список кладется ноль, итератор iter указывает на него. Причем в это время статическая переменная на стороне exe-модуля пустая, соответственно итератор не валидный.
- *.exe: Вызывается конструктор копирования для объекта класса ListAndIter. У временного объекта итератор стал не валидным. У нового объекта итератор указывает на список из DLL.dll, хотя сам объект создается на стороне exe-модуля.
- Dll.dll: Уничтожается временный объект класса ListAndIter. Так как итератор не валидный никаких действий не происходит.
- *.exe: Вызывается деструктор для объекта obj. При попытке сравнения итератора с getList().end() вылезает виндовая ошибка: «Итераторы не совместимы». То есть итератор от «другого списка».
Попробуем исправить такую ситуацию, убрав зависимость exe-модуля от статической библиотеки. Тогда всю функциональность статической библиотеки нужно экспортировать через dll (см. код ниже):
Изменения в коде:
- Создал новый заголовочный файл 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 стал шаблонным:
Для каждой полной специализации шаблона и всех объектов таких классов должна быть своя статическая переменная.
Во-первых, мы обязаны реализацию шаблонного класса поместить в заголовочный файл, т.к. шаблоны раскрываются на этапе компиляции.
Если статическая переменная является членом класса, то чтобы успешно собрать наш проект, мы вынуждены явно проинициализировать эти переменные во всех используемых модулях. В таком случае мы ЯВНО создаем две статические переменные, что возвращает нас к 1-ому примеру.
Иначе, если статическая переменная не является членом класса, а создается через статический метод, то в этом случае она также создается, но уже неявно для нас. Ошибка повторяется вновь.
Для разрешения такой ситуации необходимо создавать промежуточную lib’у, в которой и размещать эту функциональность. То есть вместо dll делать lib. Тогда снова останется одна статическая переменная.
Вывод: При использовании статических переменных в статических библиотеках нужно следить за тем, чтобы исполняемые модули не линковались статически друг в друга.
Иногда проблема не решается упрощением зависимостей. Например, класс реализован в статической библиотеке и у него есть некий статический счетчик экземпляров. Эта статическая библиотека линкуется в две разные dll, таким образом, в них создается два разных счетчика. В данном случае проблема решается путем превращения статической библиотеки в динамическую (dll). Соответственно две другие dll прилинковывают новую dll динамически. Тогда статическая переменная будет только в одной dll (в той, к которой реализован класс со счетчиком).
Весь код можно взять с github.
P.S. Много всего написал, возможно не идеально… буду рад советам и замечаниям.
Автор: dendibakh