Как обеспечить надлежащее пересечение границ динамической библиотеки, используя пользовательские средства удаления смарт-указателей

    Многие эксперты С++ агитируют использовать интеллектуальные указатели, утверждая, что из современного С++, явное использование new должно вообще исчезнуть (ну, по крайней мере, когда в С++14 пофиксят отсутствие std::make_unique). Все динамические выделения памяти должны быть инкапсулированы или в стандартную библиотеку, или контейнеры типа std::vector, или интеллектуальные указатели.

    Смарт-указатели стандартной библиотеки могут быть настроены так, чтобы они сами занимались освобождением занимаемой ими памяти. Эта возможность и заложена в основу ответа на вопрос, поставленного в заголовке статьи.

    Объект является пересекающим границу динамической библиотеки, если он инициализируется в одном блоке, а используется в другом. Это происходит, когда, например, в dll инициализируется объект и возвращается указатель на него.

    Предположим, одна библиотека (или исполнимый модуль) связывается с другой библиотекой, используя фабрику для динамической инициализации объекта и получения указателя на него. Блок, который использует этот указатель, может удалить указатель для освобождения области памяти, на которую он указывает. Если библиотека, которая выделяет память и блок, работающий с указателем, используют различные версии динамического выделения памяти ОС (CRT в Windows), то возникнет ошибка. Пример этой проблемы (в случае с Windows):

    Как правило (до появления С++11), разработчики библиотеки должны были разрабатывать функции освобождения памяти для объектов, которые были выделены в пределах этой библиотеки, для того, чтобы избежать этой проблемы. Это имело побочный эффект: интерфейсы таких библиотек становились более «тяжелыми», к тому же от них теперь требовалось «ноу-хау» для корректного выделения и освобождения памяти для объектов библиотеки. В идеале, пользователя не должна была беспокоить сама схема выделения/освобождения, он просто должен был вызывать механизм библиотеки (например, фабрику) для выделения памяти, не заботясь об ее последующем освобождении.

    Переходим к коддингу


    У нас будет два проекта: первый будет состоять просто из файла main, использующий фабрику библиотеки для инициализации объектов из нее, второй будет иллюстрировать проблемную ситуацию и ее решение.
    Проблемная область — синглтон фабрика (ProblematicFactory), которая инициализирует объект и возвращает указатель на него. Решение — другой сингтон, который, после инициализации объекта, возвращает указатель std::unique_ptr, имеющий свое собственное средство удаления, производящее освобождение памяти в DLL.
    Если запустить программу в режиме отладки с определением USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION, то можно увидеть, что отладчик обнаруживает повреждение «кучи».

    Файл main

    // main.cpp
    #include <ProblematicFactory.h>
    #include <SafeFactory.h>
    
    // измените undef на define, чтобы увидеть assert'ы о повреждении кучи
    #undef USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION
     
    int main()
    {
    #ifdef USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION
      {
        // это выделение делается в DLL
        auto wMyObject = ProblematicFactory::getInstance().create();
        // это освобождение происходит в текущем блоке
        delete wMyObject;
        // если DLL и этот блок будут линковаться с одной и той же CLR DLL, 
        // удаление произойдет нормально, иначе - это вызовет повреждение кучи
      }
    #endif
      {
        auto wMyObject = SafeFactory::getInstance().create();
        // когда программа перейдет в следующий блок, wMyObject будет
        // автоматически удален, используя пользовательское средство удаления,
        // реализованное в MyClass.h (см. далее), функция освобождения
        // в библиотеке вызываться не будет
      } 
      {
        std::shared_ptr< MyClass > wMyObject = SafeFactory::getInstance().create();
      }
      return 0;
    }
    

    Проблемная фабрика

    Это — типичная реализация фабрики, которая возвращает указатель на объект, который может быть создан библиотекой.
    // ProblematicFactory.h
    #pragma once
     
    #include "DllSwitch.h"
    #include "MyClass.h"
    
    class LIBRARYFACTORY_API ProblematicFactory
    {
    public:
      static ProblematicFactory & getInstance()
      {
         static ProblematicFactory wProblematicFactory;
         return wProblematicFactory;
      }
      MyClass * create() const
      {
         return new MyClass;
      }
    private:
      ProblematicFactory() {};
    }; 
    

    Безопасная фабрика

    Синтаксически, использование этой фабрики точно такое же, как и проблемной (см. main), но здесь указатель инкапсулируется в std::unique_ptr, а не std::shared_ptr.
    // SaveFactory.h
    #pragma once
     
    #include "DllSwitch.h"
    #include "MyClass.h"
    
    #include <memory>
    
    class LIBRARYFACTORY_API SafeFactory
    {
    public:
      static SafeFactory & getInstance();
      // Здесь std::unique_ptr не будет пересекать границу библиотеки, 
      // т.к. эта функция не будет входить в DLL. Создание произойдет
      // на стороне клиента, поэтому используется std::unique_ptr клиента.
      // Так же, нет необходимости задавать пользовательские средства
      // удаления, поскольку для класса MyClass существует std::default_delete
      inline std::unique_ptr< MyClass > create() const
      {
        return std::unique_ptr< MyClass >(doCreate());
      }
    private:
      SafeFactory();
      MyClass * doCreate() const;
    };
    

    Пересечение границы

    // MyClass.h
    #pragma once
     
    #include "DllSwitch.h"
    
    #include <memory>
    
    class LIBRARYFACTORY_API MyClass
    {
    };
     
    namespace std
    {
    template<>
    class LIBRARYFACTORY_API default_delete< MyClass >
    {
    public:
      void operator()(MyClass *iToDelete)
      {
        delete iToDelete;
      }
    };
    }
    

    Во всех вышеприведенных файлах подключается заголовочный файл DllSwitch.h, определяющая LIBRARYFACTORY_API, его содержание:
    // DllSwitch.h
    #pragma once
    
    #ifdef LIBRARYFACTORY_EXPORTS
    #define LIBRARYFACTORY_API __declspec(dllexport)
    #else
    #define LIBRARYFACTORY_API __declspec(dllimport)
    #endif
    


    UPD Все реализации функций необходимо выносить в отдельные файлы
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 11

      +1
      А что будет если default_delete<MyClass>::operator() заинлайнится компилятором, и соответственно его тело вызовется не из того модуля где объект создан, а из вызывающего?
        +1
        Что значит если? Вообще-то он будет заинлайнен компилятором (в смысле правил линковки), поскольку определение метода приведено в определении класса. С другой стороны, это элементарно решается разделением объявления и реализации и переносом реализации в файл исходных кодов.
          0
          Ну кто его знает. Может __declspec(dllimport/dllexport) для класса предотвращает встраивание методов.

          Во-вторых, по никакого обязательного будет нет.
          Компилятор может встроить метод, а может вызвать его. Во втором случае символ метода будет прилинкован согласно типу связывания (в MSVC он задается атрибутом dllimport), т.е. будет скорее всего использован правильный метод из другой ДЛЛ.

          А задавал я вопрос потому что не уверен что приведенное решение гарантирует что-либо.
          (Ну а насчет того работает ли оно вообще — поверил автору)
            0
            Я имел в виду «встривание» в смысле действий правил линковки.

            Сейчас уточнил, действительно, это просто UB:

            7.1.2 Function specifiers [dcl.fct.spec]

            2 A function declaration (8.3.5, 9.3, 11.3) with an inline specifier declares an inline function. [...] An implementation is not required to perform this inline substitution at the point of call; however, even if this inline substitution is omitted, the other rules for inline functions defined by 7.1.2 shall still be respected.

            3 A function defined within a class definition is an inline function. [...]

            4 An inline function shall be defined in every translation unit in which it is odr-used and shall have exactly
            the same definition in every case (3.2)
            . [...]

            4) не выполняется, потому что delete определен в разных библиотеках по разному.
        0
        А зачем функцию std::unique_ptr< MyClass > create() const оставлять в хедере? В данном случае, я не вижу проблемы того, что unique_ptr будет создан в библиотеке. Сам по себе unique_ptr память не выделяет.

        Более того, мне кажется, что и определять свой собственный default_delete< MyClass > не обязательно. Если создавать в объект unique_ptr внутри библиотеки, не указывая deleter, будет создан объект класса по умолчанию и отдан unique_ptr-у по значению. Когда мы будем отдавать unique_ptr, то deleter будет перемещен в новый unique_ptr. То есть, клиент получит deleter из библиотеки.

        Суммируя, библиотека должна просто создавать отдавать unique_ptr или shared_ptr и все должно работать само по себе. Или я что-то упускаю?

        edit: вдогонку, ссылка, подтверждающая мою мысль stackoverflow.com/a/5835036/661451
          0
          Прошу прощения, моя оплошность. Реализация всех функций должна быть в отдельных файлах
        0
        Ок, это отвечает на первый вопрос. Но это мелочи.
        Меня больше интересует, насколько верно мое рассуждение, что современные умные указатели настолько умные, что ничего делать не надо, а все есть?
          +1
          У вас файл DllSwitch.h не имеет ничего общего с С++. Таких конструкций или ключевых слов, как «declspec» и "#pragma once", в языке С++ не существует.
            –1
              0
              1. Я думаю тут ожидалась ссылка на главу стандарта c++, но в нём таких слов нет насколько я помню.

              2. Если отвлечься от стандарта и вернуться к реальности, то #pragma once это нормально — сейчас сложно найти компилятор который не поддерживает её. Но __declspec это как минимум windows-specific (вообще-то microsoft specific).

              Думаю чтобы избежать подобных придирок надо было указать что речь только про MSVC либо чуть усложнить DllSwitch.h добавив поддержку для gcc хотя бы (ссылка для изучения: gcc.gnu.org/wiki/Visibility )

          Only users with full accounts can post comments. Log in, please.