Когда программа уже написана и работает на компьютере пользователя, становится практически невозможно отловить ошибку без просмотра стека вызовов. Ведь с помощью него вы сможете точно определить, где произошла ошибка, и узнать какие функции вызывались до этого. Платформы .Net и Java имеют встроенную поддержку трассировки стека в классе Exception. Вы просто вызываете Exception.StackTrace (в .NET) или Exception.getStackTrace (в Java) и получаете детальную информацию о стеке. В Delphi с трассировкой стека всё не так просто. Давайте разбираться.
В Delphi 2009 у класса Exception появилось свойство StackTrace, и вы можете подумать, что при возникновении ошибки вы с помощью него сможете получить стек вызовов. Но вы ошибаетесь. Свойство StackTrace, не будет работать без подключения поставщика трассировки стека, которого по умолчанию в Delphi нет. Можете проверить: свойство всегда будет возвращать пустую строку.
Инструменты для формирования отчётов об ошибках, такие как Eurekalog или madExcept, или помощники по отладке, такие как JclDebug могут регистрировать себя в качестве поставщиков и возвращать трассировку стека при возникновении ошибок. Давайте опробуем в действии бесплатную библиотеку JEDI Code Library (JCL) (последнюю версию библиотеки можно скачать здесь). Чтобы свойство StackTrace автоматически начало возвращать стек вызовов, установите библиотеку себе на компьютер и просто подключите юнит JclDebug в свой проект:
Вот простой пример, где я подключил юнит JclDebug и сохраняю в текстовый файл трассировку стека любой ошибки, произошедшей в программе.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
Vcl.StdCtrls, System.IOUtils;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
procedure ShowException(Sender: TObject; E: Exception);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses JclDebug;
procedure TForm1.FormCreate(Sender: TObject);
begin
//Подписываемся на событие, чтобы самостоятельно обрабатывать ошибки приложения.
Application.OnException := ShowException;
//Кидаем ошибку.
raise Exception.Create('Ошибка!');
end;
procedure TForm1.ShowException(Sender: TObject; E: Exception);
begin
//Записываем в файл трассировку стека.
TFile.AppendAllText('stacktrace.txt', E.StackTrace);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
//Отписываемся от события.
Application.OnException := nil;
end;
end.
После выполнения примера в файл stacktrace.txt добавится следующий стек вызовов:
(001F8CA8){Project2.exe} [005F9CA8] Unit1.TForm1.FormCreate$qqrp14System.TObject (Line 36, "Unit1.pas" + 5) + $0
(001B56E9){Project2.exe} [005B66E9] Vcl.Forms.TCustomForm.DoCreate$qqrv (Line 3758, "Vcl.Forms.pas" + 3) + $C
(001B5309){Project2.exe} [005B6309] Vcl.Forms.TCustomForm.AfterConstruction$qqrv (Line 3642, "Vcl.Forms.pas" + 1) + $D
(001B52BB){Project2.exe} [005B62BB] Vcl.Forms.TCustomForm.$bctr$qqrp25System.Classes.TComponent (Line 3632, "Vcl.Forms.pas" + 35) + $2B
(001C023E){Project2.exe} [005C123E] Vcl.Forms.TApplication.CreateForm$qqrp17System.TMetaClasspv (Line 10557, "Vcl.Forms.pas" + 13) + $B
(0020265D){Project2.exe} [0060365D]
Как видите трассировка достаточно подробная. Давайте разберёмся, что здесь приходит. Рассмотрим первую строку. В квадратных скобках указан адрес точки, где произошёл вызов, у меня это [005FE5F4]. В круглых скобках – смещение до этой точки от начала кода в модуле, здесь это, (001FD5F4). В фигурных скобках указано имя модуля, в моём случае - {Project2.exe}. Затем идёт полное имя функции Unit1.TForm1.FormCreate$qqrp14System.TObject и справа от него в скобках - строка и имя юнита: Line 37, "Unit1.pas". После имени юнита тоже в скобках указано смещение от строки с именем метода до строки, в которой произошёл вызов, у меня это, + 5. В самом конце указано смещение от адреса, с которой начинается строка, до адреса, в которой произошёл вызов в шестнадцатеричной системе счисления: в первой строке это $0, во второй - $C.
Кому-то это количество информации покажется избыточным, а кому-то захочется добавить чего-то ещё. В примере ниже, я продемонстрирую, как создать своего поставщика используя юнит JclDebug.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
Vcl.StdCtrls, System.IOUtils;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
procedure ShowException(Sender: TObject; E: Exception);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses JclDebug;
procedure TForm1.FormCreate(Sender: TObject);
begin
//Подписываемся на событие, чтобы самостоятельно обрабатывать ошибки приложения.
Application.OnException := ShowException;
//Кидаем ошибку.
raise Exception.Create('Ошибка!');
end;
procedure TForm1.ShowException(Sender: TObject; E: Exception);
begin
//Записываем в файл трассировку стека.
TFile.AppendAllText('stacktrace.txt', E.StackTrace);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
//Отписываемся от события.
Application.OnException := nil;
end;
function GetExceptionStackInfo(P: PExceptionRecord): Pointer;
const
cDelphiException = $0EEDFADE;
var
stack: TJclStackInfoList;
strings: TStringList;
text: string;
size: integer;
begin
Result := nil;
//Получаем трассировку стека.
if P^.ExceptionCode = cDelphiException then
stack := JclCreateStackList(false, 3, P^.ExceptAddr)
else
stack := JclCreateStackList(false, 3, P^.ExceptionAddress);
//Формируем строку.
strings := TStringList.Create;
try
//Здесь можно с помощью последних четырёх параметров задать, какую информацию нужно записать в строку.
//Я отключу всю дополнительную информацию.
stack.AddToStrings(strings, false, false, false, false);
text := strings.Text;
finally
strings.Free;
end;
//Выделяем память и копируем в неё строку с трассировкой стека.
if not text.IsEmpty then
begin
size := (text.Length + 1) * SizeOf(char);
GetMem(Result, size);
Move(Pointer(text)^, Result^, size);
end;
end;
function GetStackInfoString(Info: Pointer): string;
begin
//Здесь отдаём строку со стеком вызовов сохранённую в функции GetExceptionStackInfo.
Result := PChar(Info);
end;
procedure CleanUpStackInfo(Info: Pointer);
begin
//Освобождаем память, занятую под строку со стеком.
FreeMem(Info);
end;
initialization
//Указываем свои функции для управления трассировкой стека.
Exception.GetExceptionStackInfoProc := GetExceptionStackInfo;
Exception.GetStackInfoStringProc := GetStackInfoString;
Exception.CleanUpStackInfoProc := CleanUpStackInfo;
finalization
//Снимаем указатели на свои функции для управления трассировкой стека.
Exception.GetExceptionStackInfoProc := nil;
Exception.GetStackInfoStringProc := nil;
Exception.CleanUpStackInfoProc := nil;
end.
В результате в файл stacktrace.txt добавится следующая информация.
[005F9CB4] Unit1.TForm1.FormCreate$qqrp14System.TObject (Line 36, "Unit1.pas")
[005B66F5] Vcl.Forms.TCustomForm.DoCreate$qqrv (Line 3758, "Vcl.Forms.pas")
[005B6315] Vcl.Forms.TCustomForm.AfterConstruction$qqrv (Line 3642, "Vcl.Forms.pas")
[005B62C7] Vcl.Forms.TCustomForm.$bctr$qqrp25System.Classes.TComponent (Line 3632, "Vcl.Forms.pas")
[005C124A] Vcl.Forms.TApplication.CreateForm$qqrp17System.TMetaClasspv (Line 10557, "Vcl.Forms.pas")
(0020268D) [0060368D]
Как видите, остались только адрес точки, где произошёл вызов, полное имя функции, строка и имя юнита. Аналогичным образом вы сможете сформировать трассировку стека, снабдив её необходимой вам информацией.
И в заключении хочу сказать, что сохраняя трассировку стека, например, в файл журнала, вы намного упростите себе поиск неисправностей в программе в дальнейшем.