Как исправлять ошибки в delphi

При компиляции Delphi программ могут возникать различные виды ошибок. Некоторые из них включают:

  1. Синтаксические ошибки: возникают, когда в коде программы есть синтаксические ошибки, такие как неправильное использование ключевых слов, неверное написание идентификаторов, отсутствие точек с запятой в конце строк и т.д.
  2. Ошибки типа: возникают, когда в программе используется неправильный тип данных или когда типы данных не совместимы друг с другом.
  3. Ошибки логики: возникают, когда в программе есть ошибки в логике, например, когда программист допустил ошибку при расчете значения или при написании условия.
  4. Ошибки времени выполнения: возникают во время выполнения программы, когда происходят ошибки, связанные с работой с памятью, вводом-выводом или другими системными функциями.
  5. Ошибки линковки: возникают, когда компилятор не может найти необходимые библиотеки или объектные файлы, необходимые для построения исполняемого файла.
  6. Ошибки взаимодействия с ОС: возникают, когда программа пытается выполнить операции, которые не разрешены операционной системой, например, когда программа пытается получить доступ к защищенной области памяти.
  7. Ошибки ввода-вывода: возникают, когда программа не может прочитать или записать данные из файла или с других устройств ввода-вывода.

Это не полный список ошибок, которые могут возникать при компиляции Delphi программ, но эти ошибки являются наиболее распространенными.

Как исправить ошибки при компиляции Delphi программ?

Исправление ошибок при компиляции Delphi программ зависит от типа ошибки. Вот несколько общих советов, которые могут помочь в исправлении ошибок:

  1. Внимательно прочтите сообщение об ошибке: сообщение об ошибке обычно содержит информацию о том, какая ошибка возникла и в какой части кода. Внимательное прочтение сообщения об ошибке может помочь быстро определить причину ошибки.
  2. Используйте справочник Delphi: справочник Delphi содержит информацию о ключевых словах, функциях и типах данных, которые могут быть полезны при исправлении ошибок.
  3. Используйте отладчик: отладчик может помочь определить место, где возникает ошибка, и отследить значения переменных и состояние программы в этом месте.
  4. Проверьте правильность написания кода: ошибки могут возникать из-за неправильного написания кода, так что проверьте свой код на наличие синтаксических ошибок и ошибок типа.
  5. Проверьте зависимости: если ошибка связана с линковкой, убедитесь, что все необходимые библиотеки и объектные файлы доступны и правильно подключены.
  6. Проверьте правильность использования API: если ошибка связана с взаимодействием с операционной системой, убедитесь, что вы используете API правильно и не пытаетесь выполнить операции, которые не разрешены ОС.
  7. Обратитесь к сообществу: если вы не можете исправить ошибку самостоятельно, вы можете обратиться за помощью к сообществу Delphi, например, на форумах (dropcode.ru, devhubby.com) или в группах социальных сетей.

В целом, исправление ошибок при компиляции Delphi программ зависит от специфики конкретной ошибки, поэтому важно понимать, как работает ваша программа и какие функции и библиотеки вы используете.

Ошибки — неизбежное зло программирования. Видимо пока трудно даже представить средство с помощью которого можно избавится от них. Человеку, которые выдумает это чудодейственное лекарство, благодарные потомки-программисты, несомненно, воздвигнут памятник. Пока же остается лишь заниматься обычным делом: ловлей багов.
«Нарушение Доступа» — фраза, которую пользователи видят, когда приложение делает попытки обратиться к памяти, которая не обозначена для их использования — и как следствие происходит сбой в работе программы:

Access violation at address

in module .
Read of address
Ситуация при которой Windows давала бы полную свободу программам — записывай данные куда хочешь, скорее всего бы привела к разноголосице программ и полной потери управления над компьютером. Но этого не происходит — Windows стоит на страже «границ памяти» и отслеживает недопустимые операции. Если сама она справиться с ними не в силах — происходит запуск утилиты Dr. Watson, которая записывает данные о возникшей ошибки, а сама программа закрывается.
Известно что, при программирование, особенно крупных программных продуктов, уследить за всеми процессами в коде невозможно, да и нет необходимости. Использование сторонних компонентов и библиотек только усложняет дело. Именно поэтому программисты Delphi, порой и сталкиваются со «своенравными» программами, которые то и дело норовят «сбросить пользователя». Итак, давайте рассмотрим некоторые вопросы, связанные с корректной среды программирования, так и непосредственно проблемы написания кода, которые ведут к возникновению ошибок типа «ошибка доступа» (AVS) и очертим наиболее известные пути их исправления.
Мы можем поделить AVS, с которыми сталкиваются при разработке в Delphi на два основных типах: ошибки при выполнения и некорректная разработка проекта, что вызывает ошибки при работе программы.
Ошибки возникают при старте и закрытии Delphi или формировании проекта. Причиной могут являться сбои в «железе» компьютера.
Эти ошибки могут быть вызваны различными источниками, включая систему BIOS, операционную систему или аппаратные подпрограммы драйверов. Некоторые видео-, звуковые или сетевые платы могут фактически вызывать подобного рода ошибки в Delphi. Для решения подобных аппаратных проблем можно предпринять последовательность неких «стандартных» ходов:
проверить, что не имеется никаких конфликтов между установленными устройствами, устранить обнаруженные конфликты;
попробовать слегка уменьшить «аппетита» видеодрайвера — поставить меньшее разрешение;
в случае если у вас двухпроцесорная система обеспечить равное изменение шага для каждого процессора;
И в конце концов просто попытаться заменить драйвера на более свежие.

Но помимо чисто железных проблем — большую головную боль могут вызвать ошибки в работе программного обеспечения. Особенно это касается непосредственно операционной системы. Зачастую Windows терпит крах спонтанно. Вот рекомендации которые помогут вам создать более устойчивую среду программирования:
Хотя Windows 9X популярная система, разработку лучше проводить в Windows NT или Windows 2000 — это более устойчивые операционные системы. Естественно при переходе на них придется отказаться от некоторых благ семейства Windows 95/98/Me — в частности не все программы адоптированы для Windows NT/2000. Зато вы получите более надежную и стабильную систему.
Не забывайте о том, как важно всегда иметь под рукой свежие версии компонентов для Delphi и дополнительных библиотек. В отличие от Windows создатели данных пакетов стараются от версии к версии уменьшать количество ошибок.
Следите за тем, что бы устанавливаемые компоненты были предназначены непосредственно для вашей версии Delphi. Попробуйте деинсталлировать чужеродные компоненты один за другим (или пакет за пакетом) пока проблема не будет устранена.
Контролируйте все программные продукты установленные на вашей машине и деинсталлируйте те из них, которые сбоят. Фаворитами AV среди них являются шароварные утилиты и программы и бета версии программных продуктов.
Все вышеперечисленное в основном не касалось самого процесса программирования и в малой степени зависит от разработчика. Теперь же обратимся к теме, как не допустить при разработки программного продукта ситуации при которой, он сам будет являться причиной ошибки.
Вы могли бы рассмотреть компилирование вашего приложения с директивой {$D}, данная директива компилятора может создавать файлы карты (файлы с расширением map, которые можно найти в том же каталоге, что и файлы проекта), которые могут послужить большой справкой в локализации источника подобных ошибок. Для лучшего «контроля» за своим приложением, компилируйте его с директивой {$D}. Таким образом, вы заставите Delphi генерировать информацию для отладки, которая может послужить подспорьем при выявление возникающих ошибок.
Следующая позиция в Project Options — Linker & Compiler позволяет вам, определить все для последующей отладки. Лучше всего, если помимо самого выполняемого кода будет доступна и отладочная информация — это поможет при поиске ошибок. Отладочная информация увеличивает размер файла и занимает дополнительную память при компилировании программ, но непосредственно на размер или быстродействие выполняемой программы не влияет. Включение опций отладочной информации и файла карты дают детальную информацию только, если вы компилируете программу с директивой {$D+}.
Эта информация состоит из таблицы номеров строк для каждой процедуры, которая отображает адреса объектных кодов в номера строк исходного текста. Директива $D обычно используется совместно с другой директивой — $L, что позволяет или запрещает генерацию информации о локальных символах для отладки.
Таким образом вы без труда сможете найти точный адрес той подпрограммы, которая была ответственна за ошибку. Одна из наиболее общих причин ошибок выполнения — использование объекта, который еще не был создан. Если второй адрес при выдачи ошибки — FFFFFFF (или 0000000) Вы можете почти утверждать, что было обращение к объекту, который еще не был создан. Например, вызов метода формы, которая не была создана.

procedure TfrMain.OnCreate(Sender: TObject);
 var BadForm: TBadForm;
 begin
   BadForm.Refresh; // причина  ошибки
 end;
 

Попытаемся разобратся в этой ситуации. Предположим, что BadForm есть в списке «Available forms » в окне Project Options|Forms. В этом списке находятся формы, которые должны быть созданы и уничтожены вручную. В коде выше происходит вызов метода Refresh формы BadForm, что вызывает нарушение доступа, так как форма еще не была создана, т.е. для объекта формы не было выделено памяти.
Если вы установите «Stop on Delphi Exceptions » в Language Exceptions tab в окне Debugger Options, возможно возникновения сообщение об ошибке, которое покажет, что произошло ошибка типа EACCESSVIOLATION. EACCESSVIOLATION — класс исключение для недопустимых ошибок доступа к памяти. Вы будете видеть это сообщение при разработке вашего приложения, т.е. при работе приложения, которое было запущено из среды Delphi.
Следующее окно сообщения будет видеть пользователь — и программа будет закрыта при совершение недопустимой операции:

 Access violation at address 0043F193
 in module 'Project1.exe'
 Read of address 000000.
 

Первое шестнадцатиричное число (‘0043F193’) — адрес ошибки во время выполнения программы в программе. Выберите, опцию меню ‘Search|Find Error’, введите адрес, в котором произошла ошибка (‘0043F193’) в диалоге и нажмите OK. Теперь Delphi перетранслирует ваш проект и покажет вам, строку исходного текста, где произошла ошибка во время выполнения программы, то есть BadForm.Refresh.
Естественно, что списка наиболее общих причин ошибок, вызывающих аварийное завершение работы программы, написанной в Delphi в чистом виде нет. Есть несколько общих «узких мест» в коде и структуре программы, когда подобная ошибка может произойти. Перечислим наиболее распространенные.

Недопустимый параметр API

Если вы пытаетесь передать недопустимый параметр в процедуру Win API, может произойти ошибка. Необходимо отслеживать все нововведения в API при выходе новых версий операционных систем и их обновлений.

Уничтожение исключения

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

 Zero:=0;
 try
    dummy:= 10 / Zero;
 except on E: EZeroDivide do
    MessageDlg('Can not divide by zero!', mtError, [mbOK], 0);
    E.free. // причина ошибки
 end;
 

Индексация пустой строки
Пустая строка не имеет никаких достоверных данных. Следовательно, попытка индексировать пустую строку — подобно попытке обратиться к нулю, что приведет также к ошибке:

 var s: string;
 begin
    s:='';
    s[1]:='a'; // причина ошибки
 end;
 

Обращение к динамической переменной
Вы должны строить обращение к динамической переменной корректно, иначе вы перемещаете адреса указателей и возможно разрушаете другие выделенные ячейки памяти.

 procedure TForm1.Button1Click(Sender: TObject);
 var
   p1 : pointer;
   p2 : pointer;
 begin
   GetMem(p1, 128);
   GetMem(p2, 128);
  {эта строка может быть причиной ошибки}
   Move(p1, p2, 128);
  {данная строка корректна }
   Move(p1^, p2^, 128);
   FreeMem(p1, 128);
   FreeMem(p2, 128);
 end;
 

Вот и всё, Удачи!

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

См. также Разработка API (контракта) для своей DLL и Разработка системы плагинов, часть 9: подводные камни.

Оглавление

  1. Основы отладки:
    • Запуск отладки плагинов
    • Остановка выполнения:
      • Пауза
      • Исключения
      • Точки останова
    • Инструменты отладчика:
      • Анализ значений переменных
      • Анализ пути выполнения
      • Трассировка
      • События
      • Расширенные точки останова
    • Практический пример
  2. Управлением временем жизни
    • Двойное удаление
    • Утечки памяти
  3. EAccessViolation
    • Неверные сигнатуры
    • Передача данных
    • Многопоточность
  4. Обработка ошибок в плагинах:
    • Safecall
    • Стандартные средства
    • Дополнение стандартных средств
      • Актуальные коды исключений
      • Использование оригинальных классов исключений
      • Наследование
      • Дополнительная информация
      • Идентификация точки возбуждения
      • Аппаратные исключения
  5. Заключение

Основы отладки

Сперва мне хотелось бы дать краткое введение в отладку. Если вы уже знакомы с этим материалом, то можете пропустить его. Для тех же, кому краткого введения окажется мало — см. полную версию. Я привожу здесь эту вводную часть потому, что далее я буду говорить слова вроде «ставим 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 и указать главную программу:

Попытка «запустить» DLL или bpl

Если вы хотите скомпилировать плагин (получить .dll или .bpl файл) — вам вовсе не нужно его для этого запускать. Просто выберите Project/Compile (Ctrl+F9) или Project/Build (Shift+F9).

Если же вы хотите именно отлаживать плагин, то вам нужно указать программу, которая будет грузить этот плагин. Естественно, в случае плагинов это будет программа-ядро. Для этого вам нужно открыть меню Run/Parameters и указать главную программу:

Указание программы-сервера (host)

Остановка выполнения программы

Пока программа работает, вы не много можете с ней сделать. Для того чтобы воспользоваться отладчиком, вам нужно приостановить её выполнение. У вас на выбор есть три варианта, первый — нажать на кнопку паузы («Run»/»Program pause»), второй — возбудить в программе исключение (или же оно возникнет в программе само — например, EAccessViolation), третий — расставить в нужных местах точки останова (breakpoint-ы, брейкпойнты или просто «бряки»).

Пауза

Способ первый не позволяет достичь точности. Вы останавливаете программу в тот момент, когда вы нажимаете на кнопку. Это может быть за миллион строк кода до или после того места, где вы в действительности хотели бы быть. Поэтому этот способ используется, когда вам не важно точное место останова. Пример — зависшая программа. Вы просто останавливаете её выполнение в любой момент, чтобы посмотреть, что же там произошло, что программа зависла. Ещё вариант — вам нужна программа на паузе, чтобы проанализировать, скажем, глобальные переменные. Вам не важно место останова, потому что вас интересуют значения переменных, которые, будучи раз заданными, не меняются во время работы программы.

Исключения

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

Уведомление отладчика в старых версиях 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-отладчика. Более подробно об этом я скажу чуть ниже.

Примечание: кроме уведомления об исключениях, отладчик может показывать и иные сообщения. Их не следует путать с уведомлениями об исключениях.

Точки останова

Третий способ является основным. Заключается он в том, что по тексту программы вы мышкой отмечаете места, где вы хотели бы остановиться во время работы программы. Слева от кода вы видите полоску, в которой появляются синие точки:

Уведомление с дополнительными опциями

Она показывается только в том случае, если место возникновения исключения не соответствует строчке исходного кода. Если галочка будет установлена, то после нажатия на «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 StudioProjectsPluginsDemorichedit.exe

      можно обратиться через два имени:

      C:UsersАлександрDocumentsRAD StudioProjectsPluginsDemorichedit.exe

      и
      D:RAD StudioProjectsPluginsDemorichedit.exe

      В этом случае будет предпочтительнее открывать проект

      D:RAD StudioProjectsPluginsDemorichedit.dpr

      вместо

      C:UsersАлександрDocumentsRAD StudioProjectsPluginsDemorichedit.dpr

      и запускать

      D:RAD StudioProjectsPluginsDemorichedit.exe

      вместо

      C:UsersАлександрDocumentsRAD StudioProjectsPluginsDemorichedit.exe
    • Убедитесь, что в пути к проекту есть только латинские символы (ASCII). Например, плохо: C:UsersАлександр.... Нормально: C:UsersAlexandr...
    • Не изменяйте дату-время .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-ы вы можете ставить, как во время проектирования, так и во время работы или приостановки программы. Теперь, после запуска программы, как только выполнение дойдёт до одной из заданных вами точек останова, отладчик немедленно остановит программу.

Инструменты отладчика

Итак, после остановки программы в отладчике (любым из трёх описанных выше способов) вы можете использовать его возможности для анализа программы. Большинство вещей, о которых мы будем сейчас говорить, доступны именно в режиме отладки (например, в контекстное меню редактора кода в режиме отладки добавляются новые команды). Вы можете определить, в каком режиме находится среда, взглянув на заголовок окна:

Установленная точка останова

В этом случае мы захотели остановиться перед выполнением строки с присваиванием свойства Caption. Заметим, что breakpoint-ы вы можете ставить, как во время проектирования, так и во время работы или приостановки программы. Теперь, после запуска программы, как только выполнение дойдёт до одной из заданных вами точек останова, отладчик немедленно остановит программу.

Инструменты отладчика

Итак, после остановки программы в отладчике (любым из трёх описанных выше способов) вы можете использовать его возможности для анализа программы. Большинство вещей, о которых мы будем сейчас говорить, доступны именно в режиме отладки (например, в контекстное меню редактора кода в режиме отладки добавляются новые команды). Вы можете определить, в каком режиме находится среда, взглянув на заголовок окна:

Режим проектирования (design-time) — нет подписи
Режим прогона (run-time), программа работает — добавлено «Running» 
Режим прогона (run-time), программа работает — добавлено «Running» 
Режим прогона (run-time), программа приостановлена — добавлено «Stopped»

Для примера возьмите любую свою программу, поставьте breakpoint на первое действие при нажатии какой-нибудь кнопки, запустите программу и щёлкните по кнопке (мы сейчас будем обсуждать возможности отладчика, а вы сможете щупать их прямо на своей программе). Если вы используете новые Delphi, то заметите, как преображается при этом среда — исчезает инспектор объектов, палитра компонентов и т.п. Зато появляется множество окон: «Call Stack», «Watch List», «Local Variables» и т.п. Каждое из этих окон предоставляет вам какую-то возможность отладчика:

Среда во время отладки

Если какого-то окна на экране нет, вы можете показать его, используя меню «View»/»Debug windows»:

Среда во время отладки

Если какого-то окна на экране нет, вы можете показать его, используя меню «View»/»Debug windows»:

Инструменты отладчика

Если вы не видите на экране какое-то окно, о котором идёт речь, — просто выберите его из этого меню.

Попробуйте сейчас, пока программа стоит на паузе, переключиться на программу. Вы щёлкаете по кнопке в панели задач, но ничего не происходит. Попробуйте свернуть все окна, вы увидите примерно такую картину:

«Зависшая» программа под отладкой

Вы можете видеть, что наша программа как бы висит (последним сворачивалось окно Delphi, поэтому рисунок окна Delphi отпечатался на окне нашей программы). Она не прорисовывается, она не реагирует на ваши действия, другим словом — висит. Да, но не забывайте, что мы только что поставили с вами программу на паузу! Это значит, что она не работает. А если программа не работает, то она и не может ни перерисовываться, ни реагировать на ваши щелчки мышью. Так что ничего страшного в таком поведении нет — так и должно быть. Как только вы возобновите работу программы (снимите её с паузы), она снова будет вести себя как полагается.

Анализ значений переменных

Итак, продолжаем. Наша программа стоит на паузе. В окне «Local Variables» показываются локальные переменные в текущем месте. Как только мы остановились, отладчик показывает нам чему равны локальные переменные в текущей функции (т.е. функции, в которой мы остановились). Если вы хотите посмотреть значения локальных переменных для других функций — просто дважды щёлкните по нужной функции в окне стека вызова (Call Stack).

Для некоторых переменных отладчик может сказать нам, что он не может получить значение переменной («Variable ‘XYZ‘ inaccessible here due to optimization»). Это работа оптимизатора (кстати, вы можете отключить его, сбросив в опциях проекта галочку «Optimization»). Он выбрасывает переменную, как только в ней отпадёт необходимость.

Итак, «Local Variables» — удобное окно для просмотра локальных переменных. Что делать, если хочется посмотреть не локальную переменную? Можно воспользоваться окном «Watches». Для этого щёлкните правой кнопкой по свободной области окна «Watch List» и выберите «Add watch» — появится окно ввода параметров наблюдения:

«Зависшая» программа под отладкой

Вы можете видеть, что наша программа как бы висит (последним сворачивалось окно 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). После этого на экране появляется такое окно:

Наблюдение за несколькими переменными

Последние два выражения с 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. Вы увидите, как стрелочка переместится к следующей строке:

Окно «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», то наш стек вызовов выглядел бы так:

Отладчик после выполнения второй строки

Вы видите, что программа снова стоит на паузе. Мы только что выполнили строчку с 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 при запуске процесса

Это вид окна при запуске процесса. А вот его вид после некоторой работы:

Окно Events в процессе работы программы под отладкой

В окно «Event Log» попадает различная информация по ходу работы программы: во-первых, это уведомления о загрузке/выгрузке модулей (голубой цвет), запуске и остановке потоков и процесса (тёмно-красный и серый цвет). Во-вторых, в него помещается вывод функции OutputDebugString (синий цвет). Для создания такой строчки, как на скриншоте, в программе была строка «OutputDebugString(‘Отладочный вывод от OutputDebugString.’);». В-третьих, это различные сообщения, связанные с точками останова (светло-красный цвет), а также сообщения от точек станова (красный цвет) и стек вызовов от них же (оранжевый цвет). Чуть позже мы обсудим точки останова более подробно. Кроме того, в это окно можно добавлять строчки и вручную — выберите пункт «Add Comment…» из контекстного меню (чёрный цвет). Также сюда добавляются уведомления об исключениях, и ещё можно включить логгинг сообщений Windows.

По-умолчанию, лог очищается при каждом запуске процесса. Вы также можете сохранить его в файл для анализа или очистить руками в середине работы — для этого воспользуйтесь соответствующими командами из контекстного меню. Кроме того, в опциях отладчика есть настройка окна «Event Log» (которая также доступна из контекстного меню окна «Event Log»).

В частности, помимо настройки поведения и внешнего вида, здесь можно включить/отключить логгинг определённых типов событий. Если интересующее вас событие происходит редко и/или тонет в общей массе событий, можно просто выключить все другие типы событий. Именно это является причиной, почему по-умолчанию отключен логгинг сообщений Windows — их всегда бывает очень много. Кроме того, вероятно, вы захотите отключить опцию «Display process info with events» — она показывает дополнительную информацию о процессе, вызвавшем событие. Поскольку чаще всего вы будете отлаживать только один процесс, эта информация не несёт полезной нагрузки и только создаёт шум в логе. В случае отладки двух процессов эта опция позволит отличать события от разных процессов.

Расширенные точки останова

В самом начале этого пункта мы буквально краем коснулись точек останова с целью быстрее познакомить вас с возможностями отладчика, т.к. они (возможности) доступны только в режиме остановки программы, а точки останова являются основным средством для установки программы на паузу. Теперь мы рассмотрим их более подробно. И для этого сначала взглянем на окно «Breakpoints»:

Список точек останова в программе

Это окно содержит список всех точек останова в вашем проекте. Отсюда вы можете управлять ими всеми. Можно, например, удалить все точки останова, когда вы закончили отладку. Можно добавлять точки останова. Можно редактировать их свойства и временно отключать (disable). Точка останова не активна (т.е. не работает), если галочка слева от неё сброшена. Удобно временно отключать точку останова, если сейчас она вам только мешается, но в будущем ещё понадобится. Тогда вы сейчас её отключаете, а когда она снова понадобится — включаете (enable) обратно.

Кстати, включить/выключить точку останова, а также открыть окно её свойств вы можете, щёлкнув правой кнопкой мыши по красному кружку точки останова в левой части редактора кода:

Список точек останова в программе

Это окно содержит список всех точек останова в вашем проекте. Отсюда вы можете управлять ими всеми. Можно, например, удалить все точки останова, когда вы закончили отладку. Можно добавлять точки останова. Можно редактировать их свойства и временно отключать (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 позаботится обо всех вопросах управления памятью.

Тем не менее, если бы это было так просто — этого раздела тут не было бы :)

А дело тут в том, что в нашей программе у нас есть две вещи, которые существенно осложняют нашу жизнь:

  1. Явная выгрузка плагинов
  2. Использование неуправляемых объектов (например, формы)

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

Собственно, несложно сообразить, что проблем может быть всего две:

  1. Слишком раннее удаление (до обнуления ссылок)
  2. Слишком позднее удаление (не удаляется вообще)

Давайте начнём с первой проблемы.

Двойное удаление

Что означает эта проблема? Она означает, что мы удаляем объект раньше положенного времени — до того, как обнулится его счётчик ссылок. Иными словами, в момент вызова деструктора объекта счётчик ссылок интерфейса будет больше нуля. Это может происходить когда мы удаляем объект с интерфейсом через его объектную ссылку (вызовом Destroy/Free/FreeAndNil). Чем вообще это плохо? Несложно сообразить, что плохо это тем, что когда счётчик ссылок упадёт до нуля — деструктор объекта будет вызван повторно: для уже удалённого объекта!

Поскольку такая проверка потребуется нам везде — и в ядре и в плагинах (мы же всюду используем эту функциональность), то нам нужно поместить этот код в общую для всех папку. Однако, существующие PluginAPI.pas/PluginAPI_TLB.pas для этого не подходят — потому что они содержит заголовочники, а мы хотим написать код поддержки, вспомогательный код. Поэтому создайте новый модуль (File/New/Unit) и сохраните его как PluginAPIHeadersHelpers.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 в отладочном режиме даст нам такое сообщение при выходе:

Свойства точки останова

Первые две строки задают место установки точки останова. Обычно они задаются автоматически, когда вы мышью ставите точку останова, но вы можете указывать их и руками — например, при ручном добавлении точки останова через команду «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 позаботится обо всех вопросах управления памятью.

Тем не менее, если бы это было так просто — этого раздела тут не было бы :)

А дело тут в том, что в нашей программе у нас есть две вещи, которые существенно осложняют нашу жизнь:

  1. Явная выгрузка плагинов
  2. Использование неуправляемых объектов (например, формы)

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

Собственно, несложно сообразить, что проблем может быть всего две:

  1. Слишком раннее удаление (до обнуления ссылок)
  2. Слишком позднее удаление (не удаляется вообще)

Давайте начнём с первой проблемы.

Двойное удаление

Что означает эта проблема? Она означает, что мы удаляем объект раньше положенного времени — до того, как обнулится его счётчик ссылок. Иными словами, в момент вызова деструктора объекта счётчик ссылок интерфейса будет больше нуля. Это может происходить когда мы удаляем объект с интерфейсом через его объектную ссылку (вызовом Destroy/Free/FreeAndNil). Чем вообще это плохо? Несложно сообразить, что плохо это тем, что когда счётчик ссылок упадёт до нуля — деструктор объекта будет вызван повторно: для уже удалённого объекта!

Поскольку такая проверка потребуется нам везде — и в ядре и в плагинах (мы же всюду используем эту функциональность), то нам нужно поместить этот код в общую для всех папку. Однако, существующие PluginAPI.pas/PluginAPI_TLB.pas для этого не подходят — потому что они содержит заголовочники, а мы хотим написать код поддержки, вспомогательный код. Поэтому создайте новый модуль (File/New/Unit) и сохраните его как PluginAPIHeadersHelpers.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 [PluginAPICorePluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259]
4E2553 [PluginAPICorePluginManager.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 [PluginAPICorePluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259]
4E2553 [PluginAPICorePluginManager.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 [PluginAPICorePluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259]
4E2553 [PluginAPICorePluginManager.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 [PluginAPICorePluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259]
4E2553 [PluginAPICorePluginManager.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 сделать это достаточно просто:

  1. Продублируйте метод TInterfacedObject._Release в TCheckedInterfacedObject._Release.
  2. Установите на TCheckedInterfacedObject._AddRef и TCheckedInterfacedObject._Release две non-breaking точки останова, указав обеим условие «Self.FName = ‘TPlugin(DatePlugin.rep): {6DC24451-6C73-47E5-9397-BB7498F686BD}'» (без кавычек, естественно), введя выражение для логгинования «Self.FRefCount» (и снова — без кавычек), а также включив логгирование стека вызовов (можно полностью, а можно только N записей, где N — по вкусу).
  3. Отключите запись всех событий, кроме Breakpoint Messages (и, по вкусу, Output Messages).
  4. Запустите программу, а после её выполнения сохраните содержимое окна 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АлександрDocumentsRAD StudioProjectsPluginsExample5PluginsExportRTF.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 есть два места по увеличению ссылок:

  1. Запись ссылки в Result:
    Result := TPlugin.Create(Self, AFileName);
  2. Запись ссылки в 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-кодом, как оно видимо вызывающему

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

Чтобы облегчить работу для программистов на других языках, вы можете выписать в заголовочники коды исключений для основных классов. Для этого, во-первых, создайте такую вспомогательную программу:

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.

Наследование

Ну, раз уж мы передаём класс исключения, то с наследованием проблемы не возникает — класс эквивалентен источнику, так что вы можете использовать обычные проверки наследования.

Дополнительная информация

Возможно, вам захочется передавать с исключением дополнительную информацию, не подходящую для сообщения об ошибке.

К сожалению, в стандартном механизме нет поля, предназначенного для дополнительной информации. У вас есть три варианта:

  1. Не передавать дополнительную информацию
  2. Реализовать свой аналог SetErrorInfo/GetErrorInfo
  3. Упаковывать информацию в «свободное» поле — 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 нет каких-то классов — просто удалите те строки, которые генерируют ошибку «Неизвестный идентификатор»).

Читать далее.

2003 г

Delphi. Работа над ошибками

Андрей Банин, Королевство Дельфи
11 февраля 2003г.

Вступление

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

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

Выработанные правила направлены на:

  • Повышение надежности работы программы, т.е. уменьшение вероятности возникновения ошибки. Вероятность возникновения ошибки существует всегда, никто не безгрешен (включая операционную систему). Задача программиста — свести эту вероятность к минимуму.
  • Увеличение устойчивости программы — свойства, при котором она возвращается в стабильное состояние после возникновения возмущения (ошибки) (а не зависает, исчезает или уваливает операционную систему).
  • Написание единообразного и легко поддерживаемого кода.

Warnings and Hints

Компилятор Delphi снабжен «анализатором» качества кода. Он может предупреждать о потенциально опасных или бессмысленных ситуациях. Не пренебрегайте его услугами.

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

Использование констант

Используйте именованные константы. Это увеличивает «настраиваемость» исходного кода. А также избавляет от проблем связанных с изменением значения константы в случае ее множественного вхождения.

Range Check и Integer Overflow Check

К сожалению, эти опции компилятора по умолчанию отключены в Delphi, и многие разработчики не пользуются их услугами, а зря. Появления этих ошибок говорит о наличии в программе семантических ошибок, таких как неправильная индексация массива или использование несоответствующего целочисленного типа. Последствия этих ошибок могут быть весьма коварны. Я советую оставлять эти флаги всегда включенными, независимо от того — это отладочная или «финальная» версия программы. Лучше иметь неработающую программу (или ее часть), чем программу работающую неправильно (IMHO).

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

Будьте недоверчивы

Очень часто алгоритмы кодируются из расчета на «нормальный» режим работы (достаточно ресурсов, присутствуют все необходимые компоненты, пользователи нажимают правильные комбинации клавиш и т.д.). Такие реализации очень плохо справляются с возникающими возмущениями. Во избежание этих проблем следуйте следующим простым правилам:

  • Проверяйте значения переменных на допустимость. Особенно это касается переменных типа указатель, процедурных переменных и объектов.
  • Защищайте пары выделение-освобождение ресурсов блоками try/finally. Предполагайте, что исключение может произойти в любом операторе.
  • Используйте процедуру Assert для проверки условий, которые всегда должны быть истинными.

Объем кода, добавленный для проверок и обработки ошибок, может достигать порядка «полезного» кода! Но, такой стиль программирования является необходимым условием при написании сложных систем. Что поделаешь, из бревен небоскреб не построишь

Значения по умолчанию и «неопределенные» значения

В логике распределения значений для переменных всегда необходимо предусматривать «неопределенное» значение и значение по умолчанию. Отсутствие таких значений достаточно часто приводят к семантическим ошибкам.

Правило №1:

  • Для указателей и объектов пустым значением должно являться значение nil.
  • Для числовых типов лучше всего резервировать значение ноль.
  • Для строковых переменных — пустая строка
  • Для перечислимых типов необходимо предусмотреть специальное значение.


Пример:
TDayOfWeek = (dwNone,dwSun,dwMon,dwTue,dwWen,dwThu,dwFri,dwSat);

Правило №2:

«Неопределенными» значениями лучше всего выбирать такие, чье двоичное представление соответствует нулю (нулям). Это увеличивает устойчивость, когда не выполнена начальная инициализация переменной, но произведена инициализация блока памяти, в котором она размещается.

Пример
Для перечислимых типов «неопределенное» значение должно быть первым, так как оно соответствует целочисленному нулю.

Инициализация переменных и полей

Неинициализированные переменные часто становятся причинами возникновения ошибок-фантомов. Обычно они имеют нерегулярную природу, и их трудно выявить в процессе отладки. Особенно катастрофичными могут быть последствия при таком обращении с указателями.

Правило:

  • Для глобальных переменных: использовать типизированные константы, инициализированные переменные или присваивать начальные значения переменным в секции инициализации модуля.
  • Для локальных переменных: присваивать начальные значения в первых строках процедуры или функции.
  • Для полей объектов: присваивать начальные значения полям в конструкторе и не полагаться на то, что память, выделенная под объект, инициализируется нулями.
  • Массивы, записи и выделенные блоки памяти очень удобно инициализировать при помощи функции FillChar. Но, с появлением в Delphi «управляемых» (manageable) типов (длинные строки, динамические массивы, варианты и интерфейсы), пользоваться ей необходимо с четким пониманием.

Пример

type
  TStrArray = array[1..10] of string;
var
  A : TStrArray;
...
  FillChar(A, SizeOf(A), 0); 

В данном примере вызов процедуры FillChar проинициализирует строки пустыми значениями, такой подход был нормальным в ранних версиях Delphi и Borland Pascal, но недопустим в последних версиях, в которых тип string по умолчанию соответствует типу LongString и суть указатель. Если значения строк перед инициализацией были не пусты, то мы получим утечку памяти.

Передача параметров

В Delphi параметры функций и процедур по умолчанию передаются по значению. Т.е. для них выделяется область памяти в стеке или куче, куда копируются оригинальные значения. При передаче параметров сложных типов (запись, массив, строка, вариант) это сопряжено со значительными расходами ресурсов, поэтому параметры этих типов желательно передавать по ссылке, т.е. с использованием ключевых слов var или const. Замечено, что наиболее типична эта ошибка при передаче параметра типа string.

Пример

procedure Proc(s : string); //Не очень хорошо :(
procedure Proc(const s : string); //Гораздо лучше :)

Функции, процедуры и состояния

Для начала словарь терминов:

Функция
— это подпрограмма, задачей которой является получение (извлечение, вычисление и т.д.) определенного значения на основании входных параметров и текущего состояния системы.
Процедура
— это подпрограмма, которая предназначена для выполнения каких либо действий над системой, и соответственно изменяет состояние системы.

Просьба не путать эти определения с ключевыми словами function и procedure.

Правило:

Подпрограмма должна быть либо функцией, либо процедурой. Не совмещайте эти две задачи в одной подпрограмме, разделите ее на несколько подпрограмм.

Контроль достижения предела

Довольно часто встречаются случаи, когда контроль достижения предела цикла осуществляется условием равенства.

Пример

	Repeat
...
	  Inc(I);
	Until I = Limit;

Что произойдет, если в результате ошибки (или просто модификации алгоритма) переменная I перескочит через значение Limit? Правильно — ничего хорошего. Более устойчивой будет конструкция с использованием условия отсечения диапазона, т.е. I >= Limit.

Частота выделения-освобождения ресурсов

Очевидно, что скорость потери ресурсов (памяти, дескрипторов и т.д.) пропорциональна частоте их выделения. Рассмотрите варианты реализации, в которых ресурсы выделяются наиболее редко. Таким образом, вы сможете отсрочить крах программы, и некоторые пользователи могут даже и не узнать, что с ней что-то не так.

Пример:
Допустим, в объекте есть метод DoSomething. В процессе работы он выделяет и освобождает память, которая нужна только ему. С точки зрения «выделения ресурсов по месту их использования» — все корректно, но при многократном обращении к этому методу и в случае наличия ошибки при освобождении памяти вы можете получить достаточно интенсивную утечку памяти. В данной ситуации имеет смысл рассмотреть одноразовое выделение памяти при создании объекта и освобождении при его разрушении. В данной ситуации при наличии ошибки скорость утечки будет гораздо меньше. Естественно, что данное решение необходимо рассматривать в комплексе с другими задачами (производительность, минимизация расхода ресурсов и т.д.)

Область использования переменных

Много сказано и написано на эту тему. Но еще раз повторюсь:

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

Интерфейсы объектов

Четко специфицируйте, какие методы, свойства и поля могут быть доступны и каким образом. «Прячьте» методы, свойства и поля, которые не должны быть доступны извне. Не давайте возможность пользоваться «недокументированными» возможностями ваших объектов. Если по каким либо причинам скрыть эти элементы не получается (к сожалению, система прав доступа к элементам объекта в Delphi несовершенна), тогда не забудьте оформить соответствующий комментарий.

«Просачивание» исключений в библиотеках

При написании библиотеки функций или классов не закрывайте просачивание исключений наружу, если это конечно не предусмотрено логикой библиотеки.

Пример

try
...

exception
  
  on Exception do 
    ShowMessage('Something wrong's happened :-('); 
end; 

Обработка исключений, возникших в библиотеке — это задача приложения, которое использует данную библиотеку.

Определение и использование классов

Любой модуль можно логически разделить на две части:

  • определения — в которой определяются переменные, функции, классы, их методы и т.д.
  • использования — создание экземпляров классов в секции инициализации модуля.

Правило:
При планировании библиотеки классов не совмещайте в одном модуле части определения и использования. Или другими словами — отделяйте определение класса от того, как он будет использован.

Пример

Модуль Forms содержит определения классов, вспомогательных функций и создает экземпляры глобальных переменных (Application, Screen и т.д.). Допустим, в вашем консольном приложении, не использующем графический интерфейс нужна какая-то константа из модуля Forms. Включив его в свой проект, вы получите за бесплатно довесок в несколько сотен килобайт абсолютно ненужного вам кода. В чем причина? Линковщик не может определить, какие виртуальные методы будут вызваны, так как теоретически все они могут быть вызваны косвенно. По этому достаточно одного «упоминания» класса, как весь код его виртуальных методов (а также виртуальных методов других классов, на которые он ссылается) будет влинкован в ваше приложение, тут же. Во избежание подобной проблемы модуль Forms надо было бы разделить на две части: в одной — только определения, а в другой — создания экземпляров, выше указанных, глобальных переменных.

Я столкнулся с описанной проблемой при написании серверного приложения без GUI, которое взаимодействует с базой данных. Где-то в недрах DBxxx компонент есть ссылка на модуль Forms. Эта «особенность» была замечена в Delphi 5, скорее всего эта проблема имела место и в предыдущих версиях. Справедливости ради надо отметить, что в Delphi 7 эта особенность устранена.

Циклические ссылки модулей и «осведомленность» сущностей

Технически, Object Pascal позволят создать циклические ссылки между модулями. Их наличие в программе или библиотеке свидетельствует о не очень удачной декомпозиции (IMHO). Негативными последствиями их использования есть:

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

Правило:
Избегайте использования циклических ссылок модулей. Старайтесь организовать «осведомленность» сущностей древовидной (сущности верхнего уровня знают о существовании сущностей нижнего уровня, но не наоборот). Обратное взаимодействие можно реализовывать посредством механизма событий (процедурных переменных) или при помощи сущностей «посредников».

Пример

Предположим, мы разрабатываем приложение, в котором должны производиться некоторые вычисления и процесс этих вычислений, должен представляться пользователю. Грубо говоря, в данной ситуации мы имеем две сущности: «интерфейс пользователя» и «вычислительный механизм». Они оба должны взаимодействовать друг с другом: «интерфейс пользователя» должен настраивать «вычислитель» и запускать его на выполнение, а «вычислитель» должен выдавать информацию о ходе расчетов. Можно предложить следующий вариант решения:

Модули интерфейса пользователя и вычислителя работают непосредственно друг с другом, т.е. «интерфейс пользователя» вызываем методы «вычислителя» и наоборот. Все будет работать великолепно, пока не окажется, что «вычислитель» необходимо использовать в другой задаче с другим интерфейсом пользователя (или без оного вообще). Обойти данную проблему можно, если в «вычислителе» задачу общения с «внешним миром» (в данном случае — интерфейс пользователя) возложить на функции обратного вызова (callback functions). При таком подходе, заинтересованная сторона регистрируется у «вычислителя», и он будет вызывать ее функции, не подозревая, с кем имеет дело.

Анализ:
В первом случае мы имели двунаправленную «осведомленность» сущностей друг о друге, что привело к проблемам с повторным использованием кода «вычислителя». Во втором случае у нас только однонаправленная «осведомленность» сущностей, т.е. «интерфейс пользователя» знает о вычислителе, но не наоборот. Если необходимо повторно использовать код «интерфейса пользователя», можно пойти дальше — сущность «приложение» знает о существовании сущностей «интерфейс пользователя» и «вычислитель», но последние ничего не знают друг о друге и взаимодействуют через сущность «приложение», исполняющую роль посредника.

Исключения в обработчике события OnTimer

При написания обработчика события OnTimer компонента TTimer необходимо учитывать, что возникновение исключения в нем для обычного Delphi приложения без специализированной обработки исключений приведет к выскакиванию диалога с сообщением об ошибке. Но это не останавливает работу таймера. И если причина возникновения исключения устойчива, то скоро вы увидите следующее сообщения и т.д., пока у системы не закончатся какие-нибудь ресурсы.

Решить данную проблему можно несколькими способами:

  • Обрабатывать исключения непосредственно в обработчике события
    try
    except
      on E: Exception do Application.ShowException(E);
    end;		
  • Использовать централизованный обработчик исключений, который фиксирует их в протоколе или журнале, но не выдает никаких сообщений об ошибке.
    Application.OnException := MyExceptionHandler;

Последний подход желательно использовать в серверных приложениях, когда нет пользователя, который интерактивно взаимодействует с приложением.

Заключение

Правила приведенные в этой статье носят общий характер. Практически всегда существуют исключения (такова природа правил J). Следование этим правилам, позволило мне добиться разработки устойчивого и единообразного кода. Буду признательным за любые дополнения, исправления, замечания, примечания, пожелания и критику (особенно конструктивную).

С уважением,
Андрей Банин
февраль 2003г.
Специально для Королевства Delphi

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

Ошибки
первого уровня (ошибки компиляции)
связаны с неправильной записью операторов
(орфографические, синтаксические). При
обнаружении ошибки компилятор DELPHI
останавливается напротив первого
оператора, в котором обнаружена ошибка.
В нижней части экрана появляется
текстовое окно, содержащее сведения
обо всех ошибках найденных в проекте.
Каждая строка этого окна содержит имя
файла, в котором найдена ошибка, номер
строки с ошибкой и характер ошибки. Для
быстрого перехода к интересующей ошибке
необходимо дважды щелкнуть на строке
с ее описанием. Для получения более
полной информации о характере ошибки
необходимо обратится к HELP
нажатием клавиши F1.
Следует обратить внимание на то, что
одна ошибка может повлечь за собой
другие, которые исчезнут при ее
исправлении. Поэтому следует исправлять
ошибки последовательно, сверху вниз и,
после исправления каждой ошибки
компилировать программу снова.

Ошибки
второго уровня (ошибки выполнения)
связаны с ошибками выбранного алгоритма
решения или с неправильной программной
реализацией алгоритма. Эти ошибки
проявляются в том, что результат расчета
оказывается неверным либо происходит
переполнение, деление на ноль и др.
Поэтому перед использованием отлаженной
программы ее надо протестировать, т.е.
сделать просчеты при таких комбинациях
исходных данных, для которых заранее
известен результат. Если тестовые
расчеты указывают на ошибку, то для ее
поиска следует использовать встроенные
средства отладки среды DELPHI.

В
простейшем случае для локализации места
ошибки рекомендуется поступать следующим
образом. В окне редактирования текста
установить курсор в строке перед
подозрительным участком и нажать клавишу
F4
(выполнение до курсора) или щелкнуть на
серой полосе слева от оператора для
обозначения точки прерывания (появится
красная точка) и нажать клавишу F9.
Выполнение программы будет остановлено
на указанной строке. Для просмотра
текущих значений можно поместить на
нужную переменную курсор (на экране
будет высвечено ее значение), либо нажать
Ctrl-F7
(окно оценки и модификации) или Ctrl-F5
(окно наблюдения) и в появившимся
диалоговом окне указать интересующую
переменную. Нажимая клавишу F7
(пошаговое выполнение), можно построчно
выполнять программу, контролируя
изменение тех или иных переменных и
правильность вычислений. Если курсор
находится внутри цикла, то после нажатия
F4
расчет
останавливается после одного выполнения
тела цикла. Для продолжения расчетов
следует нажать <Run> меню Run или F9.

На
практике, часто вместо вышеописанного
отладочного режима, используют вызов
окна с собщением ShowMessage(‘
‘), где в кавычках ставится текст
сообщения. Это может быть какой-либо
простой текст, или текст со значениями
программных переменных. Поскольку
данное окно является модальным, то после
его вывода работа программы
приостанавливается до тех пор, пока
окно не будет закрыто. Это позволяет в
разных местах программы просмотреть
как меняются значения тех, или иных
переменных и проверить правильность
работы программы.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #

Понравилась статья? Поделить с друзьями:
  • Как исправлять ошибки в adobe acrobat
  • Как исправлять ошибки в 2016 году
  • Как исправлять ошибки в 1с предприятие
  • Как исправлять ошибки бортового компьютера
  • Как исправлять ошибки win 7