Управление памятью / кучей в разных DLL - PullRequest
20 голосов
/ 15 февраля 2010

Хотя это кажется очень распространенной проблемой, я не собирал много информации: как я могу создать безопасный интерфейс между границами 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 тоже "применены"?

Ответы [ 8 ]

13 голосов
/ 15 февраля 2010

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

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

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

9 голосов
/ 15 февраля 2010

Перегрузка operator new, operator delete и др. Для всех ваших классов 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. Честно говоря, я не уверен, есть ли у него серьезные подводные камни или проблемы с переносимостью - но это работает для меня.

5 голосов
/ 15 февраля 2010

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

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

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

3 голосов
/ 15 февраля 2010
2 голосов
/ 15 февраля 2010

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

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

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

1 голос
/ 24 апреля 2014

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

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

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

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

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

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

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

Для класса с не виртуальным деструктором, он может делать эти вещи «встроенными», что приводит к ситуации, когда delete внутри DLL «b» может попытаться освободить память от кучи «a» == сбой .

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

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

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

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

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

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

1 голос
/ 15 февраля 2010

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

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


Эта проблема ограничена только DLL или общие объекты UNIX тоже "применены"?

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

0 голосов
/ 29 января 2014

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...