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

в 13:25, , рубрики: c++, линковка, метки: ,

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

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

Давайте представим ситуацию: есть некоторый класс, реализованный в статической библиотеке (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. Много всего написал, возможно не идеально… буду рад советам и замечаниям.

Автор: dendibakh

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js