Рейтинг@Mail.ru

Выведение стека вызовов в строку в Delphi

Автор: Alex. Опубликовано в Программирование . просмотров: 12397

Рейтинг:  5 / 5

Звезда активнаЗвезда активнаЗвезда активнаЗвезда активнаЗвезда активна
 

 

Когда программа уже написана и работает на компьютере пользователя, становится практически невозможно отловить ошибку без просмотра стека вызовов. Ведь с помощью него вы сможете точно определить, где произошла ошибка, и узнать какие функции вызывались до этого. Платформы .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 в свой проект:

uses 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]

Как видите, остались только адрес точки, где произошёл вызов, полное имя функции, строка и имя юнита. Аналогичным образом вы сможете сформировать трассировку стека, снабдив её необходимой вам информацией.

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

Tags: Учебники по программированию Delphi

Добавить комментарий