

Рисунок 4.1. Логика работы оператора try…except…end
При написании программы вы можете использовать вложенные защищенные блоки, чтобы организовать локальную и глобальную обработку исключительных ситуаций. Концептуально это выглядит следующим образом:
try
//защищаемые операторы
try
//защищаемые операторы except
//локальная обработка исключительных ситуаций
end;
//защищаемые операторы
except
// глобальная обработка исключительных ситуаций end;
Исключительные ситуации внешнего защищенного блока, возникающие за пределами вложенного блока, обрабатываются внешней секцией except...end. Исключительные ситуации вложенного защищенного блока обрабатываются вложенной секцией except...end.
4.3.2. Распознавание класса исключительной ситуации
Распознавание класса исключительной ситуации выполняется с помощью конструкций
on <класс исключительной ситуации> do <оператор>;
которые записываются в секции обработки исключительной ситуации, например:
161

try
// вычисления с вещественными числами except
on EZeroDivide do ... ; // обработка ошибки деления на нуль
on EMathError do ... ; // обработка других ошибок вещественной математики end;
Поиск соответствующего обработчика выполняется последовательно до тех пор, пока класс исключительной ситуации не окажется совместимым с классом, указанным в операторе on. Как только обработчик найден, выпоняется оператор, стоящий за словом do и управление передается за секцию except...end. Если исключительная ситуация не относится ни к одному из указанных классов, то управление передается во внешний блок try...except...end и обработчик ищется в нем.
Обратите внимание, что порядок операторов on имеет значение, поскольку распознавание исключительных ситуаций должно происходить от частных классов к общим классам, иначе говоря, от потомков к предкам. С чем это связано? Сейчас поймете. Представьте, к чему приведет изменение порядка операторов on в примере выше, если принять во внимание, что класс EMathError является базовым для EZeroDivide. Ответ простой: обработчик EMathError будет поглощать все ошибки вещественной математики, в том числе EZeroDivide, в результате обработчик EZeroDivide никогда не выполнится.
На самом высоком уровне программы бывает необходимо перехватывать все исключительные ситуации, чтобы в случае какой-нибудь неучтенной ошибки корректно завершить приложение. Для этого применяется так называемый обработчик по умолчанию (default exception handler). Он записывается в секции except после всех операторов on и начинается ключевым словом else:
try
{ вычисления с вещественными числами } except
on EZeroDivide do { обработка ошибки деления на нуль };
on EMathError do { обработка других ошибок вещественной математики }; else { обработка всех остальных ошибок (обработчик по умолчанию) };
end;
Учтите, что отсутствие части else соответствует записи else raise, которое нет смысла использовать явно. Мы со своей стороны вообще не советуем вам пользоваться обработкой исключительных ситуаций по умолчанию, поскольку все ваши приложения будут строиться, как правило, на основе библиотеки VCL, в которой обработка по умолчанию уже предусмотрена.
4.3.3. Пример обработки исключительной ситуации
В качестве примера обработки исключительной ситуации рассмотрим две функции: StringToCardinal и StringToCardinalDef.
Функция StringToCardinal выполняет преобразование строки в число с типом Cardinal. Если преобразование невозможно, функция создает исключительную ситуацию класса
EConvertError.
162

function StringToCardinal(const S: string): Cardinal; var
I: Integer;
B: Cardinal; begin
Result := 0; B := 1;
for I := Length(S) downto 1 do begin
if not (S[I] in ['0'..'9']) then
raise EConvertError.Create(S + ' is not a valid cardinal value'); Result := Result + B * (Ord(S[I]) - Ord('0'));
B := B * 10; end;
end;
Функция StringToCardinalDef также выполняет преобразование строки в число с типом Cardinal, но в отличие от функции StringToCardinal она не создает исключительную ситуацию. Вместо этого она позволяет задать значение, которое возвращается в случае неудачной попытки преобразования:
function StringToCardinalDef(const S: string; Default: Cardinal = 0): Cardinal; begin
try
Result := StringToCardinal(S); except
on EConvertError do Result := Default;
end; end;
Для преобразования исходной строки в число используется определенная выше функция StringToCardinal. Если при преобразовании возникает исключительная ситуация, то она «поглощается» функцией StringToCardinalDef, которая в этом случае возвращает значение параметра Default. Если происходит какая-нибудь другая ошибка (не EConvertError), то управление передается внешнему блоку обработки исключительных ситуаций, из которого была вызвана функция StringToCardinalDef.
Пример очень прост, но хорошо демонстрирует преимущества исключительных ситуаций перед традиционной обработкой ошибок. Представьте более сложные вычисления, состоящие из множества операторов, в каждом из которых может произойти ошибка. Насколько сложной окажется обработка ошибок многочисленными операторами if и насколько простой оператором try.
4.3.4. Возобновление исключительной ситуации
В тех случаях, когда защищенный блок не может обработать исключительную ситуацию полностью, он выполняет только свою часть работы и возобновляет исключительную ситуацию с тем, чтобы ее обработку продолжил внешний защищенный блок:
try
// вычисления с вещественными числами except
on EZeroDivide do begin
// частичная обработка ошибки
raise; // возобновление исключительной ситуации end;
end;
Если ни один из внешних защищенных блоков не обработал исключительную ситуацию, то управление передается стандартному обработчику исключительной ситуации, завершающему приложение.
4.3.5. Доступ к объекту, описывающему исключительную ситуацию
163

При обработке исключительной ситуации может потребоваться доступ к объекту, описывающему эту ситуацию и содержащему код ошибки, текстовое описание ошибки и т.д. В этом случае используется расширенная запись оператора on:
on <идентификатор объекта> : <класс исключительной ситуации> do <оператор>;
Например, объект исключительной ситуации нужен для того, чтобы выдать пользователю сообщение об ошибке:
try
// защищаемые операторы except
on E: EOutOfMemory do ShowMessage(E.Message);
end;
Переменная E — это объект исключительной ситуации, ShowMessage — процедура модуля DIALOGS, отображающая на экране небольшое окно с текстом и кнопкой OK. Свойство Message типа string определено в классе Exception, оно содержит текстовое описание ошибки. Исходное значение для текста сообщения указывается при конструировании объекта исключительной ситуации.
Обратите внимание, что после обработки исключительной ситуации освобождение соответствующего объекта выполняется автоматически, вам этого делать не надо.
4.4. Защита выделенных ресурсов от пропадания
4.4.1. Утечка ресурсов и защита от нее
Программы, построенные с использованием механизма исключительных ситуаций, обязаны придерживаться строгих правил распределения и освобождения таких ресурсов, как память, файлы, ресурсы операционной системы.
Представьте ситуацию: подпрограмма распределяет некоторый ресурс, но исключительная ситуация прерывает ее выполнение, и ресурс остается не освобожденным. Даже подумать страшно, к чему может привести такая ошибка: утечка памяти, файловых дескрипторов, других ресурсов операционной системы. Следовательно, ресурсы нуждаются в защите от исключительных ситуаций. Для этого в среде Delphi предусмотрен еще один вариант защищенного блока:
// запрос ресурса try
//защищаемые операторы, которые используют ресурс finally
//освобождение ресурса
end;
Особенность этого блока состоит в том, что секция finally...end выполняется всегда независимо от того, происходит исключительная ситуация или нет. Если какой-либо оператор секции try...finally генерирует исключительную ситуацию, то сначала выполняется секция finally...end, называемая секцией завершения (освобождения ресурсов), а затем управление передается внешнему защищенному блоку. Если все защищаемые операторы выполняются без ошибок, то секция завершения тоже работает, но управление передается следующему за ней оператору. Обратите внимание, что секция finally...end не обрабатывает исключительную ситуацию, в ней нет ни средств ее обнаружения, ни средств доступа к объекту исключительной ситуации.
164

Рисунок 4.1. Логика работы оператора try…finally…end
Блок try...finally...end обладает еще одной важной особенностью. Если он помещен в цикл, то вызов из защищенного блока процедуры Break с целью преждевременного выхода из цикла или процедуры Continue с целью перехода на следующую итерацию цикла сначала обеспечивает выполнение секции finally...end, а затем уже выполняется соответствующий переход. Это утверждение справедливо также и для процедуры Exit (выход из подпрограммы).
Как показывает практика, подпрограммы часто распределяют сразу несколько ресурсов и используют их вместе. В таких случаях применяются вложенные блоки try...finally...end:
// распределение первого ресурса try
...
// распределение второго ресурса try
//использование обоих ресурсов finally
//освобождение второго ресурса
end;
...
finally
// освобождение первого ресурса end;
Кроме того, вы успешно можете комбинировать блоки try...finally...end и try...except...end для защиты ресурсов и обработки исключительных ситуаций.
4.5. Итоги
В этой главе вы узнали многое об исключительных ситуациях и способах борьбы с ними. Теперь ваши программы наверняка дадут достойный отпор не только самому неотесанному пользователю, но и его деревянному компьютеру. Это, кстати говоря, одно из необходимых
165

качеств, которые позволят отнести вашу программу к классу хороших. Позволим себе также напомнить, что здесь были рассмотрены только ошибки времени выполнения, поэтому не забудьте прочитать гл.10, где рассказано о борьбе с логическими ошибками.
Глава 5. Динамически загружаемые библиотеки
До сих пор создаваемые нами программы были монолитными и фактически состояли из одного выполняемого файла. Это, конечно, очень удобно, но не всегда эффективно. Если вы создаете не одну программу, а несколько, и в каждой из них пользуетесь общим набором подпрограмм, то код этих подпрограмм включается в каждую вашу программу. В результате достаточно большие общие части кода начинают дублироваться во всех ваших программах, неоправданно «раздувая» их размеры. Поддержка программ затрудняется, ведь если вы исправили ошибку в некоторой подпрограмме, то вам придется перекомпилировать и переслать потребителю целиком все программы, которые ее используют. Решение проблемы напрашивается само собой — перейти к модульной организации выполняемых файлов. В среде Delphi эта идея реализуется с помощью динамически загружаемых библиотек. Техника работы с ними рассмотрена в данной главе.
5.1. Динамически загружаемые библиотеки
Динамически загружаемая библиотека (от англ. dynamically loadable library) — это библиотека подпрограмм, которая загружается в оперативную память и подключается к использующей программе во время ее работы (а не во время компиляции и сборки). Файлы динамически загружаемых библиотек в среде Windows обычно имеют расширение .dll (от англ. Dynamic-Link Library). Для краткости в этой главе мы будем использовать термин динамическая библиотека, или даже просто библиотека, подразумевая DLL-библиотеку.
Несколько разных программ могут использовать в работе общую динамически загружаемую библиотеку. При этом операционная система в действительности загружает в оперативную память лишь одну копию библиотеки и обеспечивает совместный доступ к ней со стороны всех программ. Кроме того, такие библиотеки могут динамически загружаться и выгружаться из оперативной памяти по ходу работы программы, освобождая ресурсы системы для других задач.
Одно из важнейших назначений динамически загружаемых библиотек — это взаимодействие подпрограмм, написанных на разных языках программирования. Например, вы можете свободно использовать в среде Delphi динамически загружаемые библиотеки, разработанные в других системах программирования с помощью языков C и C++. Справедливо и обратное утверждение — динамически загружаемые библиотеки, созданные в среде Delphi, можно подключать к программам на других языках программирования.
5.2. Разработка библиотеки
5.2.1. Структура библиотеки
По структуре исходный текст библиотеки похож на исходный текст программы, за исключением того, что текст библиотека начинается с ключевого слова library, а не слова program. Например:
library SortLib;
После заголовка следуют секции подключения модулей, описания констант, типов данных, переменных, а также описания процедур и функций. Процедуры и функции — это главное, что должно быть в динамически загружаемой библиотеке, поскольку лишь они могут быть экспортированы.
Если в теле библиотеки объявлены некоторые процедуры,
166

procedure BubleSort(var Arr: array of Integer); procedure QuickSort(var Arr: array of Integer);
то это еще не значит, что они автоматически станут доступны для вызова извне. Для того чтобы это разрешить, нужно поместить имена процедур в специальную секцию exports, например:
exports BubleSort, QuickSort;
Перечисленные в секции exports процедуры и функции отделяются запятой, а в конце всей секции ставится точка с запятой. Секций exports может быть несколько, и они могут располагаться в программе произвольным образом.
Ниже приведен пример исходного текста простейшей динамически загружаемой библиотеки SortLib. Она содержит единственную процедуру BubleSort, сортирующую массив целых чисел методом «пузырька»:
library SortLib;
procedure BubleSort(var Arr: array of Integer); var
I, J, T: Integer; begin
for I := Low(Arr) to High(Arr) - 1 do for J := I + 1 to High(Arr) do
if Arr[I] > Arr[J] then begin
T := Arr[I]; Arr[I] := Arr[J]; Arr[J] := T;
end;
end;
exports BubleSort;
begin end.
Исходный текст динамически загружаемой библиотеки заканчивается операторным блоком begin...end, в который можно вставить любые операторы для подготовки библиотеки к работе. Эти операторы выполняются во время загрузки библиотеки основной программой. Наша простейшая библиотека SortLib не требует никакой подготовки к работе, поэтому ее операторный блок пустой.
5.2.2. Экспорт подпрограмм
Если бы мы смогли заглянуть внутрь компилированного файла библиотеки, то обнаружили бы, что каждая экспортируемая подпрограмма представлена там уникальным символьным именем. Эти имена собраны в таблицу и используются при поиске подпрограмм — с их помощью выполняется динамическая привязка записанных в программе команд вызова к адресам соответствующих процедур и функций в библиотеке. В качестве экспортного имени может выступать любая последовательность символов, причем между заглавными и строчными буквами делается различие.
В стандартном случае экспортное имя подпрограммы считается в точности таким, как ее идентификатор в исходном тексте библиотеки (с учетом заглавных и строчных букв). Например, если секция exports имеет следующий вид,
167

exports BubleSort;
то это означает, что экспортное имя процедуры будет ’BubleSort’. При желании это имя можно сделать отличным от программного имени, дополнив описание директивой name, например:
exports
BubleSort name 'BubleSortIntegers';
В итоге, экспортное имя процедуры BubleSort будет ’BubleSortIntegers’.
Экспортные имена подпрограмм должны быть уникальны в пределах библиотеки, поэтому их нужно всегда указывать явно для перегруженных (overload) процедур и функций. Например, если имеются две перегруженные процедуры с общим именем QuickSort,
procedure |
QuickSort(var |
Arr: |
array |
of |
Integer); overload; // |
для |
целых чисел |
|
procedure |
QuickSort(var |
Arr: |
array |
of |
Real); overload; |
// |
для |
вещественных |
то при экспорте этим двум процедурам необходимо явно указать отличные друг от друга экспортные имена:
exports
QuickSort(var Arr: array of Integer) name 'QuickSortIntegers'; QuickSort(var Arr: array of Real) name 'QuickSortReals';
Полные списки параметров нужны для того, чтобы компилятор мог разобраться, о какой процедуре идет речь в каждом случае.
5.2.3. Соглашения о вызове подпрограмм
В главе 2 мы уже кратко рассказывали о том, что в различных языках программирования используются различные правила вызова подпрограмм, и что для совместимости с ними в языке Delphi существуют директивы register, stdcall, pascal и cdecl. Применение этих директив становится особенно актуальным при разработке динамически загружаемых библиотек, которые используются в программах, написанных на других языках программирования.
Чтобы разобраться с применением директив, обратимся к механизму вызова подпрограмм. Он основан на использовании стека.
Стек — это область памяти, в которую данные помещаются в прямом порядке, а и извлекаются в обратном, по аналогии с наполнением и опустошением магазина патронов у стрелкового оружия. Очередность работы с элементами в стеке обозначается термином LIFO (от англ. Last In, First Out — последним вошел, первым вышел).
ПРИМЕЧАНИЕ
Существует еще обычная очередность работы с элементами, обозначаемая термином FIFO (от англ. First In, First Out — первым вошел, первым вышел).
Для каждой программы на время работы создается свой стек. Через него передаются параметры подпрограмм и в нем же сохраняются адреса возврата из этих подпрограмм. Именно благодаря стеку подпрограммы могут вызывать друг друга, или даже рекурсивно сами себя.
Вызов подпрограммы состоит из «заталкивания» в стек всех аргументов и адреса следующей команды (для воврата к ней), а затем передачи управления на начало подпрограммы. По окончании работы подпрограммы из стека извлекается адрес воврата с передачей управления на этот адрес; одновременно с этим из стека выталкиваются аргументы. Происходит так называемая очистка стека. Это общая схема работы и у нее бывают разные реализации. В частности, аргументы могут помещаться в стек либо в прямом порядке (слева направо, как они перечислены в описании подпрограммы), либо в обратном порядке (справа налево), либо вообще, не через стек, а через свободные регистры процессора для повышения скорости
168

работы. Кроме того, очистку стека может выполнять либо вызываемая подпрограмма, либо вызывающая программа. Выбор конкретного соглашения о вызове обеспечивают директивы register, pascal, cdecl и stdcall. Их смысл поясняет таблица 5.1.
Директива |
Порядок занесения |
Кто отвечает за очистку стека |
Передача |
|
аргументов в стек |
|
аргументов |
|
|
|
через регистры |
|
|
|
|
register |
Слева направо |
Подпрограмма |
Да |
pascal |
Слева направо |
Подпрограмма |
Нет |
cdecl |
Справа налево |
Вызывающая программа |
Нет |
stdcall |
Справа налево |
Подпрограмма |
Нет |
Таблица 5.1. Соглашения о вызове подпрограмм
ПРИМЕЧАНИЕ
Директива register не означает, что все аргументы обязательно передаются через регистры процессора. Если число аргументов больше числа свободных регистров, то часть аргументов передается через стек.
Возникает резонный вопрос: какое соглашение о вызове следует выбирать для процедур и функций динамически загружаемых библиотек. Ответ — соглашение stdcall:
procedure BubleSort(var Arr: array of Integer); stdcall; procedure QuickSort(var Arr: array of Integer); stdcall;
Именно соглашение stdcall, изначально предназначенное для вызова подпрограмм операционной системы, лучше всего подходит для взаимодействия программ и библиотек, написанных на разных языках программирования. Все программы так или иначе используют функции операционной системы, следовательно они обязательно поддерживают соглашение stdcall.
5.2.4. Пример библиотеки
Вооруженные теорией, приступим к практике — разработаем какую-нибудь полезную библиотеку, а затем подключим ее к своей программе. На этом примере мы покажем вам, как оформляется динамически загружаемая библиотека, составленная из нескольких программных модулей.
Шаг 1. Запустите систему Delphi и выберите в меню команду File | New | Other... . В диалоговом окне, которое откроется на экране, выберите значок с подписью DLL Wizard и нажмите кнопку OK (рисунок 5.1):
169

Рисунок 5.1. Окно выбора нового проекта, в котором выделен пункт DLL Wizard
Среда Delphi создаст новый проект со следующей заготовкой библиотеки:
library Project1;
{ Important note about DLL memory management ... }
uses SysUtils, Classes;
begin end.
Шаг 2. С помощью команды File | New | Unit создайте в проекте новый программный модуль. Его заготовка будет выглядеть следующим образом:
unit Unit1;
interface
implementation
end.
Шаг 3. Сохраните модуль под именем SortUtils.pas, а проект — под именем SortLib.dpr. Прейдите к главному файлу проекта и удалите из секции uses модули SysUtils и Classes (они сейчас не нужны). Главный программный модуль должен стать следующим:
library SortLib;
{ Important note about DLL memory management ... }
uses
SortUtils in 'SortUtils.pas';
begin end.
Шаг 4. Наберите исходный текст модуля SortUtils:
170