Набережных Сергей дата публикации 25-08-2003 15:10 Генерация и обработка исключений без подключения SysUtils
Существует определенный класс программ, для которых достаточно важным является размер. Как правило, это утилиты с ограниченной функциональностью, и при их написании авторы часто ограничиваются использованием модулей 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
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;
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г.
[Exception] [Консольные приложения] [Исключения (exceptions)]
Обсуждение материала [ 29-08-2003 12:07 ] 2 сообщения |