Подтвердить что ты не робот

Управление памятью/кучей в DLL

Хотя это, по-видимому, очень распространенная проблема, я не собирал много информации: как я могу создать безопасный интерфейс между границами DLL в отношении выделения памяти?

Хорошо известно, что

// in DLL a
DLLEXPORT MyObject* getObject() { return new MyObject(); }
// in DLL b 
MyObject *o = getObject();
delete o;

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

Конечно, можно было бы предоставить

// in DLL a
DLLEXPORT void deleteObject(MyObject* o) { delete o; }

но, возможно, есть лучшие способы (например, smart_ptr?). Я читал об использовании пользовательских распределителей при работе с контейнерами STL.

Итак, мой запрос больше посвящен общим указателям на статьи и/или литературу, посвященные этой теме. Существуют ли особые ошибки, которые нужно учитывать (обработка исключений?), И эта проблема ограничена только DLL или слишком обширными объектами UNIX?

4b9b3361

Ответ 1

Как вы сказали, вы можете использовать boost:: shared_ptr для решения этой проблемы. В конструкторе вы можете передать пользовательскую функцию очистки, которая может быть методом deleteObject-dll, создающим указатель. Пример:

boost::shared_ptr< MyObject > Instance( getObject( ), deleteObject );

Если вам не нужен C-интерфейс для вашей DLL, вы можете getObject вернуть shared_ptr.

Ответ 2

Перегрузка operator new, operator delete et. al для всех ваших DLL-классов и реализовать их в DLL:

 void* MyClass::operator new(size_t numb) {
    return ::operator new(num_bytes);
 }

 void MyClass::operator delete(void* p) {
    ::operator delete(p);
 }
 ...

Это можно легко разместить в общем базовом классе для всех классов, экспортируемых DLL.

Таким образом, выделение и освобождение полностью выполняются в куче DLL. Честно говоря, я не уверен, есть ли у него серьезные проблемы или проблемы с переносимостью, но это работает для меня.

Ответ 3

Вы можете указать, что это "может привести к сбоям". Смешно - "может" означает полную противоположность "конечно".

Теперь утверждение в основном историческое. Существует очень простое решение: используйте 1 компилятор, 1 настройку компилятора и ссылку на DLL-форму CRT. (И вы, вероятно, можете избежать пропусков последних)

Нет ссылок на конкретные статьи, поскольку в настоящее время это не проблема. В любом случае вам понадобится 1 компилятор, 1 правило настройки. Простые вещи как sizeof(std::string) зависят от него, и в противном случае у вас будут массовые нарушения ODR.

Ответ 5

Другой вариант, который может быть применим в некоторых случаях, заключается в том, чтобы сохранить все выделение и освободить место в DLL и предотвратить перемещение объекта по этой границе. Вы можете сделать это, предоставив дескриптор, чтобы создание MyObject создало его внутри DLL-кода и возвращает простой дескриптор (например, unsigned int), через который выполняются все операции клиента:

// Client code
ObjHandle h=dllPtr->CreateObject();
dllPtr->DoOperation(h);
dllPtr->DestroyObject(h);

Так как все распределение происходит внутри dll, вы можете убедиться, что оно очищается путем обертывания в shared_ptr. Это в значительной степени метод, предложенный Джоном Лакосом в Large Scale С++.

Ответ 6

В "многоуровневой" архитектуре (очень распространенный сценарий) самый глубокий лежащий компонент отвечает за предоставление политики по этому вопросу (может возвращать shared_ptr<>, как было предложено выше, или "вызывающий отвечает за удаление этого" или "никогда удалите это, но вызовите releaseFooObject() по завершении и не получите доступ к нему после этого" или...), а компонент ближе к пользователю отвечает за выполнение этой политики.

Двунаправленный поток информации усложняет задачу.


эта проблема ограничена только DLL или слишком обширными объектами UNIX?

На самом деле это хуже, чем это: вы можете легко справиться с этой проблемой со статически связанными библиотеками. Это наличие границ кода внутри одного контекста выполнения, что позволяет случайно злоупотреблять или неправильно сообщать о каком-либо объекте.

Ответ 7

Хорошо известно, что

// in DLL a
DLLEXPORT MyObject* getObject() { return new MyObject(); }
// in DLL b 
MyObject *o = getObject();
delete o;

может привести к сбоям.

Независимо от того, имеет ли указанная выше характеристика определенная характеристика, зависит от того, как определяется тип MyObject.

Iff класс имеет виртуальный деструктор (и этот деструктор не определен inline), чем он не будет сбой и проявит четко определенное поведение.

Причина, по которой обычно приводятся причины сбоя, заключается в том, что delete выполняет две функции:

  • вызов деструктора
  • свободная память (путем вызова operator delete(void* ...))

Для класса с не виртуальным деструктором он может делать эти вещи "inline", что приводит к тому, что delete внутри DLL "b" может попытаться освободить память из "a" crap == crash.

Однако, если деструктор MyObject равен virtual, то перед вызовом "свободной" функции компилятор должен определить фактическое время выполнения класс указателя, прежде чем он сможет передать правильный указатель на operator delete():

С++ указывает, что вы должны передать тот же самый адрес оператору удалить как новый оператор. Когда вы выделяете объект используя новый, компилятор неявно знает конкретный тип объект (который используется компилятором для передачи в правильную память например, размер для оператора new.)

Однако, если ваш класс имеет базу класс с виртуальным деструктором, и ваш объект удаляется через указатель на базовый класс, компилятор не знает конкретного типа на сайте вызова и, следовательно, не может вычислить правильный адрес для перейдите к оператору delete(). Почему, спросите вы? Потому что в присутствии множественное наследование, адрес указателей базового класса может быть отличается от адреса объектов в памяти.

Итак, что происходит в этом Дело в том, что когда вы удаляете объект с виртуальным деструктором, компилятор называет то, что называется удаляющим деструктором вместо обычной последовательности вызов нормального деструктора, за которым следует оператор delete() для восстановления память.

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

Кажется, что оба GCC (из связанной статьи) и MSVC достигают этого, вызывая как dtor, так и "свободную" функцию из контекста "удаления деструктора". И этот помощник по необходимости живет внутри вашей DLL и будет всегда использовать правильную кучу, даже если "a" и "b" имеют другую.

Ответ 8

Я написал статью об использовании пользовательских объектов удаления С++ 11 для unique_ptr для передачи объектов через границы DLL (или библиотеки общих объектов в Linux). Метод, описанный в статье, не "загрязняет" подпись unique_ptr с помощью делетера.