В Delphi, как и в других языках программирования, есть механизмы вызова, отлова и обработки исключений. В этой статье опишу основные инструменты работы с исключениями.
Отлов и обработка исключений
Для защиты кода от прерывания из-за ошибки существуют специальные блоки: try-except-end и try-finally-end. Различие блоков состоит в том, что код между словами except и end выполняется в случае возникновении ошибки между словами try и except, и код в блоке finally выполняется в любом случае, даже если в блоке try произошла ошибка.
Блок try-except-end используется для отлова непредвиденных и не декларируемых ошибок. В части except-end можно просто написать код, который будет отработан при возникновении любой исключительной ситуации, или описать реакции на различные классы ошибок (некоторые классы ошибок описаны тут).
try
//Защищенный код
except
//Что делать, при возникновении ошибки
end;
try
//Защищенный код
except
on E:EDivByZero do
begin
//Если было деление на ноль
end;
on E: Exception do
begin
//Любая другая ошибка
end;
end;
Блок try-finally-end чаще всего используется для защищенной очистки памяти, например:
//Начиная с версии 10.3.3 доступно inline объявление переменных
var StrList := TStringList.Create;
try
//Работа с StrList
finally
FreeAdnNil(StrList);
end;
Если вдруг при работе со списком StrList возникнет ошибка, программа автоматически перейдет к коду FreeAdnNil(StrList), очистив память. Без использования блока try-finally-end есть риск обрасти утечками памяти.
Иногда бывает необходимость и обработать ошибки и очистить память, т.е. скомбинировать два типа блоков. В Delphi не существует единого блока try-except-finally, как в некоторых других языках, поэтому данную задачу можно решить следующим способом:
var StrList := TStringList.Create;
try
try
//Программный код
except on E: Exception do
//Обработка ошибки
end;
finally
//Очистка памяти
FreeAndNil(StrList);
end;
Если вы уверены, что в блоке except при обработке ошибки возникновение ошибки исключено, то данный код можно сократить:
var StrList := TStringList.Create;
try
//Программный код
except on E: Exception do
//Обработка ошибки
end;
//Очистка памяти
FreeAndNil(StrList);
Поднятие (вызов) исключения
Иногда требуется вызвать спое собственное исключение, сообщая о той или иной ошибке. Для этого существует ключевое слово raise:
raise Exception.Create(‘Error Message’);
Вместо класса Exception может быть применен любой класс ошибки.
Обработка глобальных исключений
У класса Application есть событие OnException, которое срабатывает при возникновении исключений в любом месте основного потока программы. Его можно использовать, например, для логирования всех ошибок:
procedure TForm1.LogExceptoin(Sender: TObject; E: Exception);
begin
Memo1.Lines.Add(E.Message);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Application.OnException := LogExceptoin;
end;
Handling errors in Delphi |
||||
Whilst we all want to spend our time writing functional code, errors will and do occur in code from time to time. Sometimes, these are outside of our control, such as a low memory situation on your PC. |
||||
In serious code you should handle error situations so that at the very least, the user is informed about the error in your chosen way. |
||||
Delphi uses the event handling approach to error handling. Errors are (mostly) treated as exceptions, which cause program operation to suspend and jump to the nearest exception handler. If you don’t have one, this will be the Delphi default handler — it will report the error and terminate your program. |
||||
Often, you will want to handle the error, and continue with your program. For example, you may be trying to display a picture on a page, but cannot find it. So you might display a placeholder instead. Much like Internet Explorer does. |
||||
Try, except where there are problems |
||||
Delphi provides a simply construct for wrapping code with exception handling. When an exception occurs in the wrapped code (or anything it calls), the code will jump to the exception handling part of the wrapping code : |
||||
|
||||
We literally try to execute some code, which will run except when an error (exception) occurs. Then the except code will take over. |
||||
Let us look at a simple example where we intentionally divide a number by zero : |
||||
|
||||
When the division fails, the code jumps to the except block. The first ShowMessage statement therefore does not get executed. |
||||
In our exception block, we can simpl place code to act regardless of the type of error. Or we can do different things depending on the error. Here, we use the On function to act on the exception type. |
||||
The On clause checks against one of a number of Exception classes. The top dog is the Exception class, parent of all exception classes. This is guaranteed to be activated above. We can pick out of this class the name of the actual exception class name (EDivByZero) and the message (divide by zero). |
||||
We could have multiple On clauses for specific errors : |
||||
|
||||
What happens when debugging |
||||
Note that when you are debugging your code within Delphi, Delphi will trap exceptions even if you have exception handling. You must then click OK on the error dialogue, then hit F9 or the green arrow to continue to your except clause. You can avoid this by changing the debug options. |
||||
And finally … |
||||
Suppose that instead of trapping the error where it occurs, you may want to let a higher level exception handler in your code to do a more global trapping. But your code may have created objects or allocated memory that is now no longer referenced. It is dangerous to leave these allocations lying around. |
||||
Delphi provides an alternative part to the exception wrapper the Finally clause. Instead of being called when an exception occurs, the finally clause is always called after part or all of the try clause is executed. It allows us to free up allocated memory, or other such activities. However, it does not trap the error — the next highest exception handling (try) block that we are nested in is located and executed. |
||||
Once you are done debugging the software it is time to relax. Get up out of your modern office furniture and take a nap or go outside. It is important to take breaks from your work and have fun. |
||||
Raising exceptions |
||||
We can not only raise exceptions at our own choosing, but we can create Exception classes to manage them. This kind of processing is somewhat beyond the basics, being more appropriate to large applications, especially those using many large modules. These modules may generate their own exception types. Here are the most common exception types : |
||||
|
||||
Обработка исключительных ситуаций в DelphiСодержание
Структурная обработка исключительных ситуаций Модель исключительных ситуаций в Delphi Синтаксис обработки исключительных ситуаций Примеры обработки исключительных ситуаций Вызов исключительной ситуации Доступ к экземпляру объекта exception Предопределенные обработчики исключительных ситуаций Исключения, возникающие при работе с базами данных Заключение ОбзорС целью поддержки структурной обработки исключительных ситуаций (exception) в Delphi введены новые расширения языка Pascal. В данной статье будет дано описание того, что из себя представляет такая обработка, почему она полезна, будут приведены соответствующий синтаксис Object Pascal и примеры использования исключительных ситуаций в Delphi. Структурная обработка исключительных ситуацийСтруктурная обработка исключительных ситуаций — это система, позволяющая программисту при возникновении ошибки (исключительной ситуации) связаться с кодом программы, подготовленным для обработки такой ошибки. Это выполняется с помощью языковых конструкций, которые как бы «охраняют» фрагмент кода программы и определяют обработчики ошибок, которые будут вызываться, если что-то пойдет не так в «охраняемом» участке кода. В данном случае понятие исключительной ситуации относится к языку и не нужно его путать с системными исключительными ситуациями (hardware exceptions), такими как General Protection Fault. Эти исключительные ситуации обычно используют прерывания и особые состояния «железа» для обработки критичной системной ошибки; исключительные ситуации в Delphi же независимы от «железа», не используют прерываний и используются для обработки ошибочных состояний, с которыми подпрограмма не готова иметь дело. Системные исключительные ситуации, конечно, могут быть перехвачены и преобразованы в языковые исключительные ситуации, но это только одно из применений языковых исключительных ситуаций. При традиционной обработке ошибок, ошибки, обнаруженные в процедуре обычно передаются наружу (в вызывавшую процедуру) в виде возвращаемого значения функции, параметров или глобальных переменных (флажков). Каждая вызывающая процедура должна проверять результат вызова на наличие ошибки и выполнять соответствующие действия. Часто, это просто выход еще выше, в более верхнюю вызывающую процедуру и т.д. : функция A вызывает B, B вызывает C, C обнаруживает ошибку и возвращает код ошибки в B, B проверяет возвращаемый код, видит, что возникла ошибка и возвращает код ошибки в A, A проверяет возвращаемый код и выдает сообщение об ошибке либо решает сделать что-нибудь еще, раз первая попытка не удалась. Такая «пожарная бригада» для обработки ошибок трудоемка, требует написания большого количества кода в котором можно легко ошибиться и который трудно отлаживать. Одна ошибка в коде программы или переприсвоение в цепочке возвращаемых значений может привести к тому, что нельзя будет связать ошибочное состояние с положением дел во внешнем мире. Результатом будет ненормальное поведение программы, потеря данных или ресурсов, или крах системы. Структурная обработка исключительной ситуации замещает ручную обработку ошибок автоматической, сгенерированной компилятором системой уведомления. В приведенном выше примере, процедура A установила бы «охрану» со связанным обработчиком ошибки на фрагмент кода, в котором вызывается B. B просто вызывает C. Когда C обнаруживает ошибку, то создает (raise) исключительную ситуацию. Специальный код, сгенерированный компилятором и встроенный в Run-Time Library (RTL) начинает поиск обработчика данной исключительной ситуации. При поиске «защищенного» участка кода используется информация, сохраненная в стеке. В процедурах C и B нет такого участка, а в A — есть. Если один из обработчиков ошибок, которые используются в A, подходит по типу для возникшей в C исключительной ситуации, то программа переходит на его выполнение. При этом, область стека, используемая в B и C, очищается; выполнение этих процедур прекращается. Если в A нет подходящего обработчика, то поиск продолжается в более верхнем уровне, и так может идти, пока поиск не достигнет подходящего обработчика ошибок среди используемых по умолчанию обработчиков в RTL. Обработчики ошибок из RTL только показывают сообщение об ошибке и форсированно прекращают выполнение программы. Любая исключительная ситуация, которая осталась необработанной, приведет к прекращению выполнения приложения. Без проверки возвращаемого кода после каждого вызова подпрограммы, код программы должен быть более простым, а скомпилированный код — более быстрым. При наличии исключительных ситуаций подпрограмма B не должна содержать дополнительный код для проверки возвращаемого результата и передачи его в A. B ничего не должна делать для передачи исключительной ситуации, возникшей в C, в процедуру A — встроенная система обработки исключительных ситуаций делает всю работу. Данная система называется структурной, поскольку обработка ошибок определяется областью «защищенного» кода; такие области могут быть вложенными. Выполнение программы не может перейти на произвольный участок кода; выполнение программы может перейти только на обработчик исключительной ситуации активной программы. Модель исключительных ситуаций в DelphiМодель исключительных ситуаций в Object Pascal является невозобновляемой(non-resumable). При возникновении исключительной ситуации Вы уже не сможете вернуться в точку, где она возникла, для продолжения выполнения программы (это позволяет сделать возобновляемая(resumable) модель). Невозобновляемые исключительные ситуации разрушают стек, поскольку они сканируют его в поисках обработчика; в возобновляемой модели необходимо сохранять стек, состояние регистров процессора в точке возникновения ошибки и выполнять поиск обработчика и его выполнение в отдельном стеке. Возобновляемую систему обработки исключительных ситуаций гораздо труднее создать и применять, нежели невозобновляемую. Синтаксис обработки исключительных ситуацийТеперь, когда мы рассмотрели, что такое исключительные ситуации, давайте дадим ясную картину, как они применяются. Новое ключевое слово, добавленное в язык Object Pascal — try. Оно используется для обозначения первой части защищенного участка кода. Существует два типа защищенных участков:
Первый тип используется для обработки исключительных ситуаций. Его синтаксис: try Statement 1; Statement 2; ... except on Exception1 do Statement; on Exception2 do Statement; ... else Statements; {default exception-handler} end; Для уверенности в том, что ресурсы, занятые вашим приложением, освободятся в любом случае, Вы можете использовать конструкцию второго типа. Код, расположенный в части finally, выполняется в любом случае, даже если возникает исключительная ситуация. Соответствующий синтаксис: try Statement1; Statement2; ... finally Statements; { These statements always execute } end; Примеры обработки исключительных ситуацийНиже приведены процедуры A,B и C, обсуждавшиеся ранее, воплощенные в новом синтаксисе Object Pascal: type ESampleError = class(Exception); var ErrorCondition: Boolean; procedure C; begin writeln('Enter C'); if (ErrorCondition) then begin writeln('Raising exception in C'); raise ESampleError.Create('Error!'); end; writeln('Exit C'); end; procedure B; begin writeln('enter B'); C; writeln('exit B'); end; procedure A; begin writeln('Enter A'); try writeln('Enter A''s try block'); B; writeln('After B call'); except on ESampleError do writeln('Inside A''s ESampleError handler'); on ESomethingElse do writeln('Inside A''s ESomethingElse handler'); end; writeln('Exit A'); end; begin writeln('begin main'); ErrorCondition := True; A; writeln('end main'); end. При ErrorCondition = True программа выдаст: begin main Enter A Enter A's try block enter B Enter C Raising exception in C Inside A's ESampleError handler Exit A end main Возможно вас удивила декларация типа ‘ESampleError =class’ вместо ‘=object’; это еще одно новое расширение языка. Delphi вводит новую модель объектов, доступную через декларацию типа ‘=class’. Описание новой объектной модели дается в других уроках. Здесь же достаточно сказать, что исключительные ситуации (exceptions) являются классами, частью новой объектной модели. Процедура C проверяет наличие ошибки (в нашем случае это значение глобальной переменной) и, если она есть (а это так), C вызывает(raise) исключительную ситуацию класса ESampleError. Процедура A помещает часть кода в блок try..except. Первая часть этого блока содержит часть кода, аналогично конструкции begin..end. Эта часть кода завершается ключевым словом except, далее следует один или более обработчиков исключительных ситуаций on xxxx do yyyy, далее может быть включен необязательный блок else, вся конструкция заканчивается end;. В конструкции, назначающей определенную обработку для конкретной исключительной ситуации (on xxxx do yyyy), после резервного слова on указывается класс исключительной ситуации, а после do следует собственно код обработки данной ошибки. Если возникшая исключительная ситуация подходит по типу к указанному после on, то выполнение программы переходит сюда (на код после do). Исключительная ситуация подходит в том случае, если она того же класса, что указан в on, либо является его потомком. Например, в случае on EFileNotFound обрабатываться будет ситуация, когда файл не найден. А в случае on EFileIO — все ошибки при работе с файлами, в том числе и предыдущая ситуация. В блоке else обрабатываются все ошибки, не обработанные до этого. Приведенные в примере процедуры содержат код (строка с writeln), который отображает путь выполнения программы. Когда C вызывает exception, программа сразу переходит на обработчик ошибок в процедуре A, игнорируя оставшуюся часть кода в процедурах B и C. После того, как найден подходящий обработчик ошибки, поиск оканчивается. После выполнения кода обработчика, программа продолжает выполняться с оператора, стоящего после слова end блока try..except (в примере — writeln(‘Exit A’)). Конструкция try..except подходит, если известно, какой тип ошибок нужно обрабатывать в конкретной ситуации. Но что делать, если требуется выполнить некоторые действия в любом случае, произошла ошибка или нет? Это тот случай, когда понадобится конструкция try..finally. Рассмотрим модифицированную процедуру B: procedure NewB; var P: Pointer; begin writeln('enter B'); GetMem(P, 1000); C; FreeMem(P, 1000); writeln('exit B'); end; Если C вызывает исключительную ситуацию, то программа уже не возвращается в процедуру B. А что же с теми 1000 байтами памяти, захваченными в B? Строка FreeMem(P,1000) не выполнится и Вы потеряете кусок памяти. Как это исправить? Нужно ненавязчиво включить процедуру B в процесс, например: procedure NewB; var P: Pointer; begin writeln('enter NewB'); GetMem(P, 1000); try writeln('enter NewB''s try block'); C; writeln('end of NewB''s try block'); finally writeln('inside NewB''s finally block'); FreeMem(P, 1000); end; writeln('exit NewB'); end; Если в A поместить вызов NewB вместо B, то программа выведет сообщения следующим образом: begin main Enter A Enter A's try block enter NewB enter NewB's try block Enter C Raising exception in C inside NewB's finally block Inside A's ESampleError handler Exit A end main Код в блоке finally выполнится при любой ошибке, возникшей в соответствующем блоке try. Он же выполнится и в том случае, если ошибки не возникло. В любом случае память будет освобождена. Если возникла ошибка, то сначала выполняется блок finally, затем начинается поиск подходящего обработчика. В штатной ситуации, после блока finally программа переходит на следующее предложение после блока. Почему вызов GetMem не помещен внутрь блока try? Этот вызов может окончиться неудачно и вызвать exception EOutOfMemory. Если это произошло, то FreeMem попытается освободить память, которая не была распределена. Когда мы размещаем GetMem вне защищаемого участка, то предполагаем, что B сможет получить нужное количество памяти, а если нет, то более верхняя процедура получит уведомление EOutOfMemory. А что, если требуется в B распределить 4 области памяти по схеме все-или-ничего? Если первые две попытки удались, а третья провалилась, то как освободить захваченную область память? Можно так: procedure NewB; var p,q,r,s: Pointer; begin writeln('enter B'); P := nil; Q := nil; R := nil; S := nil; try writeln('enter B''s try block'); GetMem(P, 1000); GetMem(Q, 1000); GetMem(R, 1000); GetMem(S, 1000); C; writeln('end of B''s try block'); finally writeln('inside B''s finally block'); if P <> nil then FreeMem(P, 1000); if Q <> nil then FreeMem(Q, 1000); if R <> nil then FreeMem(R, 1000); if S <> nil then FreeMem(S, 1000); end; writeln('exit B'); end; Установив сперва указатели в NIL, далее можно определить, успешно ли прошел вызов GetMem. Оба типа конструкции try можно использовать в любом месте, допускается вложенность любой глубины. Исключительную ситуацию можно вызывать внутри обработчика ошибки, конструкцию try можно использовать внутри обработчика исключительной ситуации. Вызов исключительной ситуацииВ процедуре C из примера мы уже могли видеть, как должна поступать программа при обнаружении состояния ошибки — она вызывает исключительную ситуацию: raise ESampleError.Create('Error!'); После ключевого слова raise следует код, аналогичный тому, что используется для создания нового экземпляра класса. Действительно, в момент вызова исключительной ситуации создается экземпляр указанного класса; данный экземпляр существует до момента окончания обработки исключительной ситуации и затем автоматически уничтожается. Вся информация, которую нужно сообщить в обработчик ошибки передается в объект через его конструктор в момент создания. Почти все существующие классы исключительных ситуаций являются наследниками базового класса Exception и не содержат новых свойств или методов. Класс Exception имеет несколько конструкторов, какой из них конкретно использовать — зависит от задачи. Описание класса Exception можно найти в on-line Help. Доступ к экземпляру объекта exceptionДо сих пор мы рассматривали механизмы защиты кода и ресурсов, логику работы программы в исключительной ситуации. Теперь нужно немного разобраться с тем, как же обрабатывать возникшую ошибку. А точнее, как получить дополнительную информацию о коде ошибки, текст сообщения и т.п. Как уже говорилось, при вызове исключительной ситуации (raise) автоматически создается экземпляр соответствующего класса, который и содержит информацию об ошибке. Весь вопрос в том, как в обработчике данной ситуации получить доступ к этому объекту. Рассмотрим модифицированную процедуру A в нашем примере: procedure NewA; begin writeln('Enter A'); try writeln('Enter A''s try block'); B; writeln('After B call'); except on E: ESampleError do writeln(E.Message); on ESomethingElse do writeln('Inside A''s ESomethingElse handler'); end; writeln('Exit A'); end; Здесь все изменения внесены в строку on ESE: ESampleError do writeln(ESE.Message); Пример демонстрирует еще одно новшество в языке Object Pascal — создание локальной переменной. В нашем примере локальной переменной является ESE — это тот самый экземпляр класса ESampleError, который был создан в процедуре C в момент вызова исключительного состояния. Переменная ESE доступна только внутри блока do. Свойство Message объекта ESE содержит сообщение, которое было передано в конструктор Create в процедуре C. Есть еще один способ доступа к экземпляру exception — использовать функцию ExceptionObject: on ESampleError do writeln(ESampleError(ExceptionObject).Message); Предопределенные обработчики исключительных ситуацийНиже Вы найдете справочную информацию по предопределенным исключениям, необходимую для профессионального программирования в Delphi.
procedure Abort; begin raise EAbort.CreateRes(SOperationAborted) at ReturnAddr; end;
Исключения, возникающие при работе с базами данныхDelphi, обладая прекрасными средствами доступа к данным, основывающимися на интерфейсе IDAPI, реализованной в виде библиотеки Borland Database Engine (BDE), включает ряд обработчиков исключительных ситуаций для регистрации ошибок в компонентах VCL работающим с БД. Дадим краткую характеристику основным из них:
repeat {пока не откроем таблицу или не нажмем кнопку Cancel} try Table1.Active := True; {Пытаемся открыть таблицу} Break; { Если нет ошибки - прерваем цикл} except on EDatabaseError do {Если нажата OK - повторяем попытку открытия Table1} if MessageDlg('Не могу открыть Table1', mtError, [mbOK, mbCancel], 0) <> mrOK then raise; end; until False;
EDBEngineError = class(EDatabaseError) private FErrors: TList; function GetError(Index: Integer): TDBError; function GetErrorCount: Integer; public constructor Create(ErrorCode: DBIResult); destructor Destroy; property ErrorCount: Integer; property Errors[Index: Integer]: TDBError; end; Особенно важны два свойства класса EDBEngineError : Errors ЗаключениеДанный урок должен был дать вам достаточно информации для того, чтобы начать исследование того, как Вы можете использовать систему обработки исключительных ситуаций в вашей программе. Вы, конечно, можете обрабатывать ошибки и без привлечения этой системы; но с ней Вы получите лучшие результаты с меньшими усилиями. Назад | Содержание |
В прошлый раз я говорил про взаимодействие плагинов между собой и то, какие возможности дают эти техники для прочих сценариев, без общения плагинов. В этой части я хотел бы рассмотреть обработку ошибок и отладку в плагинах. Я выношу эту тему вперёд других частей, потому что наша система становится всё более сложной и вам лучше бы уметь решать в ней проблемы, прежде чем усложнять её далее.
См. также Разработка API (контракта) для своей DLL и Разработка системы плагинов, часть 9: подводные камни.
Оглавление
- Основы отладки:
- Запуск отладки плагинов
- Остановка выполнения:
- Пауза
- Исключения
- Точки останова
- Инструменты отладчика:
- Анализ значений переменных
- Анализ пути выполнения
- Трассировка
- События
- Расширенные точки останова
- Практический пример
- Управлением временем жизни
- Двойное удаление
- Утечки памяти
- EAccessViolation
- Неверные сигнатуры
- Передача данных
- Многопоточность
- Обработка ошибок в плагинах:
- Safecall
- Стандартные средства
- Дополнение стандартных средств
- Актуальные коды исключений
- Использование оригинальных классов исключений
- Наследование
- Дополнительная информация
- Идентификация точки возбуждения
- Аппаратные исключения
- Заключение
Основы отладки
Сперва мне хотелось бы дать краткое введение в отладку. Если вы уже знакомы с этим материалом, то можете пропустить его. Для тех же, кому краткого введения окажется мало — см. полную версию. Я привожу здесь эту вводную часть потому, что далее я буду говорить слова вроде «ставим non-breaking бряк на строчку XYZ с логгингом стека» и мне хотелось бы, чтобы читатели понимали, о чём идёт речь.
Отладчик — это один из основных инструментов любого программиста. Он является составной частью среды Delphi и предназначен для поиска ошибок в программе. Отладчик позволяет выполнять пошаговую трассировку (выполнение кода по шагам), просматривать значения переменных в процессе выполнения программы, устанавливать точки останова (breakpoint) и т.д. Отладка — это процесс работы с программой в отладчике, при котором обнаруживают, локализуют и устраняют ошибки.
Все основные команды, через которые Delphi переходит в режим отладки, находятся в меню «Run»:
Основные команды управления отладчиком |
На самом деле, всякий раз, когда вы запускаете программу из среды Delphi по F9 или командой меню «Run»/»Run», вы запускаете программу под отладчиком. Для простого запуска программы (вне отладки) есть команда «Run without debugging» (Ctrl+Shift+F9).
Примечание: команда «Run without debugging» есть не во всех версиях Delphi. А в самых последних версиях Delphi обе команды вынесены на панель инструментов:
Два варианта запуска на панели инструментов Примечание: команда «Run without debugging» эквивалентна компиляции программы и ручному запуску её с диска вне среды. В меню она вынесена просто для удобства, чтобы не нужно было искать exe-файл в файловой системе. Если вы хотите посмотреть, как поведёт себя программа без опёки отладчика — используйте «Run without debugging».
Итак, оказывается, что в 99% случаев запуска программы — вы запускаете программу именно под отладчиком.
Запуск отладки библиотек (плагинов)
Замечу, что запускать на выполнение можно только программы. Библиотеки (DLL или bpl) — это не программы. Это именно что библиотеки — наборы функций. Их нельзя запустить. Но их можно загрузить и вызвать функцию из них. Загружать библиотеку — должен «кто-то», а именно — какая-то программа. Вот её, эту программу, вам и нужно запускать, когда вы хотите отладить библиотеку. Программа запустится, загрузит библиотеку и будет вызывать её функции.
Поскольку все плагины являются у нас библиотеками, то при попытке «запустить» плагин вы получите такое сообщение:
Попытка «запустить» DLL или bpl |
Если вы хотите скомпилировать плагин (получить .dll или .bpl файл) — вам вовсе не нужно его для этого запускать. Просто выберите Project/Compile (Ctrl+F9) или Project/Build (Shift+F9).
Если же вы хотите именно отлаживать плагин, то вам нужно указать программу, которая будет грузить этот плагин. Естественно, в случае плагинов это будет программа-ядро. Для этого вам нужно открыть меню Run/Parameters и указать главную программу:
Указание программы-сервера (host) |
Остановка выполнения программы
Пока программа работает, вы не много можете с ней сделать. Для того чтобы воспользоваться отладчиком, вам нужно приостановить её выполнение. У вас на выбор есть три варианта, первый — нажать на кнопку паузы («Run»/»Program pause»), второй — возбудить в программе исключение (или же оно возникнет в программе само — например, EAccessViolation
), третий — расставить в нужных местах точки останова (breakpoint-ы, брейкпойнты или просто «бряки»).
Пауза
Способ первый не позволяет достичь точности. Вы останавливаете программу в тот момент, когда вы нажимаете на кнопку. Это может быть за миллион строк кода до или после того места, где вы в действительности хотели бы быть. Поэтому этот способ используется, когда вам не важно точное место останова. Пример — зависшая программа. Вы просто останавливаете её выполнение в любой момент, чтобы посмотреть, что же там произошло, что программа зависла. Ещё вариант — вам нужна программа на паузе, чтобы проанализировать, скажем, глобальные переменные. Вам не важно место останова, потому что вас интересуют значения переменных, которые, будучи раз заданными, не меняются во время работы программы.
Исключения
Второй способ заключается в том, что в отладчике Delphi есть полезнейшая возможность раннего уведомления об исключениях. Каждый раз, когда в программе возникает исключение, отладчик отображает такое окно:
Уведомление отладчика в старых версиях Delphi |
Уведомление отладчика в новых версиях Delphi |
Формат сообщения всегда одинаков: «Project XXX raised exception class YYY with message ZZZ». Где XXX — имя процесса (проекта), где возникло исключение, YYY — имя класса исключения и ZZZ — сообщение об ошибке в объекте исключения.
Это окно возникает прямо в момент возбуждения исключения до того, как получит управление хоть один блок обработки исключения. Остановка программы в момент возникновения исключения позволяет вам немедленно исследовать ситуацию, используя инструменты отладчика (рассмотрим их чуть ниже) и либо исправить проблему, либо продолжить выполнение программы.
Заметим, что окно это появляется только при отладке. Его появление во время запуска программы из-под Delphi ещё не говорит о том, что при запуске программы вне среды появится хоть какое-то сообщение. Нажав на «Continue» (только в новых Delphi), вы продолжите выполнение программы (с первого блока обработки исключения), а нажав на «Break»/»Ok», вы перейдёте в отладчик, где сможете исследовать ситуацию возникновения исключения.
Иными словами, если вы видите такое окно — это значит, что в вашей программе произошло событие «возбуждение исключения». Окно показывается до выполнения кода обработки, поэтому его показ ещё не означает, что ваша программа вообще покажет хоть какое-то сообщение об ошибке. Хорошим примером является наша функция мульти-загрузки плагинов из папки из первой части: там мы ловили ошибки загрузки плагинов, собирая информацию, но не показывая сообщений об ошибках, а в конце загрузки возбуждали единственное исключение.
Если вы хотите посмотреть, как программа будет работать «вживую», без отладчика и его уведомлений — просто запустите программу вне отладчика (через «Run without debugging» или просто запустив программу руками с диска).
Иногда в этом окне также появляется опция «Show CPU view»:
Уведомление с дополнительными опциями |
Она показывается только в том случае, если место возникновения исключения не соответствует строчке исходного кода. Если галочка будет установлена, то после нажатия на «Break» откроется окно CPU-отладчика точно спозиционированное на место возникновения ошибки, иначе (галочка сброшена) — редактор исходного кода с ближайшим местом по стеку вызова (если отладчик вообще сумеет что-то найти).
Заметим, что опция «Show CPU view» показывается достаточно редко. Кстати говоря, её отсутствие в окне уведомления отладчика ещё не говорит о том, что при нажатии на «Break» вы не увидите CPU-отладчика. Более подробно об этом я скажу чуть ниже.
Примечание: кроме уведомления об исключениях, отладчик может показывать и иные сообщения. Их не следует путать с уведомлениями об исключениях.
Точки останова
Третий способ является основным. Заключается он в том, что по тексту программы вы мышкой отмечаете места, где вы хотели бы остановиться во время работы программы. Слева от кода вы видите полоску, в которой появляются синие точки:
Среда Delphi показывает наличие отладочной информации для строк кода |
Каждая синяя точка говорит о том, что в реальном машинном коде программы есть код, который попал туда именно в процессе компиляции этой строки. Т.е. строка, отмеченная синей точкой, компилируется в код, приводит к появлению кода. Если напротив строки с кодом вы не видите точки, то:
- Возможно, вы открыли файл, не принадлежащий проекту. К примеру, у вас загружен в IDE проект плагина, но вы открыли файл менеджера плагинов. Менеджер плагинов — он, вообще-то, находится в ядре. Если вы хотите его отлаживать, вам нужно открыть проект программы, а не плагина.
- Возможно, модуль ещё не загружен. К примеру, если вы отлаживаете плагин и запустили программу, то вы не увидите отметки, пока плагин не будет загружен программой. Замечу, что это может и не произойти вовсе — если, к примеру, программа и не собирается грузить плагин.
- Возможно, эта строка не используется программой. Например, если в модуле есть три функции, но программа вызывает из него только две функции, то третья функция в программу не попадёт, и напротив строк её кода вы не увидите синих отметок. Второй случай — одна строка из функции может быть выкинута оптимизатором. Например, если вы присваиваете значение, которое дальше не используется. Вы не сможете установить точки останова на все эти строки — просто потому что их физически нет в программе.
- Возможно, что вам нужно просто сделать Build проекту. Т.е. вы смотрите на старый вариант кода. В некоторых случаях нужно почистить папки от старых .dcu файлов.
- Возможно, в вашем проекте отключена отладочная информация. Отладочная информация — это информации о соответствии машинных инструкций из .exe/.dll/.bpl и строчек текстового исходного кода. Если её не будет — отладчик не сможет работать с исходным кодом и вы увидите только машинный отладчик (CPU View). Отладочную информацию можно включить/выключить в коде (директивы
{$D+}/{$D-}
) или опциях проекта (Project/Options/Compiler/Debug information). Подробнее см. мою статью о настройке проектов для отладки. - Возможно, этот код находится в пакете времени выполнения (.bpl). В этом случае он может быть собран с иными опциями, чем указано у вас в проекте. Конечно же, изменения опций проекта никак не влияют на пакеты. В этом случае либо пересоберите пакет с отладочной информацией, либо отключите сборку с пакетами.
- Возможно, Delphi не может связать выполняющуюся программу с исходным кодом и отладочной информацией. Что можно сделать, чтобы этого избежать:
- Не переименовывайте программу или плагины. Изначально задавайте им нужные имена.
- Не перемещайте программу или плагины в другое место. Вместо этого задайте нужный Output directory в настройках проекта, либо просто храните проект в выходном каталоге.
- Не запускайте программу через альтернативные имена. К примеру, папка программа может быть видна под различными именами: её имя, жёсткая ссылка на папку, подмонтированный диск (reparse point), subst-диск, сетевая папка. Если у вас есть несколько вариантов путей к одной и той же папке — везде используйте один и тот же путь, лучше всего — родной, без обвеса. К примеру, если папка C:\Users\Александр\Documents является точкой подключения диска D:\, то к файлу
RAD Studio\Projects\PluginsDemo\richedit.exe
можно обратиться через два имени:
C:\Users\Александр\Documents\RAD Studio\Projects\PluginsDemo\richedit.exe
и
D:\RAD Studio\Projects\PluginsDemo\richedit.exe
В этом случае будет предпочтительнее открывать проект
D:\RAD Studio\Projects\PluginsDemo\richedit.dpr
вместо
C:\Users\Александр\Documents\RAD Studio\Projects\PluginsDemo\richedit.dpr
и запускать
D:\RAD Studio\Projects\PluginsDemo\richedit.exe
вместо
C:\Users\Александр\Documents\RAD Studio\Projects\PluginsDemo\richedit.exe - Убедитесь, что в пути к проекту есть только латинские символы (ASCII). Например, плохо:
C:\Users\Александр\...
. Нормально:C:\Users\Alexandr\...
- Не изменяйте дату-время .dcu/.exe/.dll/.bpl файлов. В некоторых случаях изменение даты может привести к тому, что отладчик будет считать файлы изменёнными и поэтому не соответствующими друг другу. Иногда дата может меняться антивирусом или примочками к IDE.
- Не используйте опцию «Use MSBuild externally», либо включайте генерацию удалённой отладочной информации (remote debug information, RSM).
- Не удаляйте и не перемещайте .dcu файлы. Убедитесь, что выходной каталог для .dcu (Unit output directory) также находится в путях поиска (Search path). В крайнем случае попробуйте выводить .dcu файлы в тот же каталог, что и выходной .exe/.dll/.bpl файл.
- Убедитесь, что файлы проекта можно найти. Вы также можете попробовать подключить все их явно через Project/Add to project, либо же попробовать, наоборот, перечислить папки с ними в опции Search paths проекта.
- Как крайнее средство — попробуйте включить отладочную информацию TD32 и информацию для удалённой отладки. Подробнее см. уже упоминавшуюся статью.
- Ещё как крайнее средство — попробуйте заменить в настройках проекта относительные пути на абсолютные и наоборот. Ещё мощнее — сложить все (проект, выходной файл и .dcu) в одну папку.
Делайте действия по списку сверху-вниз. После каждого действия очищайте .dcu файлы и делайте Build проекту.
- Ну и, конечно же, это могут быть баги Delphi. Чем старее версия Delphi — тем больше в ней может быть багов, связанных с этим. Как правило, все они проявляются лишь при экстремально-граничных случаях: больших размерах модулей, констант и т.п. Вы можете погуглить в интернете или на Quality Central.
Итак, после того, как вы сделали Build проекту и видите синие точки — теперь вы можете выбрать место для установки точки останова. Щёлкните мышкой по любой синей точке, и она изменится на большую красную точку, а сама строка выделяется (кстати, если вы не видите синих точек вообще, необязательно делать компиляцию, чтобы их увидеть — просто щёлкайте слева от кода, где вы хотели бы остановиться):
Установленная точка останова |
В этом случае мы захотели остановиться перед выполнением строки с присваиванием свойства Caption. Заметим, что breakpoint-ы вы можете ставить, как во время проектирования, так и во время работы или приостановки программы. Теперь, после запуска программы, как только выполнение дойдёт до одной из заданных вами точек останова, отладчик немедленно остановит программу.
Инструменты отладчика
Итак, после остановки программы в отладчике (любым из трёх описанных выше способов) вы можете использовать его возможности для анализа программы. Большинство вещей, о которых мы будем сейчас говорить, доступны именно в режиме отладки (например, в контекстное меню редактора кода в режиме отладки добавляются новые команды). Вы можете определить, в каком режиме находится среда, взглянув на заголовок окна:
Режим проектирования (design-time) — нет подписи |
Режим прогона (run-time), программа работает — добавлено «Running» |
Режим прогона (run-time), программа приостановлена — добавлено «Stopped» |
Для примера возьмите любую свою программу, поставьте breakpoint на первое действие при нажатии какой-нибудь кнопки, запустите программу и щёлкните по кнопке (мы сейчас будем обсуждать возможности отладчика, а вы сможете щупать их прямо на своей программе). Если вы используете новые Delphi, то заметите, как преображается при этом среда — исчезает инспектор объектов, палитра компонентов и т.п. Зато появляется множество окон: «Call Stack», «Watch List», «Local Variables» и т.п. Каждое из этих окон предоставляет вам какую-то возможность отладчика:
Среда во время отладки |
Если какого-то окна на экране нет, вы можете показать его, используя меню «View»/»Debug windows»:
Инструменты отладчика |
Если вы не видите на экране какое-то окно, о котором идёт речь, — просто выберите его из этого меню.
Попробуйте сейчас, пока программа стоит на паузе, переключиться на программу. Вы щёлкаете по кнопке в панели задач, но ничего не происходит. Попробуйте свернуть все окна, вы увидите примерно такую картину:
«Зависшая» программа под отладкой |
Вы можете видеть, что наша программа как бы висит (последним сворачивалось окно Delphi, поэтому рисунок окна Delphi отпечатался на окне нашей программы). Она не прорисовывается, она не реагирует на ваши действия, другим словом — висит. Да, но не забывайте, что мы только что поставили с вами программу на паузу! Это значит, что она не работает. А если программа не работает, то она и не может ни перерисовываться, ни реагировать на ваши щелчки мышью. Так что ничего страшного в таком поведении нет — так и должно быть. Как только вы возобновите работу программы (снимите её с паузы), она снова будет вести себя как полагается.
Анализ значений переменных
Итак, продолжаем. Наша программа стоит на паузе. В окне «Local Variables» показываются локальные переменные в текущем месте. Как только мы остановились, отладчик показывает нам чему равны локальные переменные в текущей функции (т.е. функции, в которой мы остановились). Если вы хотите посмотреть значения локальных переменных для других функций — просто дважды щёлкните по нужной функции в окне стека вызова (Call Stack).
Для некоторых переменных отладчик может сказать нам, что он не может получить значение переменной («Variable ‘XYZ‘ inaccessible here due to optimization»). Это работа оптимизатора (кстати, вы можете отключить его, сбросив в опциях проекта галочку «Optimization»). Он выбрасывает переменную, как только в ней отпадёт необходимость.
Итак, «Local Variables» — удобное окно для просмотра локальных переменных. Что делать, если хочется посмотреть не локальную переменную? Можно воспользоваться окном «Watches». Для этого щёлкните правой кнопкой по свободной области окна «Watch List» и выберите «Add watch» — появится окно ввода параметров наблюдения:
Добавление переменной для наблюдения |
В поле «Expression» вы можете ввести имя переменной, за которой хотите следить. Кстати, это не обязательно должна быть переменная — вы можете ввести любое выражение, которое поддаётся вычислению. Например, выражение «X = 1» (без кавычек, разумеется) — оно будет равно ‘True’ или ‘False’. Остальные опции отвечают за форматирование отображения. Другой способ добавить выражения для слежки — выделить их в редакторе кода, щёлкнуть правой кнопкой и выбрать «Add watch at cursor» (Ctrl + F5).
Примечание: обычно команды отладчика располагаются в подменю «Debug» (и многие из них могут быть недоступны, если только программа на стоит на паузе под отладчиком), но если в настройках отладчика включить опцию «Rearrange editor local menu on run», то на время отладки все пункты контекстного меню редактора, связанные с отладкой, для удобства выносятся наверх.
Вот пример окна «Watches» после добавления нескольких переменных и выражений для наблюдения:
Наблюдение за несколькими переменными |
Последние два выражения с X демонстрируют два различных вида представления одной и той же величины. В первом случае мы не меняли способ отображения, а во втором — установили значение в «Memory Dump». Это может быть полезно, если умалчиваемый вид не даёт достаточной информации — см., например, вопрос №65263. Заметим, что выражение «IntToStr(Tag)» не может быть вычислено («Inaccessible value»), т.к. для того, чтобы посмотреть значение этого выражения, нужно вызвать функцию (а именно — функцию IntToStr
). Вызов функции не является безопасным действием, т.к. может иметь побочные эффекты. Например, процедура может менять значение глобальной переменной или даже показывать сообщения. Но если вы уверены, что введённое вами значение вычислять безопасно, вы можете зайти в свойства watch-а и установить галочку «Allow function calls». После этого отладчик сможет показать значение выражения «IntToStr(Tag)», а именно — ‘1’ (строка, а не число). Но будьте аккуратны!
Если вам не нужно постоянно следить за переменной, а достаточно лишь разово просмотреть её значение, то вы можете воспользоваться функцией «Evaluate/Modify». Вы выделяете в редакторе кода выражение, которое хотите вычислить, щёлкаете правой кнопкой мыши по нему и выбираете в меню «Evaluate/Modify…» (Ctrl + F7). После этого на экране появляется такое окно:
Вычисление выражение или просмотр значения переменной |
В поле «Expression» вы видите выражение, которое вы выделяли в редакторе кода (в нашем случае мы просто поставили курсор на слово «Tag»). В поле «Result» показывается текущее значение выражения. Вы можете изменять выражение и нажимать кнопку «Evaluate» для вычисления введённого значения. Также вы можете задать новое значение в поле «New value» и нажать кнопку «Modify». Разумеется, возможность модификации доступна не всегда. Например, вы не можете модифицировать выражение «Tag = 1», равное True, на значение False. Вместо этого вы должны модифицировать значение самого Tag — одной из переменных, участвующих в выражении.
Примечание: кстати говоря, не следует думать, что модификация переменной в любом окне отладчика — это очень простая операция, заключающаяся в изменении памяти, занимаемой переменной. Это может быть и верно для простых типов типа Integer
, но не для сложных динамических типов типа String
и массивов. Дело в том, что для них ведь нужно выделить память, а старое значение нужно удалить. Поэтому изменение таких переменных ведёт к вызову функций менеджера памяти программы — несмотря на то, что при этом вся пограмма находится на паузе! В типичных ситуациях это не имеет значения, но в некоторых из-за таких побочных эффектов может получаться самое различное поведение программы. Просто имейте этот момент в виду.
Альтернативным способом для быстрого просмотра значений переменных и выражений является использование всплывающих подсказок — достаточно подвести курсор мыши к имени переменной в редакторе кода (либо выделить выражение и навести на него мышь) и через короткое время всплывёт подсказка со значением переменной (в случае, если выражение можно вычислить):
Просмотр значения переменной |
Просмотр значения выражения |
Хотя если подсказка не всплывает — это ещё не значит, что интересующее вас выражение нельзя вычислить. Возможно, среда просто не понимает, чего вы хотите Попробуйте посмотреть выражение через «Evaluate/Modify».
Анализ пути выполнения
Следующее окно, которое мы рассмотрим — это «Call Stack». Так называемый стек вызовов:
Окно «Call stack» во время отладки |
Это окно показывает, какие процедуры вызывались до того, как выполнение дошло до текущего момента (текущего — т.е. там, где мы встали на паузу). Читать его нужно снизу вверх (текущий момент находится сверху, а начало программы — в самом низу). Например, на скриншоте выше мы видим, что процедура A вызывалась из P, которая в свою очередь вызвалась из Button2Click (мы смотрим сверху вниз, т.е. в обратном направлении). Также это окно пытается показывать аргументы вызова. Но для этого они должны быть доступны. Помните, что мы говорили про оптимизатор в обсуждении окна «Local Variables»? Те же слова применимы и здесь.
Текущая процедура (т.е. та, в которой мы находимся) в этом окне маркируется стрелочкой.
По поводу странного вида процедур до Button2Click мы ещё поговорим позже.
Это окно — очень важный инструмент при поиске источника ошибок. Например, при остановке после исключения вы ведь понятия не имеете, что происходит в программе. Взглянув на «Call Stack», вы легко определите, где вы находитесь и как вы сюда попали. Более того, вы можете дважды щёлкнуть по любой строке в этом окне — и вы автоматически попадёте в соответствующее место. Например, если вы сейчас щёлкните по строке с «Unit9.P» в окне «Call Stack», то вы мало того, что перейдёте в редакторе кода к процедуре P, так ещё и строка вызова процедуры A будет подсвечена красным цветом. Очень удобно, если одна процедура вызыватся несколько раз в разных местах. Щёлкнув по нужной строке в этом окне, мы легко определим, откуда был сделан вызов.
Трассировка
Итак, с помощью рассмотренной функциональности вы можете анализировать любую ситуацию в программе — проверять, чему равны у вас переменные, даже вычислять выражения, следить за путём выполнения программы. Но это только одна статичная ситуация из множества возможных. Мы пока всё ещё стоим на месте. Но отладчик позволяет нам больше, а именно: он позволяет выполнять программу по шагам, по строчкам. Посмотрите на последний снимок экрана: мы встали на заданной точке останова. Точка останова показана красной точкой слева от строки кода. Но вы также можете видеть поверх неё небольшую голубую стрелочку, которой не было, когда мы устанавливали точку останова в режиме проектирования. Эта стрелочка показывает, что сейчас будет выполнена указанная строка. Для выполнения есть две основные команды — «Step over» (F8) и «Trace into» (F7). Нажмите, например, на F8. Вы увидите, как стрелочка переместится к следующей строке:
Состояние среды при выполнении одной строки после остановки на точке останова |
Это значит, что только что наша программа выполнила строку «Tag := X;» и готова к выполнению строки с ShowMessage
. Вы можете видеть установленную точку останова и сдвинутую на одну строку вниз стрелочку (текущую позицию выполняемого кода). Нажмите на F8 ещё раз. Вы увидите, что стрелочка пропадёт, в окнах отладчика появятся надписи «process not accessible», а в заголовке появится приписка «[Running]». Это значит, что наша программа больше не стоит на паузе, а работает. Переключитесь на свою программу. Вы увидите, что она показала сообщение (ShowMessage) с текстом ‘1’ (текстовое представление Tag, который равен 1). Программа полостью работает, вы можете таскать окно по рабочему столу. Закройте окно сообщения своей программы. Немедленно всплывёт окно среды:
Отладчик после выполнения второй строки |
Вы видите, что программа снова стоит на паузе. Мы только что выполнили строчку с ShowMessage
.
Таким образом, мы с вами можем выполнять по шагам любой блок кода. Если вы не можете понять, почему ваша программа ведёт себя так, а не иначе — просто поставьте бряк на свой код, и пройдитесь по коду после остановки, выполняя каждую строчку и смотря, как и куда идёт выполнение кода, какие значения каким переменным назначаются и т.п. Большие блоки кода вы можете пропускать, ставя новые бряки и используя команду «Run»/»Run» (F9) или устанавливая курсор в нужную строку и используя «Run to cursor» (F4).
Напомним, что у нас есть две команды для пошагового выполнения — «Step Over» (F8) и «Trace Into» (F7). С первой мы уже познакомились — она просто выполняет текущую строчку и переходит на следующую. «Trace Into» работает похожим образом, но с одним отличием: если в текущей строчке есть вызов процедуры, то «Trace Into» зайдёт внутрь процедуры, в то время как «Step Over» выполнит всю процедуру одним махом. Если никаких вызовов процедур нет, то эти команды ведут себя одинаково.
Например, положим, что мы установили точку останова на вызов некоторой функции — скажем, P. Тогда, если бы вы стояли на «P;» и нажали бы F8, то программа выполнила бы P целиком, после чего мы бы оказались в отладчике на строке после «P;». А если бы нажали на F7, то мы перешли бы в процедуру P, оказавшись на сроке «begin». Разумеется, если бы мы ещё поставили точку останова внутри P, то при попытке выполнить строчку с «P;» «одним махом» с помощью F8, мы всё равно оказались бы внутри P — но уже не в результате «захода в функцию», а как результат срабатывания точки останова. Это полностью соответствует описанной логике. С одной стороны, F8 выполняет строчку целиком. С другой стороны, любой бряк приводит к остановке выполнения программы. Поэтому, когда F8 выполняет строку и в процессе этого выполнения натыкается на бряк, то она останавливает выполнение программы.
Обычно при отладке вы используете F8, выполняя код строго по строчкам. Вас обычно не интересуют детали выполнения вызываемых подпрограмм, а важен лишь конечный результат. Некоторые из этих процедур могут быть весьма нетривиальными — например, запрос реквизитов пользователя и подключение к серверу могут выполняться одной процедурой, которую мы можем выполнить одним нажатием на F8. С другой стороны, мы можем быть заинтересованы в отслеживании выполнения своих собственных процедур, поэтому, когда из одной нашей процедуры вызывается другая наша процедура, то мы будем использовать F7, чтобы зайти внутрь второй нашей процедуры и проследить её выполнение. Но опять же, если мы заранее знаем, чем кончится дело, то мы не обязаны шагать по шагам по всем процедурам — мы вполне можем использовать и F8.
Кроме этих команд ещё есть ещё несколько полезных команд движения по программе — но я оставлю вам их для самостоятельного изучения.
Мы рассмотрели большинство основных возможностей для отладки программы. Два главных инструмента отладчика — это наблюдение за переменными и пошаговое выполнение. Если вы используете описанный инструментарий несколько раз, то у вас появится потребность завершить выполнение программы раньше положенного. Например, вы запустили программу, стали её отлаживать и нашли причину ошибки. Теперь вам нужно её исправить. Но ваша программа сейчас работает или стоит на паузе. Прежде, чем вернуться к редактированию текста, вы должны завершить её. Что вы будете делать? Снимать все бряки, возобновлять выполнение программы и выходить из неё? Есть способ проще — вы можете использовать «Program reset» (Ctrl + F2). Эта команда немедленно обрывает выполнение программы. Её можно рассматривать как аналог команды «Завершить процесс» в Диспетчере Задач, только чуть более гуманный по отношению к среде Delphi.
Примечание: никогда не используйте обычное снятие процесса отлаживаемой программы. Отладчик крайне болезненно относится к снятию процесса извне. Всегда используйте только «Program reset».
Далее, вспомните про понятие отладочной информации, о котором мы говорили выше. Если модуль был скомпилирован без отладочной информации, то использовать обычный отладчик для него вы не сможете. Т.е. не будут работать бряки, поставленные на код этого модуля. Вы не сможете зайти по F7 в любую процедуру этого модуля и т.п. Посмотрите хотя бы на снимки экрана чуть выше: у нас есть вызовы ShowMessage
и IntToStr
. Если вы попробуете в них зайти, то ничего не выйдет — F7 сработает как обычная F8. Это как раз и происходит потому, что нет отладочной информации для модулей Dialogs
и SysUtils
соответственно. Все стандартные модуля Delphi не имеют отладочной информации. Обычно это очень удобно — ведь вам большую часть времени не нужно отлаживаться внутри стандартных процедур. Однако если вам всё же нужно это сделать (например, по непонятным причинам вылетает Assign
для стандартного TTreeView
и вы должны выяснить почему), то вы можете переключиться между обычной и отладочной версией системных модулей. Для этого вы устанавливаете галочку «Use debug DCUs». После этого вы можете использовать F7, чтобы заходить в стандартные процедуры, в частности, вы теперь можете зайти и в IntToStr. Разумеется, эта опция работает только для стандартных модулей Delphi. Для того чтобы использовать отладочную версию своих модулей — вы должны перекомпилировать их с нужными опциями.
Примечание: есть тут ещё один тонкий момент. Если вы компилируете своё приложение с пакетами времени выполнения, то модули, для которых вы хотите включить/выключить отладку могут находиться в пакете, а не в программе. И на них эти опции влиять, разумеется, не будут. Возможно, вам придётся пересобрать свои пакеты или временно отключить компиляцию с пакетами.
Посмотрите на наш пример, когда мы говорили про окно «Call Stack». Мы заметили, что все процедуры ниже Button2Click
имеют странный вид. Это как раз и происходило потому, что все эти процедуры являлись стандартными процедурами Delphi и поэтому размещались в модулях без отладочной информации. Если бы мы включили опцию «Use debug DCUs», то наш стек вызовов выглядел бы так:
Стек вызова после включения опции «Use Debug DCUs» |
Как видим, «странными» у нас остались только функции из системных библиотек — для них, очевидно, отладочной информации у нас нет.
События
Следующее окно для ознакомления — «Event Log»:
Окно Events при запуске процесса |
Это вид окна при запуске процесса. А вот его вид после некоторой работы:
Окно Events в процессе работы программы под отладкой |
В окно «Event Log» попадает различная информация по ходу работы программы: во-первых, это уведомления о загрузке/выгрузке модулей (голубой цвет), запуске и остановке потоков и процесса (тёмно-красный и серый цвет). Во-вторых, в него помещается вывод функции OutputDebugString
(синий цвет). Для создания такой строчки, как на скриншоте, в программе была строка «OutputDebugString(‘Отладочный вывод от OutputDebugString.’);». В-третьих, это различные сообщения, связанные с точками останова (светло-красный цвет), а также сообщения от точек станова (красный цвет) и стек вызовов от них же (оранжевый цвет). Чуть позже мы обсудим точки останова более подробно. Кроме того, в это окно можно добавлять строчки и вручную — выберите пункт «Add Comment…» из контекстного меню (чёрный цвет). Также сюда добавляются уведомления об исключениях, и ещё можно включить логгинг сообщений Windows.
По-умолчанию, лог очищается при каждом запуске процесса. Вы также можете сохранить его в файл для анализа или очистить руками в середине работы — для этого воспользуйтесь соответствующими командами из контекстного меню. Кроме того, в опциях отладчика есть настройка окна «Event Log» (которая также доступна из контекстного меню окна «Event Log»).
В частности, помимо настройки поведения и внешнего вида, здесь можно включить/отключить логгинг определённых типов событий. Если интересующее вас событие происходит редко и/или тонет в общей массе событий, можно просто выключить все другие типы событий. Именно это является причиной, почему по-умолчанию отключен логгинг сообщений Windows — их всегда бывает очень много. Кроме того, вероятно, вы захотите отключить опцию «Display process info with events» — она показывает дополнительную информацию о процессе, вызвавшем событие. Поскольку чаще всего вы будете отлаживать только один процесс, эта информация не несёт полезной нагрузки и только создаёт шум в логе. В случае отладки двух процессов эта опция позволит отличать события от разных процессов.
Расширенные точки останова
В самом начале этого пункта мы буквально краем коснулись точек останова с целью быстрее познакомить вас с возможностями отладчика, т.к. они (возможности) доступны только в режиме остановки программы, а точки останова являются основным средством для установки программы на паузу. Теперь мы рассмотрим их более подробно. И для этого сначала взглянем на окно «Breakpoints»:
Список точек останова в программе |
Это окно содержит список всех точек останова в вашем проекте. Отсюда вы можете управлять ими всеми. Можно, например, удалить все точки останова, когда вы закончили отладку. Можно добавлять точки останова. Можно редактировать их свойства и временно отключать (disable). Точка останова не активна (т.е. не работает), если галочка слева от неё сброшена. Удобно временно отключать точку останова, если сейчас она вам только мешается, но в будущем ещё понадобится. Тогда вы сейчас её отключаете, а когда она снова понадобится — включаете (enable) обратно.
Кстати, включить/выключить точку останова, а также открыть окно её свойств вы можете, щёлкнув правой кнопкой мыши по красному кружку точки останова в левой части редактора кода:
Контекстное меню точки останова |
Взглянем теперь на свойства точки останова (заметим, что некоторые их этих свойств вы можете редактировать прямо в окне «Breakpoints», не открывая окна свойств):
Свойства точки останова |
Первые две строки задают место установки точки останова. Обычно они задаются автоматически, когда вы мышью ставите точку останова, но вы можете указывать их и руками — например, при ручном добавлении точки останова через команду «Add breakpoint». Строка «Condition» задаёт дополнительное условие. Если она пуста (по-умолчанию) — бряк срабатывает каждый раз, когда до него доходит выполнение, если она не пуста (задана), то он срабатывает только в случае, если условие в данном поле истинно. Разумеется, то, что вы сюда впишете, должно вычисляться, когда выполнение доходит до точки останова, и, кроме того, всё выражение в целом должно иметь тип Boolean
.
Строка «Pass Count» определяет, на который проход мимо точки останова отладчик остановит программу. 0 или 1 означает немедленную остановку. Например, если бы мы указали «Pass Count» равным двум в нашем примере, то мы бы пропустили первую итерацию цикла и остановились бы только на второй итерации. После срабатывания точки останова счётчик сбрасывается, и отсчёт начинается снова (поэтому, мы пропустили бы третью итерацию цикла и остановились бы на четвёртой, если бы она у нас была). Может комбинироваться с полем «Condition». В этом случае сперва высчитывается поле «Condition» и, если оно равно True
, то проверяется/изменяется счётчик «Pass Count».
Поле «Group» определяет группу, в которую входит точка останова. Обычно используется, если у вас много точек исключения. В этом случае их можно сгруппировать в группу и управлять всеми точками останова в группе (например, включать/выключать) одновременно как единым целым. Для включения точки останова в группу просто введите её имя в поле «Group». Если вы уже вводили название группы для другой точки останова, то вместо повторного ввода вы можете выбрать группу из раскрывающегося списка. Иногда имеет смысл включать в группу одну-единственную точку останова. Это бывает в случаях, когда вы создаёте сложные условия с помощью продвинутых (advanced) опций (описание чуть ниже).
Флажок «Keep existing breakpoint» (в старых Delphi его нет) служит для создания новой точки останова при модификации свойств уже существующей. Например, вы поставили точку останова, задали ей свойства, а потом решили поставить точно такую же точку останова, но чуть ниже, на другую строчку. Чтобы не создавать новую точку останова и не вводить все свойства заново, вы можете открыть свойства уже существующей точки останова (с проставленными свойствами), установить галочку «Keep existing breakpoint» и изменить поле «Line number» (разумеется, сначала вам нужно посмотреть в редакторе кода номер строки, на которую вы хотите установить новую точку останова).
В режиме «Advanced» (кнопка «Advanced» сворачивает или разворачивает нижнюю часть окна) вам доступны продвинутые режимы использования точек останова, которые используются значительно реже. Флажок «Break», если он установлен, определяет обычное поведение точки останова. Если вы его сбросите, то точка останова не будет приводить к остановке программы. Зачем, в таком случае, она нужна? Дело в том, что вы можете назначить некоторые события, которые будут выполняться при прохождении точки останова. Все опции в разделе «Advanced» делают именно это. Для многих из них вы, вероятно, захотите сбросить опцию «Break», т.к. вам нужно, чтобы просто сработало событие, но не нужно при этом останавливаться. В этом случае точка останова ведёт себя подобно триггеру на задаваемое действие.
Опции «Ignore subsequent exceptions» и «Handle subsequent exceptions» обычно работают парой. Если выполнение программы проходит мимо точки останова с установленной опцией «Ignore subsequent exceptions», то отладчик отключает свои уведомления об исключениях. Опция «Handle subsequent exceptions» действует ровно наоборот — она включает уведомления. Если вы отлаживаете код, в котором часто возникают исключения перед тем, как выполнение дойдёт до интересующего вас места, то вы можете установить точку останова до и после кода, возбуждающего исключения. Последовательно задавая этим точкам останова опции «Ignore subsequent exceptions» и «Handle subsequent exceptions» и сбрасывая опцию «Break», вы добьётесь игнорирования отладчиком исключений на проблемном участке кода.
Опция «Log message» заносит заданное сообщение в окно «Event Log» каждый раз, когда срабатывает точка останова.
Опция «Eval expression» вычисляет заданное выражение каждый раз при срабатывании бряка. Если при этом включена опция «Log result», то результат вычислений добавляется в «Event Log». Очень полезная функция (вместе с «Log message»), которую можно использовать для логгирования без модификации исходного кода (т.е. устанавливаются точки останова вместо OutputDebugString
в коде, и логгинг работает сразу — нет необходимости перекомпилировать и перезапускать программу). Разумеется, в отличие от OutputDebugString
, логгинг средствами точек останова работает только при отладке из-под отладчика Delphi и не доступен при автономном прогоне программы (для OutputDebugString
при этом доступен вывод от программы DebugView). Удобно использовать эти опции для «лёгкого профайлинга» (‘лёгкого’ — в смысле примитивного): для замера времени выполнения какого-то кода, установите вокруг него две точки останова. В «Eval expression» впишите GetTickCount и сбросьте опцию «Break». После прогона разница значений в логе даст вам приближённое время выполнения участка кода в миллисекундах.
«Enable/Disable group» включает или выключает группу брейкпойнтов при срабатывании текущей точки останова. Используются довольно редко, т.к. необходимы для задания довольно сложного поведения точек останова. Один из вероятных сценариев использования этих опций — отладка двух разных потоков сразу. Например, при достижении точки останова в первом потоке отключаются все точки останова во втором потоке и наоборот. Таким образом, начав отладку одного потока (первого, в котором сработает точка останова), наш процесс отладки не прервётся другим потоком. Это избавляет вас от ручного включения и выключения точек останова при нескольких проходах отладки. Другой вариант использования этих опций — отладка системных модулей. Например, вы расставили точки останова в коде VCL. Но нужный вам код VCL выполняется при запуске приложения, а вам нужно, чтобы точки останова срабатывали только после, например, нажатия на кнопку. Поэтому можно отключить точки останова, а в нужное место (например, в dpr-файле после создания форм) поставить пустую точку останова, указав, что при проходе она должна включать все точки останова. Тогда получится, что, во время загрузки приложения точки останова будут молчать, а как только пойдёт работать ваш код — тут они и сработают.
«Log Call Stack» (нет в старых Delphi) заносит в «Event Log» стек вызовов при прохождении точки останова. Например, установив бряк с этой опцией (и без опции «Break») на начало функции, можно логгировать, кто вызывает эту функцию. Опции «Entire stack» и «Partial stack» переключают логгинг всего стека или только первых «Number of frames» записей. Это невероятно удобная опция, если у вас нет под рукой готового инструмента типа JCL или EurekaLog. Поставив точки останова с этой опцией на конструкторы класса Exception (разумеется, только с включённой опцией «Use Debug DCUs», т.к. класс Exception сидит в стандартном модуле SysUtils.pas), вы во многих случаях можете упростить отладку, т.к. при возникновении исключения в «Event Log» будет попадать стек вызова для возникшего ислючения.
Практический пример
Комплексный пример использования средств отладчика для нахождения решения проблемы можно увидеть здесь.
Управлением временем жизни
Первая проблема, которой мне хотелось бы коснуться — это управление временем жизни объектов в нашей системе. Поскольку мы реализуем систему на базе интерфейсов, а интерфейсы относятся к авто-управляемым типам данным, то, к счастью, обычно нам не нужно делать никаких специальных действий: RTL Delphi позаботится обо всех вопросах управления памятью.
Тем не менее, если бы это было так просто — этого раздела тут не было бы
А дело тут в том, что в нашей программе у нас есть две вещи, которые существенно осложняют нашу жизнь:
- Явная выгрузка плагинов
- Использование неуправляемых объектов (например, формы)
Эти два момента означают, что в нашей программе имеется смешение стилей управления — ручного (на базе объектов) и автоматического (на базе интерфейсов). И вот как раз стыке двух механизмов у нас могут появляться проблемы.
Собственно, несложно сообразить, что проблем может быть всего две:
- Слишком раннее удаление (до обнуления ссылок)
- Слишком позднее удаление (не удаляется вообще)
Давайте начнём с первой проблемы.
Двойное удаление
Что означает эта проблема? Она означает, что мы удаляем объект раньше положенного времени — до того, как обнулится его счётчик ссылок. Иными словами, в момент вызова деструктора объекта счётчик ссылок интерфейса будет больше нуля. Это может происходить когда мы удаляем объект с интерфейсом через его объектную ссылку (вызовом Destroy
/Free
/FreeAndNil
). Чем вообще это плохо? Несложно сообразить, что плохо это тем, что когда счётчик ссылок упадёт до нуля — деструктор объекта будет вызван повторно: для уже удалённого объекта!
Поскольку такая проверка потребуется нам везде — и в ядре и в плагинах (мы же всюду используем эту функциональность), то нам нужно поместить этот код в общую для всех папку. Однако, существующие PluginAPI.pas/PluginAPI_TLB.pas для этого не подходят — потому что они содержит заголовочники, а мы хотим написать код поддержки, вспомогательный код. Поэтому создайте новый модуль (File/New/Unit) и сохраните его как \PluginAPI\Headers\Helpers.pas, после чего измените его следующим образом:
unit Helpers; interface uses Windows, SysUtils, Classes; type ECheckedInterfacedObjectError = class(Exception); ECheckedInterfacedObjectDeleteError = class(ECheckedInterfacedObjectError); TDebugName = String[99]; TCheckedInterfacedObject = class(TInterfacedObject) private FName: TDebugName; protected procedure SetName(const AName: String); public constructor Create; procedure BeforeDestruction; override; end; implementation uses PluginAPI; resourcestring rsInvalidDelete = 'Попытка удалить объект %s при активной интерфейсной ссылке; счётчик ссылок: %d'; { TCheckedInterfacedObject } constructor TCheckedInterfacedObject.Create; begin FName := TDebugName(Format('[$%s] %s', [IntToHex(PtrUInt(Self), SizeOf(Pointer) * 2), ClassName])); inherited; end; procedure TCheckedInterfacedObject.SetName(const AName: String); begin FillChar(FName, SizeOf(FName), 0); FName := TDebugName(AName); end; procedure TCheckedInterfacedObject.BeforeDestruction; begin if FRefCount <> 0 then raise ECheckedInterfacedObjectDeleteError.CreateFmt(rsInvalidDelete, [String(FName), FRefCount]); inherited; end; end.
В этом коде мы создали наследника от TInterfacedObject
потому что мы хотим иметь всю функциональность TInterfacedObject
, просто добавив к ней дополнительные проверки. Сама проверка сидит в служебном методе BeforeDestruction
, который вызывается непосредственно перед уничтожением объекта. Вот в нём мы и вставили нашу проверку: если счётчик ссылок отличен от нуля, то это значит, что на текущий объект ещё ссылаются какие-то интерфейсы, а мы его удаляем раньше времени — через ручной вызов деструктора по объектной ссылке.
Чтобы как-то идентифицировать объект, с которым происходит такая плохая вещь, мы ввели свойство Name. Оно не выставляется наружу, потому что это внутреннее дело объекта — для отладочных целей. Предполагается, что вы вызовите метод SetName
в конструкторе своего объекта, передав туда какую-то строку, однозначно идентифицирующую объект. Если вы это не сделаете, то имя объекта по умолчанию будет содержать hex-представление адреса объекта и имя класса объекта.
Странный, на первый взгляд, выбор типа строки для отладочного имени и загадочный вызов FillChar
я поясню чуть позже, где будет наглядно видно, зачем нам потребовалось делать именно так.
Вы можете проверить работу этого кода, сэмулировав «плохую» ситуацию:
var C: TCheckedInterfacedObject; I: IInterface; begin C := TCheckedInterfacedObject.Create; I := C; C.Free; // <- попытка удалить объект возбудит исключение, // потому что у нас есть активная интерфейсная ссылка I end;
С другой стороны:
var C: TCheckedInterfacedObject; I: IInterface; begin C := TCheckedInterfacedObject.Create; I := C; ... I := nil; // <- уменьшает счётчик ссылок до нуля и удаляет объект, нет проблем end;
Итак, теперь всюду в нашем коде, где у нас есть TInterfacedObject
мы должны заменить его на TCheckedInterfacedObject
(не забыв вписать в uses
модуль Helpers
, конечно же), опционально задать в конструкторе уникальное имя — и теперь весь наш код будет защищён от подобной ситуации. Если теперь вы допустите ошибку в коде, наш проверочный код её увидит и возбудит исключение. Вы можете остановиться в отладчике по исключению (как я описывал в начале статьи) и исследовать ситуацию: проверить стек вызовов, чтобы узнать где происходит неверное удаление, и анализом переменных выяснить, кто и почему удаляется.
Однако, если вы попробуете запустить такой тестовый пример (от самого первого примера он отличается тем, что я поменял местами удаление объекта через интерфейс и через объект):
var C: TCheckedInterfacedObject; I: IInterface; begin C := TCheckedInterfacedObject.Create; I := C; ... I := nil; C.Free; // <- добавили end;
то увидите, что наша реализация не отлавливает такие проблемы. Вместо этого объект будет удалён дважды и, в итоге, свалится с Invalid pointer operation при попытке повторного освобождения памяти.
Мы можем улучшить наш объект следующим образом:
type ... ECheckedInterfacedObjectDoubleFreeError = class(ECheckedInterfacedObjectError); TCheckedInterfacedObject = class(TInterfacedObject) private ... function GetRefCount: Integer; protected ... public ... property RefCount: Integer read GetRefCount; end; implementation ... resourcestring ... rsDoubleFree = 'Попытка повторно удалить уже удалённый объект %s'; { TCheckedInterfacedObject } ... procedure TCheckedInterfacedObject.BeforeDestruction; begin if FRefCount < 0 then raise ECheckedInterfacedObjectDoubleFreeError.CreateFmt(rsDoubleFree, [String(FName)]) else if FRefCount <> 0 then raise ECheckedInterfacedObjectDeleteError.CreateFmt(rsInvalidDelete, [String(FName), FRefCount]); inherited; FRefCount := -1; end; function TCheckedInterfacedObject.GetRefCount: Integer; begin if FRefCount < 0 then Result := 0 else Result := FRefCount; end; ...
Задача этого кода — предотвратить повторное выполнение деструктора. Для этого мы при первом удалении объекта «скидываем» счётчик ссылок в -1 — это специальное значение, которое я выбрал «от балды» (лишь бы оно не попадало в диапазон допустимых значений счётчика: 0 и положительные). Специальное значение счётчика ссылок используется в качестве маркера «объект уже удалён». Таким образом, если объект будет удалять кто-то ещё — вы увидите сообщение о попытке повторного удаления.
Примечание: повторное выполнение деструктора возможно по той причине, что при освобождении памяти, она просто помечается как «свободная», но не удаляется на самом деле. Поэтому в ней остаётся её старое содержание, к ней также можно обратиться (здесь: «можно» = «не приведёт к исключению», а не «допустимо так поступать»). Вот почему второй вызов деструктора начнёт своё выполнение без проблем. Мы исправили эту ситуацию, записав в эту «освобождаемую» память специальное значение.
Конечно же, помимо этого сценария возможны ещё два случая: память может быть действительно освобождена, а не просто помечена, как свободная, и память может быть повторно использована при последующем выделении памяти. В первой ситуации наша проверка не нужна — вы поймаете Access Violation в момент вызова деструктора. Во второй ситуации… что ж, там может быть различное поведение. И наш код как может защитить от такой ситуации, так и нет. Но чаще всего вы также схлопочете Access Violation при попытке вызова деструктора.
Итого, из трёх возможных ситуаций (память помечается «свободной», но не используется; память помечается «свободной», а затем повторно используется; память действительно освобождается) наш код проверки помогает с первым случаем, а второй и третий в наших проверках не нуждаются — они и так выбросят исключение. И во всех трёх случаях деструктор не будет выполняться повторно — что мы и хотели.
Тогда:
var C: TCheckedInterfacedObject; I: IInterface; begin C := TCheckedInterfacedObject.Create; I := C; ... I := nil; // <- удалит объект C.Free; // <- даст по рукам за повторное удаление end;
И даже:
var C: TCheckedInterfacedObject; begin C := TCheckedInterfacedObject.Create; ... C.Free; // <- удалит объект C.Free; // <- даст по рукам за повторное удаление end;
И, наконец, последняя аналогичная проблема тут — повторный вызов деструктора через интерфейсную ссылку. Это более хитрая ситуация и обычно она выглядит так: счётчик ссылок объекта падает до нуля и вызывается деструктор этого объекта. Во время выполнения деструктора какой-то из методов объекта передаёт ссылку объекта куда-то. Это приводит к увеличению счётчика ссылок с 0 до 1. Когда вызванный метод вернёт управление, счётчик ссылок уменьшится с 1 до 0, что приведёт к повторному вызову деструктору — и это прямо в самом деструкторе этого же объекта! В итоге вы получите самое разное поведение — Invalid pointer operation, Access Violation и даже зависание.
А решение этой проблемы достаточно просто: нужно возбудить ошибку при попытке увеличения счётчика ссылок с 0 до 1:
type ... ECheckedInterfacedObjectUseDeletedError = class(ECheckedInterfacedObjectError); TCheckedInterfacedObject = class(TInterfacedObject, IInterface) private ... protected ... function _AddRef: Integer; stdcall; public ... end; implementation ... resourcestring ... rsUseDeleted = 'Попытка использовать уже удалённый объект %s'; { TCheckedInterfacedObject } ... function TCheckedInterfacedObject._AddRef: Integer; begin if FRefCount < 0 then raise ECheckedInterfacedObjectUseDeletedError.CreateFmt(rsUseDeleted, [String(FName)]); Result := inherited; end; ...
Достаточно просто. В этой реализации мы учли тот факт, что у удалённого объекта счётчик ссылок будет меньше нуля, а не ноль (как мы изменили это чуть выше).
Эту новую проверку можно проверить таким образом:
var C: TCheckedInterfacedObject; I: IInterface; begin C := TCheckedInterfacedObject.Create; I := C; ... I := nil; // <- удаление объекта I := C; // <- по рукам за использование уже удалённого объекта end;
Утечки памяти
Итак, с первой проблемой (неверное удаление) мы разобрались. Давайте теперь посмотрим на вторую (противоположную) проблему — пропуск удаления объектов (утечки памяти).
Как я уже не раз упоминал в предыдущих статьях этой серии, стандартно вам не нужно волноваться о проблемах утечек, потому что интерфейсы относятся к авто-управляемым типам данных и мы не делаем низкоуровнего изменения счётчика ссылок или приведений типов. Поэтому практически единственная проблема, которая может при этом возникнуть — циклические ссылки: когда два объекта ссылаются друг на друга, и поэтому счётчик обоих больше нуля, так что оба объекта не могут быть удалены. Цепочка связи может быть косвенной и включать в себя более двух объектов.
Стандартное решение проблемы циклических ссылок — явно попросить любой из объектов отпустить ссылку на второй объект. Мы уже много раз делали это через интерфейс IDestroyNotify
. Но если вы допустите тут ошибку (забудете какой-то вызов), то вы получите утечку памяти. В итоге это может привести к Access Violation, если вы выгрузите плагин, у которого ещё остались висячие ссылки.
Первый шаг в диагностике утечек — определить, что они вообще есть. Делается это ровно так же, как я рассказывал в статье про утечки памяти (дополнение): для старых Delphi вы подключаете специальный модуль с проверкой AllocMemCount
, а для новых Delphi достаточно просто включить ReportMemoryLeaksOnShutdown
. Это даст вам простую проверку «да/нет» на наличие утечек. Конечно же, включать её нужно как для главной программы (ядра), так и для каждого плагина.
Если утечек нет — хорошо, вы нигде не ошиблись. Если же они есть — вам нужно понять, как они возникли.
Первое, что вы можете попробовать — подключить отладочный менеджер памяти. Я буду использовать FastMM как стандартный вариант для локальной отладки. Подробно его использование описано в только что упомянутой статье.
Итак, пусть у нас есть утечка памяти. Если её у вас нет, то для проверке описываемых техник на практике вы можете руками вызвать _AddRef
на какой-нибудь объект. Например, на плагин:
procedure TMainForm.FormCreate(Sender: TObject); ... begin ... Plugins[0]._AddRef; // <- без парного вызова это - утечка end;
И тогда запуск программы с FastMM в отладочном режиме даст нам такое сообщение при выходе:
Сообщение отладочного режима FastMM о найденных утечках |
А в папке программы будет создан лог-файл с таким содержанием:
--------------------------------2012/2/24 16:32:17-------------------------------- A memory block has been leaked. The size is: 52 This block was allocated by thread 0x1B0C, and the stack trace (return addresses) at the time was: 4043E2 4E227F [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259] 4E2553 [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugins][299] 4E7298 [remain.pas][remain][remain.TMainForm.FormCreate][467] 4D3B53 [Forms.pas][Forms][Forms.TCustomForm.DoCreate][3319] 4D3713 [Forms.pas][Forms][Forms.TCustomForm.Create][3189] 4DDF85 [Forms.pas][Forms][Forms.TApplication.CreateForm][9879] 4EFD31 [richedit.dpr][richeditdemo][richeditdemo.richeditdemo][18] 74FB339A [BaseThreadInitThunk] 77539EF2 [Unknown function at RtlInitializeExceptionChain] 77539EC5 [Unknown function at RtlInitializeExceptionChain] The block is currently used for an object of class: UnicodeString The allocation number is: 7093 Current memory dump of 256 bytes starting at pointer address 7EE69870: B0 04 02 00 01 00 00 00 13 00 00 00 4D 00 65 00 6E 00 75 00 20 00 44 00 65 00 6D 00 6F 00 20 00 70 00 6C 00 75 00 67 00 69 00 6E 00 20 00 23 00 31 00 00 00 16 94 80 12 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 53 1E 00 00 E2 43 40 00 02 B1 43 00 69 B2 43 00 B0 DC 46 00 F7 32 4B 00 F7 32 4B 00 71 34 4B 00 53 5F 4B 00 19 3F 4B 00 55 4D 4D 00 73 F2 4A 00 0C 1B 00 00 0C 1B 00 00 FE 43 40 00 02 B1 43 00 69 B2 43 00 B0 DC 46 00 F7 32 4B 00 F7 32 4B 00 71 34 4B 00 53 5F 4B 00 19 3F 4B 00 55 4D 4D 00 73 F2 4A 00 34 00 00 00 B0 04 02 00 3B EB 26 85 68 A2 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 C4 14 D9 7A 00 00 00 00 71 96 E6 7E ° . . . . . . . . . . . M . e . n . u . . D . e . m . o . . p . l . u . g . i . n . . # . 1 . . . . ” Ђ . . . . . . . . . . . . . . . . . . . . . . . . . S . . . в C @ . . ± C . i І C . ° Ь F . ч 2 K . ч 2 K . q 4 K . S _ K . . ? K . U M M . s т J . . . . . . . . . ю C @ . . ± C . i І C . ° Ь F . ч 2 K . ч 2 K . q 4 K . S _ K . . ? K . U M M . s т J . 4 . . . ° . . . ; л & … h ў O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . Д . Щ z . . . . q – ж ~ --------------------------------2012/2/24 16:32:17-------------------------------- A memory block has been leaked. The size is: 36 This block was allocated by thread 0x1B0C, and the stack trace (return addresses) at the time was: 4043E2 4E227F [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259] 4E2553 [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugins][299] 4E7298 [remain.pas][remain][remain.TMainForm.FormCreate][467] 4D3B53 [Forms.pas][Forms][Forms.TCustomForm.DoCreate][3319] 4D3713 [Forms.pas][Forms][Forms.TCustomForm.Create][3189] 4DDF85 [Forms.pas][Forms][Forms.TApplication.CreateForm][9879] 4EFD31 [richedit.dpr][richeditdemo][richeditdemo.richeditdemo][18] 74FB339A [BaseThreadInitThunk] 77539EF2 [Unknown function at RtlInitializeExceptionChain] 77539EC5 [Unknown function at RtlInitializeExceptionChain] The block is currently used for an object of class: UnicodeString The allocation number is: 7094 Current memory dump of 256 bytes starting at pointer address 7EE72890: B0 04 02 00 01 00 00 00 07 00 00 00 31 00 2E 00 30 00 2E 00 30 00 2E 00 30 00 00 00 52 47 80 12 5C A5 4F 00 5C A5 4F 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 58 1E 00 00 E2 43 40 00 02 B1 43 00 69 B2 43 00 B0 DC 46 00 F7 32 4B 00 F7 32 4B 00 71 34 4B 00 53 5F 4B 00 19 3F 4B 00 55 4D 4D 00 73 F2 4A 00 0C 1B 00 00 0C 1B 00 00 FE 43 40 00 02 B1 43 00 69 B2 43 00 B0 DC 46 00 F7 32 4B 00 F7 32 4B 00 71 34 4B 00 53 5F 4B 00 19 3F 4B 00 55 4D 4D 00 73 F2 4A 00 1E 00 00 00 B0 04 02 00 3A 7B 27 85 68 A2 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 C5 84 D8 7A 4F 00 5C A5 4F 00 00 00 00 00 61 27 E7 7E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 D5 1B 00 00 E2 43 40 00 7F 22 4E 00 53 25 4E 00 ° . . . . . . . . . . . 1 . . . 0 . . . 0 . . . 0 . . . R G Ђ . \ Ґ O . \ Ґ O . . . . . . . . . . . . . . . . . . . . . . . . . X . . . в C @ . . ± C . i І C . ° Ь F . ч 2 K . ч 2 K . q 4 K . S _ K . . ? K . U M M . s т J . . . . . . . . . ю C @ . . ± C . i І C . ° Ь F . ч 2 K . ч 2 K . q 4 K . S _ K . . ? K . U M M . s т J . . . . . ° . . . : { ' … h ў O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ Е „ Ш z O . \ Ґ O . . . . . a ' з ~ . . . . . . . . . . . . . . . . Х . . . в C @ . " N . S % N . --------------------------------2012/2/24 16:32:17-------------------------------- A memory block has been leaked. The size is: 212 This block was allocated by thread 0x1B0C, and the stack trace (return addresses) at the time was: 4043E2 4E7298 [remain.pas][remain][remain.TMainForm.FormCreate][467] 4D3B53 [Forms.pas][Forms][Forms.TCustomForm.DoCreate][3319] 4D3713 [Forms.pas][Forms][Forms.TCustomForm.Create][3189] 4DDF85 [Forms.pas][Forms][Forms.TApplication.CreateForm][9879] 4EFD31 [richedit.dpr][richeditdemo][richeditdemo.richeditdemo][18] 74FB339A [BaseThreadInitThunk] 77539EF2 [Unknown function at RtlInitializeExceptionChain] 77539EC5 [Unknown function at RtlInitializeExceptionChain] The block is currently used for an object of class: UnicodeString The allocation number is: 7046 Current memory dump of 256 bytes starting at pointer address 7EEC1890: B0 04 02 00 01 00 00 00 58 00 00 00 43 00 3A 00 5C 00 55 00 73 00 65 00 72 00 73 00 5C 00 10 04 3B 04 35 04 3A 04 41 04 30 04 3D 04 34 04 40 04 5C 00 44 00 6F 00 63 00 75 00 6D 00 65 00 6E 00 74 00 73 00 5C 00 52 00 41 00 44 00 20 00 53 00 74 00 75 00 64 00 69 00 6F 00 5C 00 50 00 72 00 6F 00 6A 00 65 00 63 00 74 00 73 00 5C 00 50 00 6C 00 75 00 67 00 69 00 6E 00 73 00 5C 00 45 00 78 00 61 00 6D 00 70 00 6C 00 65 00 35 00 5C 00 50 00 6C 00 75 00 67 00 69 00 6E 00 73 00 5C 00 44 00 61 00 74 00 65 00 50 00 6C 00 75 00 67 00 69 00 6E 00 2E 00 72 00 65 00 70 00 00 00 7C 51 6A 1B 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 00 00 00 00 30 15 EC 7E 00 00 00 00 00 00 00 00 3C E8 40 00 00 00 00 00 87 1B 00 00 E2 43 40 00 7F 22 4E 00 53 25 4E 00 ° . . . . . . . X . . . C . : . \ . U . s . e . r . s . \ . . . ; . 5 . : . A . 0 . = . 4 . @ . \ . D . o . c . u . m . e . n . t . s . \ . R . A . D . . S . t . u . d . i . o . \ . P . r . o . j . e . c . t . s . \ . P . l . u . g . i . n . s . \ . E . x . a . m . p . l . e . 5 . \ . P . l . u . g . i . n . s . \ . D . a . t . e . P . l . u . g . i . n . . . r . e . p . . . | Q j . O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . . . . . 0 . м ~ . . . . . . . . < и @ . . . . . ‡ . . . в C @ . " N . S % N . --------------------------------2012/2/24 16:32:17-------------------------------- A memory block has been leaked. The size is: 212 This block was allocated by thread 0x1B0C, and the stack trace (return addresses) at the time was: 4043E2 4E227F [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259] 4E2553 [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugins][299] 4E7298 [remain.pas][remain][remain.TMainForm.FormCreate][467] 4D3B53 [Forms.pas][Forms][Forms.TCustomForm.DoCreate][3319] 4D3713 [Forms.pas][Forms][Forms.TCustomForm.Create][3189] 4DDF85 [Forms.pas][Forms][Forms.TApplication.CreateForm][9879] 4EFD31 [richedit.dpr][richeditdemo][richeditdemo.richeditdemo][18] 74FB339A [BaseThreadInitThunk] 77539EF2 [Unknown function at RtlInitializeExceptionChain] 77539EC5 [Unknown function at RtlInitializeExceptionChain] The block is currently used for an object of class: TPlugin The allocation number is: 7047 Current memory dump of 256 bytes starting at pointer address 7EEC19F0: EC 1D 4E 00 01 00 00 00 78 1C 40 00 3F 54 50 6C 75 67 69 6E 28 44 61 74 65 50 6C 75 67 69 6E 2E 72 65 70 29 3A 20 7B 36 44 43 32 34 34 35 31 2D 36 43 37 33 2D 34 37 45 35 2D 39 33 39 37 2D 42 42 37 34 39 38 46 36 38 36 42 44 7D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 BC 09 4E 00 F0 F2 F6 7E 9C 18 EC 7E 00 00 C9 02 38 B1 D3 02 94 B1 D3 02 FF FF FF FF 00 00 00 00 51 44 C2 6D 73 6C E5 47 93 97 BB 74 98 F6 86 BD 7C 98 E6 7E 9C 28 E7 7E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 70 1C 4E 00 80 1C 4E 00 94 1C 4E 00 A4 1C 4E 00 B4 1C 4E 00 00 00 00 00 37 08 CE 1A 5C A5 4F 00 00 00 00 00 D1 25 EC 7E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 9A 1B 00 00 E2 43 40 00 24 8B 4E 00 A8 AC D3 02 м . N . . . . . x . @ . ? T P l u g i n ( D a t e P l u g i n . r e p ) : { 6 D C 2 4 4 5 1 - 6 C 7 3 - 4 7 E 5 - 9 3 9 7 - B B 7 4 9 8 F 6 8 6 B D } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ј . N . р т ц ~ њ . м ~ . . Й . 8 ± У . ” ± У . я я я я . . . . Q D В m s l е G “ — » t ˜ ц † Ѕ | ˜ ж ~ њ ( з ~ . . . . . . . . . . . . . . . . p . N . Ђ . N . ” . N . ¤ . N . ґ . N . . . . . 7 . О . \ Ґ O . . . . . С % м ~ . . . . . . . . . . . . . . . . љ . . . в C @ . $ ‹ N . Ё ¬ У . --------------------------------2012/2/24 16:32:17-------------------------------- This application has leaked memory. The small block leaks are (excluding expected leaks registered by pointer): 21 - 36 bytes: UnicodeString x 1 37 - 52 bytes: UnicodeString x 1 181 - 212 bytes: TPlugin x 1, UnicodeString x 1 Note: Memory leak detail is logged to a text file in the same folder as this application. To disable this memory leak check, undefine "EnableMemoryLeakReporting".
Как мы видим, у нас утекло 3 строки и 1 объект класса TPlugin
.
Как правило, более легковесные типы в логе являются «наведёнными» утечками. Т.е. если у вас есть объект с тремя строковыми полями и этот объект утёк, то в утечках появится отчёт о 4-х утечках: объекте и трёх строках. Даже хотя строки сами по себе не утекают: если вы устраните утечку объекта, то это также устранит и «утечку» строк.
По этим соображениям, если у вас в лог-файле есть много утечек — имеет смысл начинать расследование с самых крупных типов данных.
В нашем случае таковым у нас выступает объект класса TPlugin
. Уже только эта информация сообщает нам первый кусок головоломки: теперь мы знаем, что у нас где-то осталась ссылка на объект типа TPlugin
. В случае если утёк не класс и не строка, вы можете определить эту часть по стеку вызова в лог-файле. В нашем случае стек выглядит так:
4E227F [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259] 4E2553 [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugins][299] 4E7298 [remain.pas][remain][remain.TMainForm.FormCreate][467] 4D3B53 [Forms.pas][Forms][Forms.TCustomForm.DoCreate][3319] 4D3713 [Forms.pas][Forms][Forms.TCustomForm.Create][3189] 4DDF85 [Forms.pas][Forms][Forms.TApplication.CreateForm][9879] 4EFD31 [richedit.dpr][richeditdemo][richeditdemo.richeditdemo][18] 74FB339A [BaseThreadInitThunk]
Где строка 259 в LoadPlugin выглядит так:
Result := TPlugin.Create(Self, AFileName);
В нашем случае это снова подтверждает нам, что у нас утёк управляющий плагином объект. В других случаях анализ стека даст вам понятие о типе утёкших данных.
Итак, сейчас мы получили представление о том, какого рода данные у нас утекли. Теперь нам нужно их идентифицировать — ведь этих данных у нас в программе много: кто именно их них утёк? Конкретно в нашем случае у нас есть несколько TPlugin
— по одному на каждый плагин.
Чтобы идентифицировать объект мы можем посмотреть на дамп памяти объекта:
Current memory dump of 256 bytes starting at pointer address 7EEC19F0: EC 1D 4E 00 01 00 00 00 78 1C 40 00 3F 54 50 6C 75 67 69 6E 28 44 61 74 65 50 6C 75 67 69 6E 2E 72 65 70 29 3A 20 7B 36 44 43 32 34 34 35 31 2D 36 43 37 33 2D 34 37 45 35 2D 39 33 39 37 2D 42 42 37 34 39 38 46 36 38 36 42 44 7D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 BC 09 4E 00 F0 F2 F6 7E 9C 18 EC 7E 00 00 C9 02 38 B1 D3 02 94 B1 D3 02 FF FF FF FF 00 00 00 00 51 44 C2 6D 73 6C E5 47 93 97 BB 74 98 F6 86 BD 7C 98 E6 7E 9C 28 E7 7E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 70 1C 4E 00 80 1C 4E 00 94 1C 4E 00 A4 1C 4E 00 B4 1C 4E 00 00 00 00 00 37 08 CE 1A 5C A5 4F 00 00 00 00 00 D1 25 EC 7E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 9A 1B 00 00 E2 43 40 00 24 8B 4E 00 A8 AC D3 02 м . N . . . . . x . @ . ? T P l u g i n ( D a t e P l u g i n . r e p ) : { 6 D C 2 4 4 5 1 - 6 C 7 3 - 4 7 E 5 - 9 3 9 7 - B B 7 4 9 8 F 6 8 6 B D } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ј . N . р т ц ~ њ . м ~ . . Й . 8 ± У . ” ± У . я я я я . . . . Q D В m s l е G “ — » t ˜ ц † Ѕ | ˜ ж ~ њ ( з ~ . . . . . . . . . . . . . . . . p . N . Ђ . N . ” . N . ¤ . N . ґ . N . . . . . 7 . О . \ Ґ O . . . . . С % м ~ . . . . . . . . . . . . . . . . љ . . . в C @ . $ ‹ N . Ё ¬ У .
Достаточно легко увидеть в нём такой блок:
... T P l u g i n ( D a t e P l u g i n . r e p ) : { 6 D C 2 4 4 5 1 - 6 C 7 3 - 4 7 E 5 - 9 3 9 7 - B B 7 4 9 8 F 6 8 6 B D } ...
Или:
TPlugin(DatePlugin.rep): {6DC24451-6C73-47E5-9397-BB7498F686BD}
Вот теперь совершенно очевидно, что у нас проблема с плагином DatePlugin.rep (ну, в данном случае он просто оказался загруженным первым).
Откуда в дампе взялась эта строка? А вот откуда:
constructor TPlugin.Create(const APluginManger: TPluginManager; const AFileName: String); ... begin OutputDebugString(PChar('TPlugin.Create: ' + ExtractFileName(AFileName))); SetName(Format('TPlugin(%s)', [ExtractFileName(AFileName)])); inherited Create; ... FID := FPlugin.ID; ... SetName(Format('TPlugin(%s): %s', [ExtractFileName(AFileName), GUIDToString(FID)])); end;
Т.е. это отладочное имя, которое мы ввели выше в TCheckedInterfacedObject
. Сейчас вам должно быть уже понятно, почему в качестве типа данных для FName
была выбрана короткая строка: потому что она статическая — т.е. размещается непосредственно в поле данных. Если бы мы использовали бы динамические строки (String
, AnsiString
, WideString
, UnicodeString
, PChar
, PAnsiChar
, PWideChar
), то вместо содержания строки мы бы увидели в дампе адрес — указатель на данные строки, которые в дамп как раз не попали бы. Не очень полезно, да. Это же объясняет и вызов FillChar
сделано это было, чтобы было удобнее читать строку в дампе. Удобно, когда неиспользуемые данные обнулены, а не заполнены мусором, который мы по ошибке можем принять за строку.
Итак, с помощью подсказки FastMM (имя класса), стека вызовов и дампа памяти мы сумели точно идентифицировать проблемный объект: это управляющий класс-обёртка TPlugin
, созданный для плагина DatePlugin.rep.
Хорошо, шаг первый (идентификация ресурса) мы сделали. Теперь шаг два — найти место предполагаемого удаления и выяснить, почему это там не произошло удаления.
Вот тут начинаются более интересные вещи. Если бы у нас был объект или любой другой управляемый вручную тип данных, шаг два был бы тривиален:
O := TSomeObject.Create; ... FreeAndNil(O);
Всё, что нам нужно было бы сделать — остановиться на строке «O := TSomeObject.Create» и протрассировать программу по шагам до строки «FreeAndNil(O)», следя не теряем ли мы в пути ссылку и доходим ли мы вообще до строки удаления. Всё.
Но поскольку мы имеем интерфейс (авто-управляемый тип данных), то шаг два будет выглядеть существенно сложнее. Не, место предполагаемого удаления найти не проблема — очевидно, что это вызов _Release
: ведь объект должен удаляться, когда счётчик ссылок опускается до 0. Вызов деструктора происходит именно внутри _Release
. А проблема тут в том, что _Release
вызывается не только для удаления объекта, но и вообще весьма даже часто — для учёта ссылок. Более того, сам его вызов не имеет значения — важно, насколько парным он будет для вызова _AddRef
. Иными словами, в правильной ситуации у нас должно быть равное число вызовов _AddRef
и _Release
(на каждый _AddRef
приходится вызов _Release
). В неправильной (есть утечка) — один из вызовов _Release
был пропущен. Ситуация с испорченной ссылкой на интерфейс теоретически возможна, но крайне маловероятна — в таком случае мы в 99.99% случаев получим Access Violation в первый же момент вызова _Release
для испорченной ссылки.
Итак, в нашем случае шаг два сводится к тому, чтобы определить пропущенный вызов _Release
.
Для новых Delphi сделать это достаточно просто:
- Продублируйте метод
TInterfacedObject._Release
вTCheckedInterfacedObject._Release
. - Установите на
TCheckedInterfacedObject._AddRef
иTCheckedInterfacedObject._Release
две non-breaking точки останова, указав обеим условие «Self.FName = ‘TPlugin(DatePlugin.rep): {6DC24451-6C73-47E5-9397-BB7498F686BD}'» (без кавычек, естественно), введя выражение для логгинования «Self.FRefCount» (и снова — без кавычек), а также включив логгирование стека вызовов (можно полностью, а можно только N записей, где N — по вкусу). - Отключите запись всех событий, кроме Breakpoint Messages (и, по вкусу, Output Messages).
- Запустите программу, а после её выполнения сохраните содержимое окна Events в лог-файл.
В результате вы получите примерно такой лог (показан полностью, но подредактирован для улучшения читабельности):
Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) PluginManager.TPluginManager.LoadPlugins(???,'.rep') remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 2 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) PluginManager.TPluginManager.LoadPlugins(???,'.rep') remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.LoadPlugin('C:\Users\Александр\Documents\RAD Studio\Projects\Plugins\Example5\Plugins\ExportRTF.rep') PluginManager.TPluginManager.LoadPlugins(???,'.rep') remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.BuildFilterList remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.BuildFilter((1820700819, 31407, 20208, (185, 142, 217, 219, 222, 149, 7, 24))) remain.TMainForm.BuildFilterList remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.BuildFilterList remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.BuildFilter((155353976, 13498, 17190, (133, 80, 191, 28, 167, 47, 223, 83))) remain.TMainForm.BuildFilterList remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) PluginManager.TPluginManager.DoLoaded remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 5 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 6 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.PopulatePlugins remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 7 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.PopulatePlugins remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 6 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.PopulatePlugins remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 5 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.PopulatePlugins remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfClear(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 5 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.TObject.GetInterface((3037498046, 41226, 19417, (170, 23, 3, 17, 50, 111, 225, 166)),(no value)) System.TInterfacedObject.QueryInterface((3037498046, 41226, 19417, (170, 23, 3, 17, 50, 111, 225, 166)),(no value)) PluginManager.TPlugin.QueryInterface((3037498046, 41226, 19417, (170, 23, 3, 17, 50, 111, 225, 166)),(no value)) SysUtils.Supports(Pointer($7EEC1AB8) as IInterface,(3037498046, 41226, 19417, (170, 23, 3, 17, 50, 111, 225, 166)),(no value)) PluginManager.NotifyRelease PluginManager.TPluginManager.UnloadAll remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfClear(???) PluginManager.NotifyRelease PluginManager.TPluginManager.UnloadAll remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 5 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release PluginManager.TPlugin.ReleasePlugin PluginManager.TPlugin.Delete PluginManager.NotifyRelease PluginManager.TPluginManager.UnloadAll remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfClear(???) PluginManager.TPluginManager.UnloadAll remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfClear(???) remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 2
Во-первых, мы видим, что первая запись в логе начинается с числа 2 — это означает, что было сделано два вызова _AddRef
, которые не попали в лог — т.е. они были вызваны до установки отладочного имени объекта. Хорошо, учтём это.
Теперь считаем, сколько в логе есть вызовов. В этом логе есть 12 (+ 2) вызовов _AddRef
и 13 вызовов _Release
. Т.е. 14 вызовов _AddRef
и 13 вызовов _Release
. Одного вызова _Release
не хватает. Это же подтверждается счётчиком ссылок: он равен 2 в последней записи лога. Поскольку точка останова стоит на начало метода, то это значение — до выполнения метода _Release
. После его выполнения 2 станет 1. И никаких больше вызовов у нас нет. Т.е. финальное значение счётчика ссылок — 1. Это также подтверждается дампом памяти от FastMM, где счётчик ссылок лежит первым полем:
1C 1E 4E 00 01 00 00 00 ...
Здесь 1C 1E 4E 00 — развёрнутый указатель на VMT объекта (т.е. $004E1E1C), а 01 00 00 00 — первое поле (т.е. $00000001 или просто 1).
Итак, мы подтвердили утечку объекта из-за отсутствия одного парного вызова _Release
к какому-то из _AddRef
. Теперь нам осталось всего-лишь найти этот вызов _AddRef
. Для этого нам нужно изучить лог-файл и каждую запись _AddRef
в нём. Для каждой записи мы должны найти исходный код (к сожалению, в логе нет номеров строк, но, тем не менее, это можно сделать), затем найти парный вызов _Release
к этому месту и проверить его наличие в логе.
К примеру, возьмём первые три записи в логе. Мы видим два _AddRef
и один _Release
. Все они вызваны из системной функции копирования интерфейсов (IntfCopy
), которая в свою очередь вызвана из TPluginManager.LoadPlugins
. Отсюда следует, что в TPluginManager.LoadPlugins
была сделана копия ссылки на интерфейс. Но если вы посмотрите на исходный код TPluginManager.LoadPlugins
, то увидите, что там нет таких мест. Зато там есть вызов TPluginManager.LoadPlugin
. Мы делаем предположение, что вызов TPluginManager.LoadPlugin
не попал в стек вызова. Такое бывает. Посмотрим, согласуется ли это с нашим логом. В TPluginManager.LoadPlugin
есть два места по увеличению ссылок:
- Запись ссылки в
Result
:Result := TPlugin.Create(Self, AFileName);
- Запись ссылки в
FItems
:FItems[FCount] := Result;
Уменьшения ссылки нет. Но зато оно есть в вызвавшей нас функции TPluginManager.LoadPlugins
:
LoadPlugin(Path + SR.Name); // LoadPlugin - функция, возвращает IPlugin
Итак, вроде всё согласуется. Будем считать, что наша гипотеза верна и мы реконструировали ситуацию: LoadPlugin
создаёт плагин, записывает ссылку в Result, который затем будет освобождёт в LoadPlugins
как ненужный. Кроме того, эта ссылка записывается в массив FItems
. Итого: +2 -1 — и у нас осталась ссылка в FItems
.
Если вы не можете высказать гипотезу или проверить её — просто включите остановку на точках останова и задайте условия так, чтобы остановиться в нужный момент (на нужной записи). Например, задав Pass Count. После этого выполните пошаговую трассировку, поднимаясь из вызова _AddRef
или _Release
к вызывающему — это даст вам полную картину происходящего.
Так где же должна удаляться ссылка на объект в FItems
? FItems
удаляется в UnloadAll
. Есть у нас такой вызов? Есть:
Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfClear(???) PluginManager.TPluginManager.UnloadAll remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3
Окей, +2 -2. Т.е. мы можем вычеркнуть из лога эти 4 блока. Останется 10 (+2) _AddRef
и 11 _Release
.
Смотрим дальше. После вычёркивания первые 4 вызова в новом (подправленном) логе являются парными (происходят в BuildFilterList
/BuildFilter
: +2 -2 — и потому тоже могут быть вычеркнуты. Остаётся 8 (+2) вызовов _AddRef
и 9 вызовов _Release
.
Следующие два вызова — снова парные. На этот раз — внутри DoLoaded
. Вычёркиваем. Осталось 7 (+2) и 8.
Дальше идёт 4 вызова _AddRef
подряд и 4 вызова _Release
подряд. Хотя стек для _AddRef
снова не полон, но стек для _Release
даёт нам подсказку, что это происходит в PopulatePlugins
. И, действительно:
procedure PopulatePlugins; var X: Integer; MI: TMenuItem; begin for X := 0 to Plugins.Count - 1 do begin MI := TMenuItem.Create(MainMenu); MI.Caption := Format('%d: %s', [Plugins[X].Index, Plugins[X].Name]); // раз и два MI.Hint := Format('ID: %s', [GUIDToString(Plugins[X].ID)]); // три MI.Tag := Plugins[X].Index; // четыре MI.OnClick := PluginClick; miLoaded.Add(MI); end; end;
Получается, что все вызовы — парные и, значит, могут быть вычеркнуты. Останется 5 (+2) и 6.
Следующий вызов — как раз проблемный:
Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3
Для этого вызова нам не удастся найти не то что пару, а даже место в коде, где мы предполагаем очистить ссылку — что и будет говорить о баге в этом месте.
На практике такое вряд ли у вас будет — вы же будете так делать специально. Гораздо вероятнее такая ситуация: мы нашли место, где планируете освободить ссылку (скажем, в FormDestroy
), но в логе такого вызова нет. Это означает, что у нас нет пары к этому вызову _AddRef
. Вот вам и проблема. Теперь, всё что вам осталось сделать — пошаговую трассировку, чтобы выяснить почему это был пропущен (гипотетический в данном случае) вызов _Release
в предполагаемом месте.
Если вы не уверены насчёт своих размышлений по поводу этого вызова — можете пока оставить его в логе. Просто продолжайте вычёркивать парные записи и дальше, пока у вас не останется три записи — два _Release
для тех вызовов _AddRef
, что произошли до присвоения отладочного имени. И один вызов _AddRef
, который не имеет пары. Вот вам и проблема (найденная другим способом).
Если же у вас старые версии Delphi, то вы не сможете построить вышеуказанный лог. В этом случае просто сделайте точки останова обычными (Break). Запустите программу — и следите за стеком вызовов в окне Call Stack каждый раз, когда вы останавливаетесь на точках останова. Ищите парные вызовы, игнорируйте их, а то, что останется (не парные) — это и будет проблемой.
Вот, собственно, и всё.
Замечу только, что вместо того, чтобы искать утечки памяти «в лоб» — можно попробовать решить проблему пересмотром кода. Иногда это может быть быстрее, иногда — нет. Просто просмотрите свой код на предмет сохраняемых в поля объекта интерфейсный ссылок. Убедитесь, что каждая такая ссылка обнуляется в уведомлении от IDestroyNotify
. Если нет — добавьте.
Access Violation
Итак, Access Violation. Самая популярная ошибка в Delphi программах (и не только Delphi программах). Причина для неё всего одна — вы обратились к памяти, которая недоступна. Но происходить это может из-за совершенно разных ошибок.
Во-первых, я замечу, что если вы получаете в программе исключение Access Violation, то в первую очередь вам нужно:
- Включить отладочные опции (в первую очередь — Range Check Errors и Assertions).
- Просмотреть код и убедиться, что вы не нарушаете правила.
- Убедиться, что в коде нет проблем с временем жизни (см. пункт 2 в этой статье).
Так что ниже я буду предполагать, что ваша проблема НЕ вызвана одним из вышеуказанных пунктов. Эти случаи были уже более чем подробно описаны, поэтому я не буду дублировать их здесь ещё раз. Кроме того, я не буду описывать общие методы решения проблем, поскольку они так же описаны в отдельной статье. Ниже я приведу только несколько конкретных примеров, специфичных именно для нашей схемы плагинов.
Неверные сигнатуры
Одна из проблем, специфичная для разработки библиотек — несовпадение сигнатур (прототипов) функций. Например:
// DLL: function DoSomething(A: Integer): Integer; stdcall; // .exe: var DoSomething: function(A: Integer): Integer;
Как видите, функция объявлена по-разному в DLL и .exe (во втором случае пропущен stdcall
). Несложно сообразить, что попытка вызвать функцию приведёт к всяческим плохим вещам (хотя и не всегда).
К сожалению, подобные ошибки не могут быть пойманы компилятором, потому что они включают в себя две стороны — вас и вторую сторону, которая не является частью проекта.
Чтобы исправить подобную ошибку вам нужно просто внимательно просмотреть свой код и проверить, что все сигнатуры совпадают. Крайне полезно не дублировать объявления, а использовать общие файлы. На эту ошибку стоит грешить в первую очередь, если ваша программа вылетает при вызове внешней функции или непосредственно в момент возврата из неё.
Передача данных
Вторая проблема, специфичная для библиотек — неверное управление памятью. Я подробно пояснял это в отдельной статье. В DLL и в .exe есть свои собственные менеджеры памяти. Это — два разных, раздельных менеджера памяти. Поэтому, память, выделенную в одном исполняемом модуле (DLL/.exe), нельзя освободить в другом модуле (.exe/DLL) и наоборот. А если вы попытаетесь это сделать — вы испортите состояние менеджера памяти, что приведёт к Access Violation, вылетам, порче данных или зависанию программы.
В нашей схеме мы легко обходим эту проблему, потому что мы обмениваемся только интерфейсами, для данных мы используем потоки данных или аналогичные обёртки, а для строк у нас используется WideString
— это специальный тип данных, который всегда использует системный менеджер памяти, так что его можно безболезненно передавать между модулями.
Однако, если вы вздумаете передавать в плагин (или, наоборот, в ядро) какие-то данные напрямую — вам лучше бы продумать управление памятью иначе у вас будут происходить плохие вещи.
Ещё раз уточню, что при чтении и записи данных в блок памяти другого модуля проблем нет. Весь вопрос тут заключается только в выделении и высвобождении памяти.
В принципе, менеджер памяти в отладочном режиме должен помочь решить эту проблему. При попытке сделать что-то глупое вроде удаления блока памяти из чужого исполняемого модуля он немедленно возбудит ошибку типа Invalid Pointer Operation, так что вы сможете найти источник проблемы.
Многопоточность
Следующая проблема не специфична для библиотек, да и в нашей системе мы пока с ней не сталкивались. Тем не менее, я упомяну её тут отдельным пунктом, потому что тут есть один не совсем очевидный момент.
Итак, если в вашей программе используется несколько потоков и вы получаете совершенно случайные Access Violation в разных, не связанных местах, то ошибки организации многопоточности — первые подозреваемые. При этом проблемы происходят из-за ошибок в синхронизации доступа к общим ресурсам (не обязательно глобальным). Частным случаем этих проблем является неверно установленный режим работы менеджера памяти.
Напомню, что по умолчанию менеджер памяти работает в режиме одного потока. Это значит, что он не выполняет блокировку при доступе к внутренним данным учёта. Такой режим включен по умолчанию по соображениям производительности — большинство программ однопоточны, а отсутствие необходимости выполнять синхронизацию позволяет работать с полной скоростью. Но если вы будете использовать этот режим в многопоточном приложении, то получите проблему одновременного, не синхронизированного доступа двух потоков к глобальным данным учёта. Понятно, что ни к чему хорошему это не приведёт.
Менеджер памяти переводится в многопоточный режим работы (в котором он выполняет дополнительную синхронизацию доступа) использованием TThread
, BeginThread
или вручную — через IsMultiThreaded
.
Проблемы возникнут, если вы забудете это сделать для многопоточного приложения. Особенно коварна эта ситуация тем, если, к примеру, вы в своём плагине не используете потоки, а ваши функции ядро будет вызывать из разных потоков. Таким образом, даже хотя ваш код потоки не создаёт, но в результате вам всё равно нужно использовать многопоточный режим работы.
Вообще, подобные вещи (кто кого и из какого потока вызывает) должны быть прописаны в документации к плагинам. Если это не указано, то следует подразумевать, что вся работа с плагином должна происходить в контексте одного и того же потока. Как правило, так и поступают — просто не указывают требования к потокам, подразумевая именно такое поведение. А там, где поведение иное (скажем, какая-то функция, которую можно вызывать из любого потока) — в документации явно прописывают эти особые условия.
Итак, решение проблемы: просто вставьте IsMultiThreaded := True
в код инициализации программы и/или плагинов. Чем раньше он будет выполняться — тем лучше.
Обработка ошибок в плагинах
Фух, я сказал УЖАСНО МНОГО всего про отладку плагинов и ядра. Но отладка — это лишь часть методов поиска проблем. Было бы неплохо, если бы сама система помогала бы нам в поиске причин ошибок. Особенно это справедливо для случаев, когда отладку применить не получится. Для этого нам необходимо, чтобы система сообщала нам об ошибках, приводя при этом максимум деталей.
Давайте посмотрим, что там у нас с обработкой ошибок в программе. Как вы помните из первой части, мы ввели такие правила: всё строим на интерфейсах, а методы интерфейса имеют тип вызова safecall
.
Safecall
Заметим, что исключения не должны пересекать границы модулей. Это связано с тем, что исключение представляется объектом Delphi. Соответственно, программа на, скажем, C++ ничего не знает про объекты Delphi. Соответственно, она не сможет правильно обработать исключение Delphi и правильно освободить ресурсы. Вот почему исключение должно обрабатываться в том же модуле, где оно было возбуждено. За вычетом исключений остаётся только обработка ошибок на базе кодов ошибок, что является достаточно неудобным делом. Практически все проблемы новичков «этот код не работает» сводятся к одну наивному программисту, не умеющему делать правильную обработку ошибок для подхода с кодами ошибок.
Итак, в Delphi есть понятие SafeCall-вызова. Он характеризуется тем, что сам компилятор следит за тем, чтобы исключение не вышло за пределы метода в явном виде, а только в виде кода ошибки. Любое исключение, пересекающее границу SafeCall-метода таким образом, иногда называют safecall-исключением. По сути же оно ничем не отличается от остальных исключений. Для примера рассмотрим, например, такой простой объект:
type ETestException = class(Exception); TTestObject = class(TObject) function TestMe: Integer; safecall; end; function TTestObject.TestMe: Integer; safecall; begin raise ETestException.Create('Тестовое исключение.'); Result := -1; end;
Здесь мы самым нахальным образом нарушаем требование о том, что метод обязан ловить все исключения.
Но из-за того, что метод помечен как safecall
, компилятор предпринимает дополнительные действия. Во-первых, несмотря на то, что мы объявили метод как возвращающий значение типа Integer
, компилятор воспринимает его, как возвращающий тип HRESULT
. Посмотрим на скомпилированный код в виде псевдокода:
function TTestObject.TestMe(out AResult: Integer): HResult; stdcall; begin try // Начало кода функции raise ETestException.Create('Тестовое исключение.'); AResult := -1; // Конец кода функции Result := S_OK; except on E: Exception do Result := HandleAutoException(E); end; end;
Как видим, кроме модификаций прототипа (заголовка), компилятор обернул тело функции в try-except.
Грубо говоря, HandleAutoException
делает две вещи: вызывает виртуальную функцию TObject.SafeCallException
и удаляет объект исключения E. Назначение этой функции просто: вы должны конвертировать в ней исключение в код ошибки. Поскольку TObject
ничего не знает о том, как вы хотите обрабатывать исключения, ни о том, какие исключения могут возникнуть в ваших методах, то его умалчиваемая реализация предельно проста:
function TObject.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; begin Result := HResult($8000FFFF); { E_UNEXPECTED } end;
Чуть позже мы приведём пример своей реализации этого метода, а пока посмотрим, как работает магия компилятора при вызове SafeCall-метода. Код «I := TestObject.TestMe;» компилятор компилирует в:
CheckAutoResult(TestObject.TestMe(I));
Где CheckAutoResult
проверяет возвращаемое значение и, если оно неуспешно (в смысле HRESULT
), то вызывает функцию SafeCallErrorProc
, а если она не назначена — то возбуждает run-time ошибку номер 229, которая при подключенном SysUtils
преобразуется в ESafeCallException
.
Стандартные средства
Итак, использование safecall
означает обработку ошибок на базе исключений, а не кодов ошибок. Откуда следует:
- Вам не нужно вставлять явные проверки успешности вызова;
- Ситуация по умолчанию — реакция на ошибку, а не скрытие;
- По умолчанию исключения всплывают до глобального обработчика, который показывает сообщение об ошибке;
- Вы можете передавать с исключениями дополнительные данные;
- Вы можете использовать наследование;
В реализации по-умолчанию в TObject
исключения в safecall-методах перехватываются и конвертируются в код ошибки E_UNEXPECTED
, на вызывающей стороне при отключенном SysUtils
это приводит к возникновению runtime-ошибки 229:
Обработка SafeCall-исключения модулем System |
Пока это не очень похоже на «помогать с идентификацией ошибки», как мы бы хотели. Но, в конце концов, это пока всего-лишь модуль System
. При подключенном SysUtils
мы получаем ESafeCallException
(«Исключение в SafeCall-методе»):
Обработка SafeCall-исключения модулем SysUtils |
А при подключении модуля ComObj
подключается пользовательская процедура SafeCallErrorProc
, которая возбуждает EOleException
, которое, в отличие от ESafeCallException
, уже учитывает некоторую информацию об исключении (напомним, что E_UNEXPECTED
— это ошибка типа «Разрушительный сбой»):
Обработка SafeCall-исключения модулем ComObj |
В итоге у нас получается, что исключение как бы пересекает границу между объектом и вызывающей стороной. На самом деле, конечно, оно не пересекает её — на границе исключение конвертируется в HRESULT
и затем, после пересечения границы, собирается обратно (кстати, учитывайте этот момент, если будете делать трассировку стека для исключений). Просто эта реализация скрыта компилятором. Заметьте, что для этого мы не пишем ни единой строчки кода по управлению исключениями — все действия выполняются автоматически компилятором. Плюс ещё в нагрузку мы получаем возможность использовать функции как функции (из-за того, что HRESULT
скрыт из кода).
Но этого всё ещё недостаточно.
Как вы уже поняли, много полезных функций для SafeCall-методов находятся именно в модуле ComObj
. Например, там есть функция HandleSafeCallException
, позволяющая передать вместе с кодом ещё и дополнительную информацию об исключении. При этом используются стандартные способы ОС, поэтому этой информацией может воспользоваться вызывающий код. К сожалению, доступно только ограниченное количество полей для передачи. Во-первых, это код ошибки. Если возникшее исключение будет класса EOleSysError
, то код ошибки возьмётся из свойства ErrorCode
объекта исключения, для всех прочих исключений это будет E_UNEXPECTED
. Во-вторых, это само сообщение исключения (свойство Message исключения). В-третьих, это GUID объекта, возбудившего исключение. Может быть пустым GUID или вы можете сгенерировать GUID для своего объекта и указать его. Только не забудьте, что GUID должен быть уникальным — не следует использовать один и тот же GUID для двух разных классов. Далее, это идентификатор места возникновения ошибки — произвольная строка (иногда здесь удобно передавать имя класса исключения). И, наконец, имя файла справки, ассоциированного с исключением. Если в вашем приложении есть файл справки с описанием ошибок, то в этом поле должно идти полное имя этого файла справки. Конкретный контекст (раздел справки) берётся из свойства HelpContext
объекта исключения. Несмотря на то, что у HandleSafeCallException
есть параметр ExceptAddr
, в текущей реализации под Windows он не используется. Заметим, что его обычно и не нужно передавать. Дело в том, что исходное исключение всё равно заканчивает свою жизнь на границе метода, поэтому обычно этот адрес лишён смысла для клиентской (вызывающей) стороны.
Кроме того, следует сказать, что, чтобы использовать этот механизм в COM-объектах, объект должен ещё реализовывать интерфейс ISupportErrorInfo
. Вызывая ISupportErrorInfo.InterfaceSupportsErrorInfo
, клиентская сторона может определить, что объект поддерживает дополнительную информацию. Но для обычных объектов (не являющихся COM-объектами) этого делать, разумеется, не обязательно. Ведь достаточно просто указать в документации к своему коду, как его нужно использовать. Например, такие слова: Delphi-программисты — используйте SafeCall; все остальные — используйте IErrorInfo
, который передаётся с использованием функций SetErrorInfo
/GetErrorInfo
.
Итак, с учётом сказанного, мы можем дописать наш тестовый пример так:
const TestObjGUID: TGUID = '{9044E2E9-B9D9-4E03-9264-8CB0BFB65FD0}'; type ETestException = class(Exception); TTestObject = class(TObject) function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; override; function TestMe: Integer; safecall; end; function TTestObject.TestMe: Integer; safecall; begin raise ETestException.Create('Тестовое исключение.'); Result := -1; end; function TTestObject.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; begin // Здесь Result - это код ошибки, вы можете вернуть свой код // в зависимости от типа исключения. // HandleSafeCallException релизует стандартное добавление информации к исключению Result := HandleSafeCallException(ExceptObject, ExceptAddr, TestObjGUID, String(ExceptObject.ClassName), '' { файл справки }); end;
Теперь при вызове метода TestMe мы получим более подробное сообщение об ошибке (разумеется, только при подключенном модуле ComObj
):
Обработка SafeCall-исключения с дополнительной информацией |
Как видим, чисто визуально картина не отличается от обработки обычного (не SafeCall) исключения модулем Forms
. Но при этом на вызывающей стороне возбуждается исключение типа EOleException
, у которого заполнены свойства ErrorCode
(для нашего примера это E_UNEXPECTED
), Message
(‘Тестовое исключение.’), Source
(‘ETestException’), HelpFile
(») и HelpContext
(0). GUID объекта в нашей реализации никуда не сохраняется.
Дополнение стандартных средств
Это — уже гораздо лучше, но всё ещё не достаточно хорошо. Давайте перечислим проблемы:
- Невозможно определить тип исключения по коду исключения
- Не совпадают классы исключений на вызываемой (класс исходного исключения) и вызывающей стороне (
EOleException
); - Не работает наследование (следствие из предыдущего пункта);
- Нет передачи дополнительной информации;
- Место возникновения ошибки не идентифицируется;
Итого, несмотря на то, что исключение автоматически переносится через границу модуля и даже сообщает исходное сообщение об ошибке — пользы от этого не много. Давайте посмотрим, что мы можем с этим сделать.
Далее, я буду предполагать, что все дополнения мы будем вносить в класс TCheckedInterfacedObject
, который мы ввели в разделе 2 этой статьи выше. Так уж получилось, что этот класс является у нас базой для всех объектов с интерфейсами, так что внеся правки в него, мы дадим нашу дополнительную функциональность всем интерфейсам в программе. Если в коде будут использоваться объекты, реализующие интерфейсы, но не являющиеся наследниками TCheckedInterfacedObject
, то вам нужно будет скопировать наш код в эти классы.
Актуальные коды исключений
Итак, реализация по умолчанию всегда возвращает код ошибки E_UNEXPECTED
— что бесполезно, если мы хотим уметь отличать одно исключение от другого. Простое решение этой проблемы заключается в наследовании наших классов исключений от EOleSysError
. Конструктор класса будет формировать свой собственный код исключения, который затем подцепится стандартным кодом и передастся вызывающему. Альтернативное решение — оставить классы как есть, но переопределить стандартный код, чтобы он строил код исключения по классу. В принципе, эти два подхода можно совместить, что я и сделаю. Мне кажется это удобным, потому что вы сможете использовать однотипный код проверки на коды исключений и в вызываемом и в вызывающем, но при этом вы также будете иметь коды исключений для всех прочих классов.
Сразу замечу, что особняком здесь будут стоять системные коды ошибок. В Delphi они представлены EOSError
.
Итак, вопрос — как нам создавать свои собственные коды исключений? Да ещё делать это так, чтобы код существовал для любого наперёд заданного класса.
Напомню, что для «своих» кодов в HRESULT
предназначен «поставщик» FACILITY_ITF
. Эти коды выделяются программистом, а не системой. Причём эти коды зависят от компонента. Т.е. у вас может быть два класса с одинаковыми кодами исключений. А разными их делает тот факт, что возвращаются они разными классами. Сам источник ошибки идентифицируется по GUID. Для системных кодов ошибок используется нулевой GUID. А под непосредственно код ошибки отводится 16 бит (Word).
16 бит — этого достаточно, чтобы разместить в них CRC-код (CRC16). Откуда следует простой алгоритм создания кодов исключений: просто возьмите CRC от имени класса исключения и сохраните полученное значение в HRESULT с FACILITY_ITF
.
Итак, давайте напишем функцию получения кода по исключению:
function HResultFromException(const E: Exception): HRESULT; begin // Базовый класс Exception - кода стандартен if E.ClassType = Exception.ClassType then Result := E_UNEXPECTED else // Для EOleSysError у нас уже есть код if E is EOleSysError then Result := EOleSysError(E).ErrorCode else // Для EOSError код можно получить if E is EOSError then Result := HResultFromWin32(EOSError(E).ErrorCode) else // Для всех прочих - строим код сами Result := MakeResult(SEVERITY_ERROR, FACILITY_ITF, CalcCRC16(E.ClassName)); end;
Тогда, базовый класс для всех исключений в системе плагинов будет выглядеть так:
type EBaseException = class(EOleSysError) private function GetDefaultCode: HRESULT; public constructor Create(const Msg: string); constructor CreateFmt(const Msg: string; const Args: array of const); constructor CreateRes(Ident: Integer); overload; constructor CreateRes(ResStringRec: PResStringRec); overload; constructor CreateResFmt(Ident: Integer; const Args: array of const); overload; constructor CreateResFmt(ResStringRec: PResStringRec; const Args: array of const); overload; constructor CreateHelp(const Msg: string; AHelpContext: Integer); constructor CreateFmtHelp(const Msg: string; const Args: array of const; AHelpContext: Integer); constructor CreateResHelp(Ident: Integer; AHelpContext: Integer); overload; constructor CreateResHelp(ResStringRec: PResStringRec; AHelpContext: Integer); overload; constructor CreateResFmtHelp(ResStringRec: PResStringRec; const Args: array of const; AHelpContext: Integer); overload; constructor CreateResFmtHelp(Ident: Integer; const Args: array of const; AHelpContext: Integer); overload; end; ... { EBaseException } function EBaseException.GetDefaultCode: HRESULT; begin Result := MakeResult(SEVERITY_ERROR, FACILITY_ITF, CalcCRC16(ClassName)); end; constructor EBaseException.Create(const Msg: string); begin inherited Create(Msg, GetDefaultCode, 0); end; constructor EBaseException.CreateFmt(const Msg: string; const Args: array of const); begin inherited Create(Format(Msg, Args), GetDefaultCode, 0); end; // ... и т.д.
Смысл кода в том, что каждый конструктор теперь устанавливает код исключения, возвращаемый нашей функцией.
Теперь нам нужно во всех наших исходниках заменить строчку «class(Exception)» на «class(EBaseException)», например:
ECheckedInterfacedObjectError = class(EBaseException);
Хотя, специальных классов у нас пока что никаких и нет — только стандартные.
Ладно, в любом случае нам осталось добавить код, строящий коды для произвольных классов исключений. Делается это так:
const // GUID, обозначающий "нас" GUID_DefaultErrorSource: TGUID = '{EFA9AA52-4A4E-4007-85D8-5F46CB65C426}'; type TCheckedInterfacedObject = class(TInterfacedObject, IInterface) private ... protected ... public ... function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; override; end; ... function TCheckedInterfacedObject.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; var E: TObject; CreateError: ICreateErrorInfo; ErrorInfo: IErrorInfo; Source: WideString; begin // Значение по умолчанию Result := E_UNEXPECTED; // Получаем информацию E := ExceptObject; if Succeeded(CreateErrorInfo(CreateError)) then begin // Источник - ProgID приложения или класса, вызвавшего ошибку Source := 'pluginsystem.' + ClassName; CreateError.SetSource(PWideChar(Source)); // Дополнительные данные для исключений if E is Exception then begin // Сообщение CreateError.SetDescription(PWideChar(WideString(Exception(E).Message))); // ИД темы в справке CreateError.SetHelpContext(Exception(E).HelpContext); // Путь к справке никак не задаётся, но при желании вы можете сделать: // CreateError.SetHelpFile(PWideChar(WideString(Application.HelpFile))); // Код ошибки Result := HResultFromException(Exception(E)); end; // Для наших ошибок - указываем GUID источника, для всех прочих - пустой if HResultFacility(Result) = FACILITY_ITF then CreateError.SetGUID(GUID_DefaultErrorSource) else CreateError.SetGUID(GUID_NULL); // Устанавливаем настроенные дополнительные данные if CreateError.QueryInterface(IErrorInfo, ErrorInfo) = S_OK then SetErrorInfo(0, ErrorInfo); end; end;
Поскольку мы не используем COM, то ProgID и GUID не имеют большого смысла — тем не менее, мы устанавливаем в них значения «похожие на правду». Быть может, они пригодятся вызывающей стороне.
Тогда, если у нас в плагине возбуждается какое-то исключение, то:
// Плагин: raise ECheckedInterfacedObjectDeleteError.Create('Error Message');
// Ядро: try Plugin[X].DoSomething; except on E: Exception do begin if E is EOleException then Application.MessageBox(PChar(Format( 'Класс: %s' + sLineBreak + 'Сообщение: %s' + sLineBreak + 'Код: %s' + sLineBreak + 'Источник (GUID): %s' + sLineBreak + 'Источник (ProgID): %s' + sLineBreak + 'Файл справки: %s' + sLineBreak + 'Номер темы: %d', [E.ClassName, E.Message, IntToHex(EOleException(E).ErrorCode, 8), 'не сохраняется в EOleException', EOleException(E).Source, EOleException(E).HelpFile, EOleException(E).HelpContext])), 'Исключение', MB_OK or MB_ICONERROR) else raise; end; end;
Исключение с custom-кодом, как оно видимо вызывающему |
Готово. Теперь вызывающий сможет идентифицировать возникшую ошибку по коду исключения, который уникален для каждого класса исключения в нашей программе.
Чтобы облегчить работу для программистов на других языках, вы можете выписать в заголовочники коды исключений для основных классов. Для этого, во-первых, создайте такую вспомогательную программу:
procedure TForm1.Button1Click(Sender: TObject); begin Label1.Caption := IntToHex(MakeResult(SEVERITY_ERROR, FACILITY_ITF, CalcCRC16(Edit1.Text)), 8); Clipboard.AsText := Label1.Caption; end;
Запустите её и вводите в Edit имена классов исключений. А в буфере обмена (и в метке) вы получите их код. Останется только записать это в заголовочник, например:
const E_CheckedInterfacedObjectError = HRESULT($80044383); E_CheckedInterfacedObjectDeleteError = HRESULT($80045A95); E_CheckedInterfacedObjectDoubleFreeError = HRESULT($80048672); E_CheckedInterfacedObjectUseDeletedError = HRESULT($8004D50D);
Заметьте, что классы исключений и TCheckedInterfacedObject
— это вещи, специфичные именно для Delphi. Ими могут воспользоваться программисты на Delphi. Все прочие же их использовать, понятно, не смогут. Но благодаря тому, что мы использовали языко-независимый подход — они смогут работать в рамках нашей системы. В частности, вместо классов исключений и вспомогательной обёртки TCheckedInterfacedObject
программисты на других языках будут использовать коды ошибок в стиле COM (IErrorInfo
/GetErrorInfo
/SetErrorInfo
/HRESULT
) — вот и всё. Просто мы сделали удобнее жизнь программистов Delphi.
Примечание: по этой причине в заголовочниках на самом деле вовсе не нужны коды для EChecked-исключений. Это — наши, внутренние исключения, они не имеют смысла для вызывающего. Я показал их только для примера. У нас, пока, нет никаких своих классов исключений, которые имели бы специальный смысл для вызывающего. Вот когда в будущем такие исключения появятся — вот тогда в заголовочники нужно будет вписать коды для них — на манер того, как я это только что показал.
Использование оригинальных классов исключений
Теперь, когда у нас есть уникальные коды ошибок, сделать передачу классов исключений становится очень просто:
type OleSysErorClass = class of EOleSysError; OleExceptionClass = class of EOleException; // Наш обработчик safecall-ошибок procedure CustomSafeCallError(ErrorCode: HResult; ErrorAddr: Pointer); // Строит исключение по коду и дополнительной информации function CreateExceptionFromCode(ACode: HRESULT): Exception; var ExceptionClass: ExceptClass; ErrorInfo: IErrorInfo; Source, Description, HelpFile: WideString; HelpContext: Longint; begin // Определяем класс: // а). системные ошибки if HResultFacility(ACode) = FACILITY_WIN32 then ExceptionClass := EOSError else // б). наши ошибки case HRESULT(ErrorCode) of E_CheckedInterfacedObjectError: ExceptionClass := ECheckedInterfacedObjectError; E_CheckedInterfacedObjectDeleteError: ExceptionClass := ECheckedInterfacedObjectDeleteError; E_CheckedInterfacedObjectDoubleFreeError: ExceptionClass := ECheckedInterfacedObjectDoubleFreeError; E_CheckedInterfacedObjectUseDeletedError: ExceptionClass := ECheckedInterfacedObjectUseDeletedError; else // в). все прочие - общий класс ExceptionClass := EOleException; end; // Получаем дополнительную информацию if GetErrorInfo(0, ErrorInfo) = S_OK then begin ErrorInfo.GetSource(Source); ErrorInfo.GetDescription(Description); ErrorInfo.GetHelpFile(HelpFile); ErrorInfo.GetHelpContext(HelpContext); end else begin Source := ''; Description := ''; HelpFile := ''; HelpContext := 0; end; // Создаём объект исключения с информацией if ExceptionClass.InheritsFrom(EOleException) then Result := OleExceptionClass(ExceptionClass).Create(Description, ACode, Source, HelpFile, HelpContext) else if ExceptionClass.InheritsFrom(EOleSysError) then Result := OleSysErorClass(ExceptionClass).Create(Description, ACode, HelpContext) else begin Result := ExceptionClass.Create(Description); if Result is EOSError then EOSError(Result).ErrorCode := HResultCode(ACode); end; end; var E: Exception; begin E := CreateExceptionFromCode(HRESULT(ErrorCode)); raise E at ErrorAddr; end; // Установка и снятие обработчика initialization SafeCallErrorProc := CustomSafeCallError; finalization SafeCallErrorProc := nil; end.
И снова: я использовал EChecked-исключения только ради примера. Вам нужно будет помещать в этот список коды и классы исключений, участвующих в вашей системе.
Теперь, предыдущий пример даст нам:
// Плагин: raise ECheckedInterfacedObjectDeleteError.Create('Error Message');
// Ядро: try Plugin[X].DoSomething; except on E: Exception do begin if E is EOleException then Application.MessageBox(PChar(Format( 'Класс: %s' + sLineBreak + 'Сообщение: %s' + sLineBreak + 'Код: %s' + sLineBreak + 'Источник (GUID): %s' + sLineBreak + 'Источник (ProgID): %s' + sLineBreak + 'Файл справки: %s' + sLineBreak + 'Номер темы: %d', [E.ClassName, E.Message, IntToHex(EOleException(E).ErrorCode, 8), 'не сохраняется в EOleException', EOleException(E).Source, EOleException(E).HelpFile, EOleException(E).HelpContext])), 'Исключение', MB_OK or MB_ICONERROR) else if E is EOleSysError then Application.MessageBox(PChar(Format( 'Класс: %s' + sLineBreak + 'Сообщение: %s' + sLineBreak + 'Код: %s', [E.ClassName, E.Message, IntToHex(EOleSysError(E).ErrorCode, 8)])), 'Исключение', MB_OK or MB_ICONERROR) else raise; end; end;
Исключение с custom-классом, как оно видно вызывающему |
Во, мы сумели передать исходное исключение через границу модуля без изменений! Сообщение — сохранилось, код — сохранился, класс — сохранился. Класс!
Конечно же, набор классов, которые передаются «прозрачно» жёстко зафиксировано в нашем обработчике — это классы из case
. Но это не является проблемой. Когда вы пишете программу и хотите обрабатывать какой-то класс — вы всегда можете добавить его в case. А если почему-то вам лень это делать — вы всегда сможете обработать исключение как EOleException
.
Наследование
Ну, раз уж мы передаём класс исключения, то с наследованием проблемы не возникает — класс эквивалентен источнику, так что вы можете использовать обычные проверки наследования.
Дополнительная информация
Возможно, вам захочется передавать с исключением дополнительную информацию, не подходящую для сообщения об ошибке.
К сожалению, в стандартном механизме нет поля, предназначенного для дополнительной информации. У вас есть три варианта:
- Не передавать дополнительную информацию
- Реализовать свой аналог
SetErrorInfo
/GetErrorInfo
- Упаковывать информацию в «свободное» поле —
Source
У меня нет готового совета, как тут лучше поступать. Лично я пока что следовал первому пункту — просто не было необходимости в передаче дополнительных данных. Замечу только, что третий пункт возникает потому, что мы не используем COM, так что это поле свободно для наших целей.
Идентификация точки возбуждения
Проблема этого пункта связана с тем, что в процессе «путешествия» от точки возбуждения к обработчику (от вызываемого к вызывающему) исключение будет удалено и пересоздано. Т.е. фактически, в программе будет два исключения вместо одного — даже хотя для нашего кода это скрыто под капотом языка. В связи с этим и возникает проблема: в обработчике мы увидим лишь второе исключение, которое возбуждается в нашем обработчике safecall-ошибок, но не исходное. Это затрудняет поиск причины ошибок. Нет, если вы запустили программу под отладчиком — проблемы нет: вы увидите оба исключения (отладчик покажет стандартное уведомление о возникновении исключения). Тогда вы просто остановитесь ещё на первом исключении и сможете исследовать проблему (в источнике).
Но что если вам почему-то не удастся воспользоваться отладкой исходного исключения?
Ответ заключается в том, что вы должны использовать в своей программе трейсер исключений и записывать информацию по исключению в лог-файл или передавать вместе с исключением как дополнительную информацию. Для Delphi на сегодня есть такие варианты: JCL, EurekaLog, madExcept.
Мне кажется, что оптимальным решением будет захватывать стек исключения и сохранять его в дополнительную информацию для исключения. Например:
function TCheckedInterfacedObject.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; var ... Source: WideString; begin ... Source := { текстовое представление стека для исключения ExceptObject }; ... end;
Тогда эта информация будет доступна вызывающему. Он может показывать её в сообщении об ошибке, например:
type TForm1 = class(TForm) ... private procedure CustomExceptionHandler(Sender: TObject; E: Exception); end; ... procedure TForm1.FormCreate(Sender: TObject); ... begin ... Application.OnException := CustomExceptionHandler; ... end; procedure TForm1.CustomExceptionHandler(Sender: TObject; E: Exception); var Msg: String; begin if E is EOleException then Msg := E.Message + sLineBreak + EOleException(E).Source else Msg := E.Message; // тут можно сохранить исключение в лог-файл (баг-репорт), если надо Application.MessageBox(PChar(Msg), 'Ошибка', MB_OK or MB_ICONERROR); end;
Аппаратные исключения
Ещё стоит упомянуть об особом случае для safecall-исключений. Дело в том, что обработку через SafeCallException
получают только программные исключения. Аппаратные исключения всегда возвращают E_UNEXPECTED
. Мне не известно, намеренное ли это решение или баг в реализации. С одной стороны, такое поведение не указано в документации. С другой стороны, оно логично: аппаратная ошибка по определению не имеет смысла для вызывающего. Для него нет разницы между ними, он не будет делать специальную обработку таких ошибок.
Т.е., к примеру, в вашей системе может быть предусмотрено исключение EUnableToSetFocus
(E_UnableToSetFocus = $80049330
), которое возбуждается, если плагин хочет перевести фокус на редактор нашей программы, но это делать нельзя. Плагин вполне может сделать специальную проверку на этот класс исключения: если исключение = EUnableToSetFocus
/E_UnableToSetFocus
, то ничего не делать. Ну или сделать что-то ещё. Вот, это — специальный класс, который имеет особый смысл. Он явно отличен от каких-нибудь EInvalidInsert
, EMonitor
и EProgrammerNotFound
Но аппаратное исключение не может быть таким специальным исключением. Оно всегда будет обрабатываться единообразно, равно как и все прочие исключения.
Конечно, вы можете обойти эту проблему, просто развернув safecall-метод в обычный stdcall и сделав всю обработку руками. Но зачем это делать? Вы лишаетесь удобства ради случаев, которые всё равно потребуют запуска отладчика, либо использования трейсера.
Итого: если в вашей программе вы видите ошибку «Разрушительный сбой» (напомню, это текст ошибки от E_UNEXPECTED
), то знайте, что в ней произошло либо аппаратное, либо неизвестное исключение (чаще всего — исходным исключением будет банальный Access Violation). И чтобы его исправить, нам нужно запустить отладчик
А если вам хочется иметь больше информации, чем просто «Разрушительный сбой» — просто используйте трейсер исключений.
Заключение
Итак, в этой статье мы подробно разжевали борьбу с ошибками в нашей системе плагинов. Надеюсь, что теперь устранение проблем будет для вас вполне посильной задачей.
А раз так, то самое время приступить к дальнейшему усложнению системы…
Скачать пример к этому моменту можно тут. В примеры был добавлен весь наш служебный отладочный код, так что теперь наша система умеет грамотно сообщать об ошибках. Поскольку само поведение системы не изменилось (мы просто добавили обработку ошибок), то я добавил новый плагин, который добавляет в меню программы три новых пункта. Каждый пункт при вызове просто выбрасывает разнотипные исключения. Вы можете использовать этот плагин для исследования работы механизма обработки ошибок. Я также заместил умалчиваемый обработчик исключений в программе, чтобы дополнительно показывать класс исключения.
P.S. Чёрт, мне очень хотелось бы поговорить о правильных подходах к обработке исключений в своих программах — потому что свои классы исключений появляются в основном именно при правильной разработке на исключениях. Но я не могу тут это здесь сделать, я уже и так немерено текста написал. Поэтому я ограничусь тем, что ткну в направлении этой статьи.
К примеру, мы можем захотеть дать плагину возможность отказаться грузится. Например, если плагин предназначен для работы с железкой, которая отсутствует. В принципе, плагин может просто возбудить исключение в методе инициацизации. Но это приведёт к показу сообщения об ошибке загрузки плагина и, что более важно, к его отключению из списка автозагружаемых (если такая функциональность предусмотрена в ядре). Вот поэтому нам нужно ввести специальный класс исключения:
type EPluginLoadAbort = class(EBaseException); const E_PluginLoadAbort = HRESULT($80041881);
Плагин может возбудить это исключение в функции инициализации, а ядро может сделать для неё особую обработку: просто отменить загрузку плагина, вернув nil в качестве результата (IPlugin
), не показывая сообщения об ошибке и не отключая плагин.
Вот, это был пример реального класса исключения, который может иметь специальный смысл и который нужно включать в заголовочники.
С другой стороны, было бы также весьма полезно включить коды для, скажем, EAssertionFailed
и EStreamError
. По этой причине я включил в примеры несколько стандартных классов — исключительно для удобства (если в вашей версии Delphi нет каких-то классов — просто удалите те строки, которые генерируют ошибку «Неизвестный идентификатор»).
Читать далее.
Разрабатывая какое-нибудь приложение, вы должны написать код. который будет решать поставленную задачу, а также код, который будет выполнять проверку на наличие ошибок. Как правило, код для обработки ошибок строится на основе оператора if.
Оператор if часто используется для проверки данных, вводимых пользователем, а также результатов выполнения функций. В простых алгоритмах можно ограничиться применением оператора if, однако в приложениях с графическим интерфейсом пользователя, где пользователи имеют полную свободу действий, ошибки могут возникать когда угодно и где угодно. Использование одного только оператора if для защиты приложения — не самая лучшая идея.
С задачей перехвата ошибок и реагирования на них лучше всего справляется механизм обработки исключений. Если в приложении, написанном с помощью Delphi, возникает ошибка, то приложение автоматически генерирует исключение. Исключением представляет собой объект, который описывает возникающую ошибку.
Генерация исключения означает всего лишь то. что приложение создало объект исключения и максимально подробно описало ошибку.
Если мы не обрабатываем исключение (то есть не приготовлен специальный код для перехвата исключения), приложение само сделает это автоматически. Обычно приложение обрабатывает исключение, выводя на экран монитора окно с сообщением о возникшей ошибке. Например, если вы передадите функции StrToInt строку, содержащую символы, которые не могут быть преобразованы в числовое значение, или вообще пустую строку, то функция сгенерирует исключение (рис. 13.1).
Рис. 13.1. Исключение, обработанное приложением
Чтобы обработать исключение, сгенерированное функцией StrToInt, мы должны поместить вызов функции StrToInt в защищенный блок кода. Защищенным является блок кода, который может реагировать на некоторое исключение. В Delphiзащищенный блок выглядит следующим образом:
Операторы, которые могут сгенерировать исключение, записываются в блоке try, а в обработчике исключений пишется код, который занимается обработкой исключений. Обработчик исключения является частью защищенного блока, начинающегося с зарезервированного слова except в Delphi.
Если вы передадите функции StrToInt допустимую строку, и при этом исключение не возникнет, будет выполнен только тот код, который находится в блоке try. Код в блоке исключения выполняется только в том случае, если оператор, находящийся Внутри этого блока, сгенерирует исключение.
В следующих двух примерах показано, как осуществляется вызов функции StrToInt и перехват исключения, которое может быть сгенерировано этой функцией (рис. 13.2). В листинге 13.1А показан пример перехвата исключений в приложениях, написанных с помощью Delphi.
Листинг 13.1А. Перехват исключения в Delphi
Теперь давайте попытаемся создать простой калькулятор, с помощью которого можно будет делить числа. Интерфейс пользователя этой небольшой программы показан на рис. 13.3.
Чтобы разделить значения, введенные в компонентах TEdit, мы должны написать код, который сначала преобразует их в целые числа, а затем разделит одно на другое. Этот код может легко сгенерировать два исключения.
Одно из них, EConvertError, может быть сгенерировано в том случае, если значение одного из компонентов TEdit невозможно преобразовать к целому типу, а другое. EDivByZero, может быть сгенерировано тогда, когда предпринимается попытка разделить первое число на 0.
Листинг 13.2. Деление двух чисел
Несмотря на то что вы можете написать обработчики для перехвата всех исключений, вы должны постараться обрабатывать только специфические исключения. Обработать специфическое исключение можно с помощью зарезервированного слова on. с которым связан следующий синтаксис:
on
Некоторое_Исключение
do
Обработка_Исключения;
Конструкцию on-do можно использовать только в рамках обработчика исключений:
По мере возможности, для обработки различных исключений лучше использовать конструкцию on-do. Например, вы можете обработать исключение EConvertError, выводя сообщение об ошибке, а исключение EDivByZero — уведомляя пользователя о том. что второе число не может быть равно нулю, и автоматически заменяя его единицей. В листинге 13.ЗА показан пример обработки специфических исключений в Delphi.
Листинг 13.3А. Обработка специфических исключений
Если конструкцию on-do использовать для обработки специфических исключений, вы должны также написать код для обработки ошибок, о которых вам ничего не будет известно. Чтобы обработать исключения, которые вам не удастся обработать специфическим образом, можно добавить к обработчику исключения часть else.
В ответ на ошибку, возникшую во время работы приложения, создается экземпляр объекта исключения. Когда это исключение будет обработано, его объект будет автоматически освобожден. Если вы не хотите обрабатывать специфическое исключение, или же не знаете, как это сделать, вы должны разрешить Delphi самостоятельно разобраться с ним. Для этого вы должны повторно сгенерировать исключение, то есть повторно создать экземпляр объекта исключения. Для этой цели в Delphiиспользуется зарезервированное слово raise.
Например, следующий обработчик исключения обрабатывает только исключение EConvertError. Как только возникнет какое-то другое исключение, обработчик исключения сгенерирует его повторно. В итоге исключение останется «в силе» после завершения работы обработчика, и будет передано на обработку уже другому обработчику, который обычно используется по умолчанию. В листинге 13.4А показан пример повторного вызова исключения в Delphi.
Листинг 13.4А. Повторная генерация исключения в Delphi
Итак, если будет сгенерировано исключение EConvertError. то обработчик справится с ним самостоятельно, а если возникнет любое другое исключение, скажем. EDivByZero или EAccessViolation. то обработчик сгенерирует его повторно и направит его другому обработчику (рис. 13.4).
Зарезервированное слово raise используется также и для генерации исключения. Чтобы сгенерировать исключение в Delphi, используйте зарезервированное слово raise, указывая вслед за ним экземпляр объекта исключения. Экземпляром объекта исключения обычно является вызов конструктора исключения.
Синтаксис генерации исключения обычно выглядит следующим образом:
Вы можете, например, создать специальный вариант функции StrToInt, которая будет генерировать исключение EConvertError с помощью специальных сообщений об ошибке, если строку нельзя будет преобразовать в целое число. В листинге 13.5А представлена версия этой функции в Delphi.
Листинг 13.5А. Генерация исключений в Delphi
Конструкция оп-с!о позволяет получать на время объект исключения с помощью следующего синтаксиса:
В качестве идентификатора обычно применяется заглавная буква Е. Когда вы получаете объект исключения, вы можете использовать его подобно любому другому объекту и даже обращаться к его свойствам и методам. Единственное, что не рекомендуется делать, это уничтожать объект исключения, поскольку объекты исключения автоматически управляются обработчиком исключения. На рис. 13.6 показан результат использования объекта исключения.
В качестве идентификатора обычно применяется заглавная буква Е. Когда вы получаете объект исключения, вы можете использовать его подобно любому другому объекту и даже обращаться к его свойствам и методам. Единственное, что не рекомендуется делать, это уничтожать объект исключения, поскольку объекты исключения автоматически управляются обработчиком исключения. На рис. 13.6 показан результат использования объекта исключения.
Листинг 13.6. Использование объекта исключения
Создать специальное исключение несложно, и этот процесс ничем не отличается от создания специального класса. Специальные исключения должны порождаться от класса Exception или другого потомка этого класса. Имена классов исключений должны начинаться с заглавной буквы Е.
В листинге 13.7А показана генерация и перехват специального исключения в Delphi. На рис. 13.7 можно видеть результат работы со специальным исключением.
Листинг 13.7А. Работа со специальным исключением
Зарезервированное слово try позволяет построить два различных блока: блок обработчика исключений и блок защиты ресурсов. Блок обработчика исключений создается с помощью зарезервированного слова except, а блок защиты ресурсов— с помощью зарезервированного слова finally. Синтаксическая структура блока защиты ресурсов в Delphi выглядит следующим образом:
Блоки обработки исключений и защиты ресурсов используются по-разному и работают тоже по-разному. Операторы обработчика исключений выполняются только в том случае, если операторы в блоке try сгенерировали исключение, а операторы в блоке finally выполняются всегда, даже если операторы в блоке try не сгенерировали никакого исключения. Если в блоке try возникнет исключение, управление будет передано блоку finally, после чего будет выполнен код очистки. Если в блоке try исключения не возникнут, операторы в блоке finally будут выполняться после операторов в блоке try.
Подходящим способом использования блока защиты ресурсов является распределение или, с другой стороны, затребование ресурса перед блоком try. После того как вы затребуете ресурс, поместите операторы, использующие ресурс, внутрь блока try. Когда работа с ресурсом будет завершена, вы должны будете освободить его. Операторы, освобождающие ресурс, должны быть написаны в блоке finally.
Блок защиты ресурса часто используется для того, чтобы обеспечить надлежащее освобождение динамически созданных объектов. Например, динамическое создание модальной формы необходимо всегда защищать с помощью блока try-finally (см. листинг 13.8).
Листинг 13.8. Динамическое создание формы с защитой ресурса, версий Delphi
В листинге 13.9 представлен более короткий способ динамического создания формы, защищенной блоком try-finally:
Другое отличие между блоками обработки исключений и блоками обработки ресурсов заключается в том, что блок обработки ресурсов не обрабатывает исключения. Таким образом, если исключение возникнет, оно будет передано первому доступному обработчику исключений. Например, если вы выполните следующий код. то исключение EDivByZero приведет к тому, что обработчик исключений, используемый по умолчанию, выведет на экран монитора сообщение об ошибке, информирующее пользователя о возникшем исключении.
Если вы хотите обработать исключение EDivByZero (или любое другое исключение) внутри блока защиты ресурсов, вы должны написать вложенный блок обработчика исключений. О вложенных блоках читайте в этой статье: Вложенные блоки