Генерация и обработка исключений без подключения SysUtils
й, дата публикации 25 августа 2003г. |
Существует определенный класс программ, для которых достаточно важным является размер. Как правило, это утилиты с ограниченной функциональностью, и при их написании аничиваются использованием модулей Windows и Messages. Однако, при этом нередко хотелось бы иметь полноценный сервис обработки исключений, не утяжеляя проект модулем SysUtils. Попробуем решить эту задачу.
В Delphi работа с исключениями разделена на две части: собственно механизм генерации и обработки, расположенный в модуле System, и набор сервисных функций и классов, находящийся в SysUtils.
Реализация механизма в System сама по себе представляет немалый интерес, но ее рассмотрение выходит за рамки данной статьи. Нас интересует только небольшая ее часть, а именно процедура _ExceptionHandler. Это обработчик исключений, установленный при старте приложения, и получающий управление при генерации системой исключения – например, при вызове приложением функции RaiseException. _ExceptionHandler проводит ряд проверок, в зависимости от которых предпринимаются различные действия. Кратко рассмотрим только некоторые из них:
- Если это исключение сгенерировано Delphi-программой, то происходит переход к пункту 5.
- Если значение переменной ExceptObjProc не равно nil, то вызывается функция, адрес которой находится в этой переменной, иначе переход к пункту 4.
- Если вызванной в п. 2 функции удалось “подобрать” соответствующий класс исключений, то происходит переход к пункту 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; |
Аналог 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; |
В заключении хочу отметить, что рассмотренный пример актуален для узкого класса приложений. Использование модулей SysUtils и Forms вносит в работу с исключениями весьма существенные коррективы.
Набережных Сергей
25 августа 2003г.