Статьи Королевства Дельфи

       

Генерация и обработка исключений без подключения SysUtils


й,
дата публикации 25 августа 2003г.


Существует определенный класс программ, для которых достаточно важным является размер. Как правило, это утилиты с ограниченной функциональностью, и при их написании аничиваются использованием модулей Windows и Messages. Однако, при этом нередко хотелось бы иметь полноценный сервис обработки исключений, не утяжеляя проект модулем SysUtils. Попробуем решить эту задачу.

В Delphi работа с исключениями разделена на две части: собственно механизм генерации и обработки, расположенный в модуле System, и набор сервисных функций и классов, находящийся в SysUtils.

Реализация механизма в System сама по себе представляет немалый интерес, но ее рассмотрение выходит за рамки данной статьи. Нас интересует только небольшая ее часть, а именно процедура _ExceptionHandler. Это обработчик исключений, установленный при старте приложения, и получающий управление при генерации системой исключения – например, при вызове приложением функции RaiseException. _ExceptionHandler проводит ряд проверок, в зависимости от которых предпринимаются различные действия. Кратко рассмотрим только некоторые из них:

  1. Если это исключение сгенерировано Delphi-программой, то происходит переход к пункту 5.
  2. Если значение переменной ExceptObjProc не равно nil, то вызывается функция, адрес которой находится в этой переменной, иначе переход к пункту 4.
  3. Если вызванной в п. 2 функции удалось “подобрать” соответствующий класс исключений, то происходит переход к пункту 5.
  4. Так как исключение осталось “неопознанным”, происходит нотификация пользователя и аварийное завершение процесса.
  5. Если значение переменной ExceptProc не равно nil, то вызывается процедура, адрес которой находится в этой переменной, иначе переход к пункту 4.

Таким образом, нас интересуют две переменные: ExceptObjProc и ExceptProc. Заглянем в SysUtils, чтобы посмотреть, как они используются в нем. В секции инициализации этот модуль присваивает им адреса функции GetExceptionObject и процедуры ExceptHandler соответственно. Первая из них пытается подобрать по коду ошибки соответствующий класс исключения и, при удаче, возвращает его экземрляр. Вторая производит нотификацию пользователя, используя строку сообщения из объекта, и вызывает Halt с кодом 1.

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

Прежде всего, нам необходим базовый класс исключения, по аналогии с Exception из SysUtils. Ниже приводится один из возможных вариантов его реализации:

interface uses Windows; type TLogHandler = procedure (ExceptObject: TObject; ExceptAddr: Pointer); LException = class private FExceptAddress: Pointer; protected function GetExceptionMessage: string; virtual; abstract; function GetExceptionTitle: string; virtual; property ExceptionAddress: Pointer read FExceptAddress; procedure ShowException; virtual; public property ExceptionMessage: string read GetExceptionMessage; property ExceptionTitle: string read GetExceptionTitle; function GetAddrString: string; end; var LogHandler: TLogHandler = nil; implementation { LException } function LException.GetAddrString: string; const CharBuf: array[0..15] of Char = '0123456789ABCDEF'; var BufLen: integer; Value: Cardinal; begin BufLen:=Succ(SizeOf(FExceptAddress) shl 1); SetLength(Result, BufLen); Result[1]:='$'; Value:=Cardinal(FExceptAddress); while BufLen > 1 do begin Result[BufLen]:=CharBuf[Value and $F]; Value:=Value shr 4; Dec(BufLen); end; end; function LException.GetExceptionTitle: string; begin Result:='Error'; end; procedure LException.ShowException; begin MessageBox(0, PChar(ExceptionMessage), PChar(ExceptionTitle), MB_ICONERROR or MB_TASKMODAL); end;

Раз уж мы внедряемся в обработку исключений, то почему бы не предусмотреть заодно и механизм ведения лога ошибок? Для этого и предусмотрен тип TLogHandler и переменная LogHandler. Остальной код прост и вряд ли нуждается в комментариях.

Далее, нам необходимо описать наш обработчик и присвоить его адрес переменной:


type TExceptHandler = TLogHandler; var OldHandler: TExceptHandler; procedure ExceptHandler(ExceptObject: TObject; ExceptAddr: Pointer); begin if Assigned(LogHandler) then try LogHandler(ExceptObject, ExceptAddr); except end; if ExceptObject is LException then begin LException(ExceptObject).FExceptAddress:=ExceptAddr; LException(ExceptObject).ShowException; Halt(1); end else if Assigned(OldHandler) then OldHandler(ExceptObject, ExceptAddr); end; procedure InitProc; begin OldHandler:=TExceptHandler(ExceptProc); ExceptProc:=@ExceptHandler; end; procedure FinalProc; begin TExceptHandler(ExceptProc):=OldHandler; end; initialization InitProc; finalization FinalProc;
Как видите, этот код не сложнее предыдущего. Прежде всего, мы вызываем обработчик логов, подстраховавшись от возможных ошибок блоком try-except. На всякий случай, все-таки ведение логов – не то место, где позволительно допускать ошибки. Далее мы проверяем, является ли объект исключения “нашим”, то есть потомком класса LException. Если это так, мы вызываем его методы и завершаем программу вызовом Halt. В противном случае мы вызываем предыдущий в цепочке обработчик. В секции инициализации мы устанавливаем свой обработчик, сохранив адрес предыдущего, а в секции финализации все восстанавливаем в первоначальном виде.

Аналог GetExceptionObject требуется реже и его реализация не представляет какой-либо сложности, поэтому я оставляю это читателям.

В качестве примера рассмотрим вариант реализации класса исключения для консольного приложения:

type LConsoleException = class(LException) private FMsg: string; protected function GetExceptionMessage: string; override; procedure ShowException; override; public constructor Create(Msg: string); end; { EConsoleException } constructor LConsoleException.Create(Msg: string); begin FMsg:=Msg; end; function LConsoleException.GetExceptionMessage: string; begin Result:=ExceptionTitle + ': ' + FMsg + ' at address ' + GetAddrString + #13#10; end; procedure LConsoleException.ShowException; var s: string; Len: DWORD; H: THandle; begin s:=ExceptionMessage; Len:=Length(s); H:=GetStdHandle(STD_ERROR_HANDLE); if H <> INVALID_HANDLE_VALUE then begin WriteConsole(H, PChar(s), Len, Len, nil); CloseHandle(H); end else inherited; end;
Мы переопределили конструктор, чтобы установить текст сообщения, и перекрыли два метода: GetExceptionMessage, чтобы отформатировать сообщение, и ShowException, чтобы перенаправить сообщение в стандартный вывод консоли. Генрация этого исключения вне защитного блока приведет к записи в стандартный вывод консоли сообщения об ошибке и завершению приложения. Если же его поместить в блок try-except, мы получим возможность вывести в консоль сообщение об ошибке и продолжить выполнение программы.

В заключении хочу отметить, что рассмотренный пример актуален для узкого класса приложений. Использование модулей SysUtils и Forms вносит в работу с исключениями весьма существенные коррективы.

Набережных Сергей
25 августа 2003г.



Содержание раздела