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

в 7:57, , рубрики: c++, dll, library, smart pointers, Программирование, разработка, метки: , , ,

Многие эксперты С++ агитируют использовать интеллектуальные указатели, утверждая, что из современного С++, явное использование 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

Автор: Renzo

Источник

Поделиться

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