суббота, 1 июня 2019 г.

Выделение и освобождение памяти между исполняемыми модулями

Это перевод Allocating and freeing memory across module boundaries. Автор: Реймонд Чен.

Я уверен, что вам уже вдолбили в голову, что память нужно освобождать тем же менеджером памяти, который её и выделил. LocalAlloc соответствует LocalFree, GlobalAlloc соответствует GlobalFree, GetMem соответствует FreeMem. Но отсюда следуют интересные выводы.

Если у вас есть функция, которая выделяет и возвращает некоторые данные - вызывающая сторона должна знать, как освободить эту память. Есть множество способов добиться этого. Один из них состоит в том, чтобы чётко указать, как память должна быть освобождена. Например, документация FormatMessage прямо заявляет: вы должны использовать функцию LocalFree, чтобы освободить буфер, выделенный функцией при передаче флага FORMAT_MESSAGE_ALLOCATE_BUFFER. Все BSTR должны быть освобождены с помощью SysFreeString. И вся память, передаваемая через границы интерфейса COM, должна быть выделена и освобождена с помощью менеджера памяти COM.

Но заметьте, что если вы решите, что блок памяти должен быть освобожден с помощью run-time библиотеки языка (например, с помощью FreeMem), то у вас возникнет новая проблема: с помощью какой библиотеки?

Если вы решите скомпоновать ваш модуль со статической библиотекой RTL (т.е. без run-time пакетов BPL), то у вашего модуля будет своя собственная копия библиотеки RTL. Когда ваш модуль вызывает GetMem или New, память может быть освобождена только вашим модулем, вызывающим FreeMem или Dispose. Если же другой модуль вызовет FreeMem или Dispose - он будет использовать свою собственную библиотеку RTL, отличную от вашей. Действительно, даже если вы решите скомпоновать ваш модуль с run-time пакетами (т.е. с RTL в BPL), вам всё равно придётся договориться о том, какую версию RTL использовать. Если ваша DLL использует rtl70.bpl для выделения памяти, то любой, кто хочет освободить эту память, также должен использовать rtl70.bpl.

Если вы внимательно следили за рассуждениями, то могли обнаружить надвигающуюся проблему. Требование использовать определенную версию RTL всеми вашими клиентами может показаться разумным, если вы управляете всеми клиентами и готовы перекомпилировать их все при каждом изменении компилятора. Но в реальной жизни люди часто не хотят идти на такой риск. «Если это не сломано, то и нечего его чинить». При переходе на новый компилятор есть риск появления ошибок в программе.

На практике вы можете захотеть собирать в новом компиляторе только часть вашей программы, оставив старые модули в покое (например, вы можете использовать преимущества новых языковых функций, которые доступны только в новом компиляторе). Но если вы сделаете это, то потеряете способность освобождать память, выделенную старой DLL, так как эта DLL ожидает, что вы будете использовать rtl70.bpl, тогда как новый компилятор использует rtl220.bpl.

Решение этой проблемы требует предварительного планирования. Одним из вариантов является использование фиксированного внешнего менеджера памяти, такого как LocalAlloc или CoTaskMemAlloc. Эти менеджеры памяти доступны всегда и всем, вне зависимости от используемого компилятора.

Другой вариант - обернуть ваш предпочитаемый менеджер памяти в экспортируемую функцию, которая будет вызывать функции менеджера памяти. К примеру, такой механизм используется семейством функций NetApi. Например, функция NetGroupEnum выделяет память и возвращает её через параметр bufptr. Когда вызывающая сторона заканчивает работу с памятью, она освобождает её с помощью функции NetApiBufferFree. Таким образом, используемый менеджер памяти изолирован от вызывающей стороны. Внутренне функции NetApi могут использовать LocalAlloc или HeapAlloc или, возможно, даже GetMem и FreeMem. Это не имеет значения; главное - что NetApiBufferFree освобождает память через тот же менеджер памяти, что использовал NetGroupEnum.

Хотя я лично предпочитаю использовать фиксированный менеджер памяти, многим разработчикам удобнее использовать технику обёртки. Таким образом, они могут использовать свой любимый менеджер памяти во всём модуле. Всё будет работать в любом случае. Смысл тут в том, что когда память покидает вашу DLL, код, которому вы передали памяти, должен знать, как её освободить, даже если он использует компилятор, отличный от того, который использовался для сборки вашей DLL.

Читать далее: Разработка API (контракта) для своей DLL.

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

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

Примечание. Отправлять комментарии могут только участники этого блога.