Delphi. Распределение памяти и динамические библиотеки

Delphi. Распределение памяти и динамические библиотеки

Давайте сначала рассмотрим комментарий от "Борланд", создаваемый мастером динамических библиотек DLL.

Важное примечание об управлении памятью в DLL: модуль ShareMem должен быть указан первым в разделе USES вашей библиотеки и в этом же разделе вашего проекта (пункт меню Project -> View Source), если ваша DLL экспортирует любые процедуры или функции, которые передают строки как результат или используют как параметры. Это относится ко всем строкам, передаваемым или получаемым вашей библиотекой — даже тем, которые используются в записях (records) и классах (classes). ShareMem — это интерфейс к библиотеке BORLNDMM.DLL, менеджеру совместного использования памяти, который должен быть развернут наряду с вашим проектом. Чтобы избегать использовать BORLNDMM.DLL, передавайте строки как PChar или ShortString.

Почему эти предосторожности необходимы?
Причина скрывается в способе Delphi выделять и использовать память. В то время как Windows предоставляет родные функции распределения памяти (VirtualAlloc, HeapAlloc, GlobalAlloc, LocalAlloc и т.д.), "Дельфи" осуществляет его собственную политику распределения, или, более точно, существует свой менеджер памяти, который осуществляет ее подраспределение. В языке "Паскаль" ("Дельфи") это называют кучей (Heap); C/C++-программисты более знакомы с термином free store. Задача подраспределителя состоит в том, чтобы разместить всю динамическую память: от всей памяти, явно размещенной программистом, до неявно размещенной компилятором при создании строк, динамических массивов и объектов.
Немногие из разработчиков понимают, что они неявно выделяют память в утверждениях типа:

var s: string
...
s: = s + "abc";

Функции динамического распределения памяти, с которыми большинство пользователей "Дельфи" знакомы, — это GetMem(), FreeMem(), New() и Dispose(). Но фактически многие кажущиеся простыми действия в "Дельфи" приводят к выделению или освобождению памяти кучи. К ним можно отнести: создание объектов с использованием конструктора, строковые переменные и действия над ними, действия над типами данных shortstring, создание и изменение размеров динамических массивов, строковые значения в вариантных переменных, явное распределение памяти функциями GetMem(), FreeMem(), New() и Dispose().
В "Дельфи" все объекты "живут" в куче. Это подобно Java и C#, но не относится к C++, где объекты могут существовать и в стеке, и в куче, и даже в сегменте данных. Разработчики, знакомые с программированием под Windows еще в бытность оной 16-битной, могут задаться вопросом, почему "Дельфи" не использует кучу Windows (через HeapCreate(), HeapAlloc(), HeapFree() и т.д.) или даже через виртуальные функции памяти VirtualAlloc() и VirtualFree(). Причина этого проста — скорость. Функции обслуживания кучи Windows очень медленны по сравнению с родным распределением памяти из "Дельфи". Виртуальное распределение памяти работает еще медленнее, но это только потому, что они не были предназначены для ассигнования большого количества маленьких блоков (для чего, собственно, куча и предназначена). Однако менеджер распределения памяти "Дельфи" в конечном счете все равно вызывает эти виртуальные функции памяти, когда требуются для работы большие блоки памяти, чтобы потом их перераспределять как более мелкие.
Код менеджера памяти находится в модулях System.pas и GetMem.inc, так что компилируется с каждой программой. В общем случае это не является проблемой, но в приложениях, использующих библиотеки, также написанные в "Дельфи", это имеет некоторое значение. Так как DLL — отдельно компилируемое приложение, библиотека получает свою собственную копию менеджера памяти и, таким образом, отдельную кучу. Это — самая важная вещь, которую необходимо помнить: каждое отдельное приложение, будь то исполняемый .exe-файл или динамическая библиотека .dll, управляется его собственным менеджером памяти, обладающим своей кучей. Все последующие проблемы просто возникают из-за наличия одного приложения, .exe либо .dll, которое по ошибке управляет частью памяти, совершенно не принадлежащей его собственной куче.

Что такое "куча"?
Для тех, кто не знаком с понятием кучи и его использованием в "Дельфи": куча — это область памяти, в которой хранится динамически выделенная приложением память. В наиболее структурированных языках, подобных C и C++, "Дельфи", и даже в новом C# от Microsoft программист может использовать два вида памяти: статический и динамический. Основные типы данных, называемые также типами значений, являются статическими, и их требуемые объемы и конфигурации памяти известны и устанавливаются во время компиляции. Целые числа "Дельфи", перечислительные типы, записи и статические множества — примеры статических переменных. В C все типы данных являются статическими. Поэтому программист должен явно выделить динамическую память через некоторую форму функции распределения памяти подобно malloc(). Размер динамической памяти, с другой стороны, может корректироваться во время выполнения приложения. Пример того — длинные строки "Дельфи", тип class и динамические множества. В Visual Basic'е многие типы данных размещаются динамически, и среди них также присутствуют переменные типа Variant и динамические множества. Как правило, любой тип данных, размер которого может быть изменен во времени выполнения, может динамически размещаться в памяти. С точки зрения компилятора эти два вида памяти являются очень разными и "живут" в совершенно разных секциях памяти приложения: глобальные статически размещенные переменные "живут" в глобальной статической области данных, локальные статически размещенные переменные "живут" в стеке, а динамические блоки памяти "живут" в куче. Фактически такое разделение размещения объектов в памяти заложено в самую основу современного программирования и распространяется до самых недр операционных систем и далее — до самых аппаратных средств ЭВМ непосредственно. Вот почему многие чипы (например, семейство Intel x86) имеют поддержку сегментов стека.
Последняя строка в сгенерированном автоматически комментарии заслуживает отдельного внимания: передавайте строковую информацию, используя PChar- или ShortString-параметры. Это, как кажется, предполагает, что использование PChar или ShortString может решить проблему. Однако это — ошибочное мнение и часто вводит в заблуждение программистов, может убаюкать даже опытных разработчиков и дать им ложное чувство безопасности. Но давайте рассмотрим такой пример:

В DLL:
function GetPChar: PChar;
begin
result: = StrAlloc(13);
StrCopy (result, 'Привет, Мир!');
end;

В EXE:
var p: PChar;
...
p := GetPChar;
// чо-то делаем
StrDispose(p); // Вот тут ошибка — возможно, куча DLL повреждена, появляется сообщение "Invalid pointer operation".

И снова причина ошибки — сложившееся восприятие о типе PChar, которое предполагает, что "программный интерфейс приложения (то есть попросту API) Windows делает это таким образом, и значит, это правильно". Но программный интерфейс приложения Windows очень редко размещает PChar для того, чтобы передать его к приложению. От вызывающего приложения требуется выделить для PChar буфер и передать параметр, определяющий его длину, а API тогда сам пишет в этот буфер. Фактически есть очень небольшое, но все-таки весомое преимущество для использования PChar в "Дельфи", так как такой способ передачи строки намного более безопасен и более эффективен. Только очень продвинутые пользователи, имеющие точное и кристально ясное понимание причин так делать, должны их использовать.
При передаче объектов наблюдаем схожую картину:

В DLL:
function GetPChar: PChar;
begin
Result := StrAlloc( 13 );
StrCopy( Result, 'Привет, Мир!' );
end;

procedure FreePChar( p: PChar );
begin
StrDispose( p );
end;

В EXE:
var p: PChar;
...
p:= GetPChar;
// что-то делаем
FreePChar( p ); // безвредное для кучи освобождение ресурсов.

В зависимости от того, какое действие совершила исполняемая программа по отношению к объекту, это может причинить ущерб не одной, а сразу обеим кучам. Обратите внимание, что в "Дельфи 6" модуль, освободивший память из кучи другого модуля, фактически может и не испортить структуру кучи. Менеджер кучи держит свободные блоки памяти в связном списке и в моменты удаления блоков пытается сливать два смежных свободных блока. "Invalid pointer operation" происходит тогда, когда модуль пытается освободить последний выделенный блок списка освобождения памяти другого модуля и, будучи не в состоянии распознать некий ошибочный его элемент, пытается слить свободный блок с тем, что, как ему кажется, является маркером (а может быть, и просто мусором), что и приводит к ошибке. Хотя ошибка не выходит за рамки текущего экземпляра, дальнейшая работа менеджера кучи может быть нарушена уже в любом другом месте из-за искажений в структуре кучи.
Вышеупомянутый пример использования PChar может быть "исправлен" следующим образом:

В DLL:
function GetPChar: PChar;
begin
result: = StrAlloc (13);
StrCopy (result, 'Привет Мир!');
end;

procedure FreePChar (p: PChar);
begin
StrDispose (p);
end;

В EXE:
var p: PChar;
...
p: = GetPChar;
// сделать кое-что
FreePChar (p); // безвредное для кучи освобождение ресурсов

Но нет никакой эквивалентной замены "исправлению" при работе с TStringList, так как строки, созданные в пределах кучи исполняемой программы, могут быть освобождены деструктором TStringList, нанося вред куче исполняемой программы.

Так что же является надлежащим решением?

Существует несколько вариантов. Первый — использовать модули Sharemem.pas и Borlndmm.dll. Это, кажется, самое безопасное и простое решение, которое не требует никаких специальных предосторожностей. Есть, однако, одно замечание: все запросы выделения памяти через модуль Borlndmm.dll могут работать в 2-8 раз медленнее, чем "нормальное" выделение памяти. Описанный способ также требует распространения модуля управления памятью вместе с вашим приложением.
Второй — никогда не передавайте динамические данные между модулями. Это довольно трудно для понимания и требует глубоких знаний системы типов и классов в "Дельфи". Когда в разработку вовлечено большое количество разработчиков, этого особенно трудно избежать.
Разрабатывайте все библиотеки, как будто они были написаны на других языках программирования и/или с использованием API. Такой подход разумен в том случае, если проект пишется более, чем в одном языке. Тогда простой отказ от любых Delphi-ориентированных конструкций во всех DLL не сможет добавить еще больше сложности в написании. Неудобство — то, что много конструкций "Дельфи" будут недоступны.
Четвертое. Использование объектов COM в DLL: это экстремальное решение, особенно если COM будет использоваться для обеспечения межбиблиотечных связей. Это также замедляет работу приложения и выливается в значительные усилия при разработке.
Есть и пятый выбор. Модуль FastShareMem — это попытка построения альтернативного решения. Он очень прост в использовании — не сложнее, чем включение еще одного модуля в проект, — и не вызывает никакого замедления работы приложения. Более полно об этом модуле я расскажу в следующей статье.

Денис "Denver" Мигачев,
dtm@tut.by



Компьютерная газета. Статья была опубликована в номере 37 за 2003 год в рубрике программирование :: delphi

©1997-2024 Компьютерная газета