См. также Разработка API (контракта) для своей DLL и Разработка системы плагинов, часть 9: подводные камни.
Оглавление
Основы отладки
Сперва мне хотелось бы дать краткое введение в отладку. Если вы уже знакомы с этим материалом, то можете пропустить его. Для тех же, кому краткого введения окажется мало - см. полную версию. Я привожу здесь эту вводную часть потому, что далее я буду говорить слова вроде "ставим non-breaking бряк на строчку XYZ с логгингом стека" и мне хотелось бы, чтобы читатели понимали, о чём идёт речь.Отладчик — это один из основных инструментов любого программиста. Он является составной частью среды Delphi и предназначен для поиска ошибок в программе. Отладчик позволяет выполнять пошаговую трассировку (выполнение кода по шагам), просматривать значения переменных в процессе выполнения программы, устанавливать точки останова (breakpoint) и т.д. Отладка — это процесс работы с программой в отладчике, при котором обнаруживают, локализуют и устраняют ошибки.
Все основные команды, через которые Delphi переходит в режим отладки, находятся в меню "Run":
Основные команды управления отладчиком |
Примечание: команда "Run without debugging" есть не во всех версиях Delphi. А в самых последних версиях Delphi обе команды вынесены на панель инструментов:Итак, оказывается, что в 99% случаев запуска программы - вы запускаете программу именно под отладчиком.
Примечание: команда "Run without debugging" эквивалентна компиляции программы и ручному запуску её с диска вне среды. В меню она вынесена просто для удобства, чтобы не нужно было искать exe-файл в файловой системе. Если вы хотите посмотреть, как поведёт себя программа без опёки отладчика — используйте "Run without debugging".
Два варианта запуска на панели инструментов
Запуск отладки библиотек (плагинов)
Замечу, что запускать на выполнение можно только программы. Библиотеки (DLL или bpl) - это не программы. Это именно что библиотеки - наборы функций. Их нельзя запустить. Но их можно загрузить и вызвать функцию из них. Загружать библиотеку - должен "кто-то", а именно - какая-то программа. Вот её, эту программу, вам и нужно запускать, когда вы хотите отладить библиотеку. Программа запустится, загрузит библиотеку и будет вызывать её функции.Поскольку все плагины являются у нас библиотеками, то при попытке "запустить" плагин вы получите такое сообщение:
Попытка "запустить" DLL или bpl |
Если же вы хотите именно отлаживать плагин, то вам нужно указать программу, которая будет грузить этот плагин. Естественно, в случае плагинов это будет программа-ядро. Для этого вам нужно открыть меню Run/Parameters и указать главную программу:
Указание программы-сервера (host) |
Остановка выполнения программы
Пока программа работает, вы не много можете с ней сделать. Для того чтобы воспользоваться отладчиком, вам нужно приостановить её выполнение. У вас на выбор есть три варианта, первый — нажать на кнопку паузы ("Run"/"Program pause"), второй — возбудить в программе исключение (или же оно возникнет в программе само - например,EAccessViolation
), третий - расставить в нужных местах точки останова (breakpoint-ы, брейкпойнты или просто "бряки").Пауза
Способ первый не позволяет достичь точности. Вы останавливаете программу в тот момент, когда вы нажимаете на кнопку. Это может быть за миллион строк кода до или после того места, где вы в действительности хотели бы быть. Поэтому этот способ используется, когда вам не важно точное место останова. Пример — зависшая программа. Вы просто останавливаете её выполнение в любой момент, чтобы посмотреть, что же там произошло, что программа зависла. Ещё вариант — вам нужна программа на паузе, чтобы проанализировать, скажем, глобальные переменные. Вам не важно место останова, потому что вас интересуют значения переменных, которые, будучи раз заданными, не меняются во время работы программы.Исключения
Второй способ заключается в том, что в отладчике Delphi есть полезнейшая возможность раннего уведомления об исключениях. Каждый раз, когда в программе возникает исключение, отладчик отображает такое окно:Уведомление отладчика в старых версиях Delphi |
Уведомление отладчика в новых версиях Delphi |
Это окно возникает прямо в момент возбуждения исключения до того, как получит управление хоть один блок обработки исключения. Остановка программы в момент возникновения исключения позволяет вам немедленно исследовать ситуацию, используя инструменты отладчика (рассмотрим их чуть ниже) и либо исправить проблему, либо продолжить выполнение программы.
Заметим, что окно это появляется только при отладке. Его появление во время запуска программы из-под Delphi ещё не говорит о том, что при запуске программы вне среды появится хоть какое-то сообщение. Нажав на "Continue" (только в новых Delphi), вы продолжите выполнение программы (с первого блока обработки исключения), а нажав на "Break"/"Ok", вы перейдёте в отладчик, где сможете исследовать ситуацию возникновения исключения.
Иными словами, если вы видите такое окно - это значит, что в вашей программе произошло событие "возбуждение исключения". Окно показывается до выполнения кода обработки, поэтому его показ ещё не означает, что ваша программа вообще покажет хоть какое-то сообщение об ошибке. Хорошим примером является наша функция мульти-загрузки плагинов из папки из первой части: там мы ловили ошибки загрузки плагинов, собирая информацию, но не показывая сообщений об ошибках, а в конце загрузки возбуждали единственное исключение.
Если вы хотите посмотреть, как программа будет работать "вживую", без отладчика и его уведомлений - просто запустите программу вне отладчика (через "Run without debugging" или просто запустив программу руками с диска).
Иногда в этом окне также появляется опция "Show CPU view":
Уведомление с дополнительными опциями |
Заметим, что опция "Show CPU view" показывается достаточно редко. Кстати говоря, её отсутствие в окне уведомления отладчика ещё не говорит о том, что при нажатии на "Break" вы не увидите CPU-отладчика. Более подробно об этом я скажу чуть ниже.
Примечание: кроме уведомления об исключениях, отладчик может показывать и иные сообщения. Их не следует путать с уведомлениями об исключениях.
Точки останова
Третий способ является основным. Заключается он в том, что по тексту программы вы мышкой отмечаете места, где вы хотели бы остановиться во время работы программы. Слева от кода вы видите полоску, в которой появляются синие точки:Среда Delphi показывает наличие отладочной информации для строк кода |
- Возможно, вы открыли файл, не принадлежащий проекту. К примеру, у вас загружен в IDE проект плагина, но вы открыли файл менеджера плагинов. Менеджер плагинов - он, вообще-то, находится в ядре. Если вы хотите его отлаживать, вам нужно открыть проект программы, а не плагина.
- Возможно, модуль ещё не загружен. К примеру, если вы отлаживаете плагин и запустили программу, то вы не увидите отметки, пока плагин не будет загружен программой. Замечу, что это может и не произойти вовсе - если, к примеру, программа и не собирается грузить плагин.
- Возможно, эта строка не используется программой. Например, если в модуле есть три функции, но программа вызывает из него только две функции, то третья функция в программу не попадёт, и напротив строк её кода вы не увидите синих отметок. Второй случай - одна строка из функции может быть выкинута оптимизатором. Например, если вы присваиваете значение, которое дальше не используется. Вы не сможете установить точки останова на все эти строки - просто потому что их физически нет в программе.
- Возможно, что вам нужно просто сделать Build проекту. Т.е. вы смотрите на старый вариант кода. В некоторых случаях нужно почистить папки от старых .dcu файлов.
- Возможно, в вашем проекте отключена отладочная информация. Отладочная информация - это информации о соответствии машинных инструкций из .exe/.dll/.bpl и строчек текстового исходного кода. Если её не будет - отладчик не сможет работать с исходным кодом и вы увидите только машинный отладчик (CPU View). Отладочную информацию можно включить/выключить в коде (директивы
{$D+}/{$D-}
) или опциях проекта (Project/Options/Compiler/Debug information). Подробнее см. мою статью о настройке проектов для отладки. - Возможно, этот код находится в пакете времени выполнения (.bpl). В этом случае он может быть собран с иными опциями, чем указано у вас в проекте. Конечно же, изменения опций проекта никак не влияют на пакеты. В этом случае либо пересоберите пакет с отладочной информацией, либо отключите сборку с пакетами.
- Возможно, Delphi не может связать выполняющуюся программу с исходным кодом и отладочной информацией. Что можно сделать, чтобы этого избежать:
- Не переименовывайте программу или плагины. Изначально задавайте им нужные имена.
- Не перемещайте программу или плагины в другое место. Вместо этого задайте нужный Output directory в настройках проекта, либо просто храните проект в выходном каталоге.
- Не запускайте программу через альтернативные имена. К примеру, папка программа может быть видна под различными именами: её имя, жёсткая ссылка на папку, подмонтированный диск (reparse point), subst-диск, сетевая папка. Если у вас есть несколько вариантов путей к одной и той же папке - везде используйте один и тот же путь, лучше всего - родной, без обвеса. К примеру, если папка C:\Users\Александр\Documents является точкой подключения диска D:\, то к файлу
RAD Studio\Projects\PluginsDemo\richedit.exe
можно обратиться через два имени:
C:\Users\Александр\Documents\RAD Studio\Projects\PluginsDemo\richedit.exe
и
D:\RAD Studio\Projects\PluginsDemo\richedit.exe
В этом случае будет предпочтительнее открывать проект
D:\RAD Studio\Projects\PluginsDemo\richedit.dpr
вместо
C:\Users\Александр\Documents\RAD Studio\Projects\PluginsDemo\richedit.dpr
и запускать
D:\RAD Studio\Projects\PluginsDemo\richedit.exe
вместо
C:\Users\Александр\Documents\RAD Studio\Projects\PluginsDemo\richedit.exe - Убедитесь, что в пути к проекту есть только латинские символы (ASCII). Например, плохо:
C:\Users\Александр\...
. Нормально:C:\Users\Alexandr\...
- Не изменяйте дату-время .dcu/.exe/.dll/.bpl файлов. В некоторых случаях изменение даты может привести к тому, что отладчик будет считать файлы изменёнными и поэтому не соответствующими друг другу. Иногда дата может меняться антивирусом или примочками к IDE.
- Не используйте опцию "Use MSBuild externally", либо включайте генерацию удалённой отладочной информации (remote debug information, RSM).
- Не удаляйте и не перемещайте .dcu файлы. Убедитесь, что выходной каталог для .dcu (Unit output directory) также находится в путях поиска (Search path). В крайнем случае попробуйте выводить .dcu файлы в тот же каталог, что и выходной .exe/.dll/.bpl файл.
- Убедитесь, что файлы проекта можно найти. Вы также можете попробовать подключить все их явно через Project/Add to project, либо же попробовать, наоборот, перечислить папки с ними в опции Search paths проекта.
- Как крайнее средство - попробуйте включить отладочную информацию TD32 и информацию для удалённой отладки. Подробнее см. уже упоминавшуюся статью.
- Ещё как крайнее средство - попробуйте заменить в настройках проекта относительные пути на абсолютные и наоборот. Ещё мощнее - сложить все (проект, выходной файл и .dcu) в одну папку.
- Ну и, конечно же, это могут быть баги Delphi. Чем старее версия Delphi - тем больше в ней может быть багов, связанных с этим. Как правило, все они проявляются лишь при экстремально-граничных случаях: больших размерах модулей, констант и т.п. Вы можете погуглить в интернете или на Quality Central.
Установленная точка останова |
Инструменты отладчика
Итак, после остановки программы в отладчике (любым из трёх описанных выше способов) вы можете использовать его возможности для анализа программы. Большинство вещей, о которых мы будем сейчас говорить, доступны именно в режиме отладки (например, в контекстное меню редактора кода в режиме отладки добавляются новые команды). Вы можете определить, в каком режиме находится среда, взглянув на заголовок окна:Режим проектирования (design-time) - нет подписи |
Режим прогона (run-time), программа работает - добавлено "Running" |
Режим прогона (run-time), программа приостановлена - добавлено "Stopped" |
Среда во время отладки |
Инструменты отладчика |
Попробуйте сейчас, пока программа стоит на паузе, переключиться на программу. Вы щёлкаете по кнопке в панели задач, но ничего не происходит. Попробуйте свернуть все окна, вы увидите примерно такую картину:
"Зависшая" программа под отладкой |
Анализ значений переменных
Итак, продолжаем. Наша программа стоит на паузе. В окне "Local Variables" показываются локальные переменные в текущем месте. Как только мы остановились, отладчик показывает нам чему равны локальные переменные в текущей функции (т.е. функции, в которой мы остановились). Если вы хотите посмотреть значения локальных переменных для других функций - просто дважды щёлкните по нужной функции в окне стека вызова (Call Stack).Для некоторых переменных отладчик может сказать нам, что он не может получить значение переменной ("Variable 'XYZ' inaccessible here due to optimization"). Это работа оптимизатора (кстати, вы можете отключить его, сбросив в опциях проекта галочку "Optimization"). Он выбрасывает переменную, как только в ней отпадёт необходимость.
Итак, "Local Variables" — удобное окно для просмотра локальных переменных. Что делать, если хочется посмотреть не локальную переменную? Можно воспользоваться окном "Watches". Для этого щёлкните правой кнопкой по свободной области окна "Watch List" и выберите "Add watch" — появится окно ввода параметров наблюдения:
Добавление переменной для наблюдения |
Примечание: обычно команды отладчика располагаются в подменю "Debug" (и многие из них могут быть недоступны, если только программа на стоит на паузе под отладчиком), но если в настройках отладчика включить опцию "Rearrange editor local menu on run", то на время отладки все пункты контекстного меню редактора, связанные с отладкой, для удобства выносятся наверх.
Вот пример окна "Watches" после добавления нескольких переменных и выражений для наблюдения:
Наблюдение за несколькими переменными |
IntToStr
). Вызов функции не является безопасным действием, т.к. может иметь побочные эффекты. Например, процедура может менять значение глобальной переменной или даже показывать сообщения. Но если вы уверены, что введённое вами значение вычислять безопасно, вы можете зайти в свойства watch-а и установить галочку "Allow function calls". После этого отладчик сможет показать значение выражения "IntToStr(Tag)", а именно — '1' (строка, а не число). Но будьте аккуратны!Если вам не нужно постоянно следить за переменной, а достаточно лишь разово просмотреть её значение, то вы можете воспользоваться функцией "Evaluate/Modify". Вы выделяете в редакторе кода выражение, которое хотите вычислить, щёлкаете правой кнопкой мыши по нему и выбираете в меню "Evaluate/Modify..." (Ctrl + F7). После этого на экране появляется такое окно:
Вычисление выражение или просмотр значения переменной |
Примечание: кстати говоря, не следует думать, что модификация переменной в любом окне отладчика — это очень простая операция, заключающаяся в изменении памяти, занимаемой переменной. Это может быть и верно для простых типов типа
Integer
, но не для сложных динамических типов типа String
и массивов. Дело в том, что для них ведь нужно выделить память, а старое значение нужно удалить. Поэтому изменение таких переменных ведёт к вызову функций менеджера памяти программы — несмотря на то, что при этом вся пограмма находится на паузе! В типичных ситуациях это не имеет значения, но в некоторых из-за таких побочных эффектов может получаться самое различное поведение программы. Просто имейте этот момент в виду.Альтернативным способом для быстрого просмотра значений переменных и выражений является использование всплывающих подсказок - достаточно подвести курсор мыши к имени переменной в редакторе кода (либо выделить выражение и навести на него мышь) и через короткое время всплывёт подсказка со значением переменной (в случае, если выражение можно вычислить):
Просмотр значения переменной |
Просмотр значения выражения |
Анализ пути выполнения
Следующее окно, которое мы рассмотрим — это "Call Stack". Так называемый стек вызовов:Окно "Call stack" во время отладки |
Текущая процедура (т.е. та, в которой мы находимся) в этом окне маркируется стрелочкой.
По поводу странного вида процедур до Button2Click мы ещё поговорим позже.
Это окно — очень важный инструмент при поиске источника ошибок. Например, при остановке после исключения вы ведь понятия не имеете, что происходит в программе. Взглянув на "Call Stack", вы легко определите, где вы находитесь и как вы сюда попали. Более того, вы можете дважды щёлкнуть по любой строке в этом окне — и вы автоматически попадёте в соответствующее место. Например, если вы сейчас щёлкните по строке с "Unit9.P" в окне "Call Stack", то вы мало того, что перейдёте в редакторе кода к процедуре P, так ещё и строка вызова процедуры A будет подсвечена красным цветом. Очень удобно, если одна процедура вызыватся несколько раз в разных местах. Щёлкнув по нужной строке в этом окне, мы легко определим, откуда был сделан вызов.
Трассировка
Итак, с помощью рассмотренной функциональности вы можете анализировать любую ситуацию в программе — проверять, чему равны у вас переменные, даже вычислять выражения, следить за путём выполнения программы. Но это только одна статичная ситуация из множества возможных. Мы пока всё ещё стоим на месте. Но отладчик позволяет нам больше, а именно: он позволяет выполнять программу по шагам, по строчкам. Посмотрите на последний снимок экрана: мы встали на заданной точке останова. Точка останова показана красной точкой слева от строки кода. Но вы также можете видеть поверх неё небольшую голубую стрелочку, которой не было, когда мы устанавливали точку останова в режиме проектирования. Эта стрелочка показывает, что сейчас будет выполнена указанная строка. Для выполнения есть две основные команды — "Step over" (F8) и "Trace into" (F7). Нажмите, например, на F8. Вы увидите, как стрелочка переместится к следующей строке:Состояние среды при выполнении одной строки после остановки на точке останова |
ShowMessage
. Вы можете видеть установленную точку останова и сдвинутую на одну строку вниз стрелочку (текущую позицию выполняемого кода). Нажмите на F8 ещё раз. Вы увидите, что стрелочка пропадёт, в окнах отладчика появятся надписи "process not accessible", а в заголовке появится приписка "[Running]". Это значит, что наша программа больше не стоит на паузе, а работает. Переключитесь на свою программу. Вы увидите, что она показала сообщение (ShowMessage) с текстом '1' (текстовое представление Tag, который равен 1). Программа полостью работает, вы можете таскать окно по рабочему столу. Закройте окно сообщения своей программы. Немедленно всплывёт окно среды:Отладчик после выполнения второй строки |
ShowMessage
.Таким образом, мы с вами можем выполнять по шагам любой блок кода. Если вы не можете понять, почему ваша программа ведёт себя так, а не иначе — просто поставьте бряк на свой код, и пройдитесь по коду после остановки, выполняя каждую строчку и смотря, как и куда идёт выполнение кода, какие значения каким переменным назначаются и т.п. Большие блоки кода вы можете пропускать, ставя новые бряки и используя команду "Run"/"Run" (F9) или устанавливая курсор в нужную строку и используя "Run to cursor" (F4).
Напомним, что у нас есть две команды для пошагового выполнения — "Step Over" (F8) и "Trace Into" (F7). С первой мы уже познакомились — она просто выполняет текущую строчку и переходит на следующую. "Trace Into" работает похожим образом, но с одним отличием: если в текущей строчке есть вызов процедуры, то "Trace Into" зайдёт внутрь процедуры, в то время как "Step Over" выполнит всю процедуру одним махом. Если никаких вызовов процедур нет, то эти команды ведут себя одинаково.
Например, положим, что мы установили точку останова на вызов некоторой функции - скажем, P. Тогда, если бы вы стояли на "P;" и нажали бы F8, то программа выполнила бы P целиком, после чего мы бы оказались в отладчике на строке после "P;". А если бы нажали на F7, то мы перешли бы в процедуру P, оказавшись на сроке "begin". Разумеется, если бы мы ещё поставили точку останова внутри P, то при попытке выполнить строчку с "P;" "одним махом" с помощью F8, мы всё равно оказались бы внутри P - но уже не в результате "захода в функцию", а как результат срабатывания точки останова. Это полностью соответствует описанной логике. С одной стороны, F8 выполняет строчку целиком. С другой стороны, любой бряк приводит к остановке выполнения программы. Поэтому, когда F8 выполняет строку и в процессе этого выполнения натыкается на бряк, то она останавливает выполнение программы.
Обычно при отладке вы используете F8, выполняя код строго по строчкам. Вас обычно не интересуют детали выполнения вызываемых подпрограмм, а важен лишь конечный результат. Некоторые из этих процедур могут быть весьма нетривиальными — например, запрос реквизитов пользователя и подключение к серверу могут выполняться одной процедурой, которую мы можем выполнить одним нажатием на F8. С другой стороны, мы можем быть заинтересованы в отслеживании выполнения своих собственных процедур, поэтому, когда из одной нашей процедуры вызывается другая наша процедура, то мы будем использовать F7, чтобы зайти внутрь второй нашей процедуры и проследить её выполнение. Но опять же, если мы заранее знаем, чем кончится дело, то мы не обязаны шагать по шагам по всем процедурам — мы вполне можем использовать и F8.
Кроме этих команд ещё есть ещё несколько полезных команд движения по программе - но я оставлю вам их для самостоятельного изучения.
Мы рассмотрели большинство основных возможностей для отладки программы. Два главных инструмента отладчика — это наблюдение за переменными и пошаговое выполнение. Если вы используете описанный инструментарий несколько раз, то у вас появится потребность завершить выполнение программы раньше положенного. Например, вы запустили программу, стали её отлаживать и нашли причину ошибки. Теперь вам нужно её исправить. Но ваша программа сейчас работает или стоит на паузе. Прежде, чем вернуться к редактированию текста, вы должны завершить её. Что вы будете делать? Снимать все бряки, возобновлять выполнение программы и выходить из неё? Есть способ проще — вы можете использовать "Program reset" (Ctrl + F2). Эта команда немедленно обрывает выполнение программы. Её можно рассматривать как аналог команды "Завершить процесс" в Диспетчере Задач, только чуть более гуманный по отношению к среде Delphi.
Примечание: никогда не используйте обычное снятие процесса отлаживаемой программы. Отладчик крайне болезненно относится к снятию процесса извне. Всегда используйте только "Program reset".
Далее, вспомните про понятие отладочной информации, о котором мы говорили выше. Если модуль был скомпилирован без отладочной информации, то использовать обычный отладчик для него вы не сможете. Т.е. не будут работать бряки, поставленные на код этого модуля. Вы не сможете зайти по F7 в любую процедуру этого модуля и т.п. Посмотрите хотя бы на снимки экрана чуть выше: у нас есть вызовы
ShowMessage
и IntToStr
. Если вы попробуете в них зайти, то ничего не выйдет — F7 сработает как обычная F8. Это как раз и происходит потому, что нет отладочной информации для модулей Dialogs
и SysUtils
соответственно. Все стандартные модуля Delphi не имеют отладочной информации. Обычно это очень удобно — ведь вам большую часть времени не нужно отлаживаться внутри стандартных процедур. Однако если вам всё же нужно это сделать (например, по непонятным причинам вылетает Assign
для стандартного TTreeView
и вы должны выяснить почему), то вы можете переключиться между обычной и отладочной версией системных модулей. Для этого вы устанавливаете галочку "Use debug DCUs". После этого вы можете использовать F7, чтобы заходить в стандартные процедуры, в частности, вы теперь можете зайти и в IntToStr. Разумеется, эта опция работает только для стандартных модулей Delphi. Для того чтобы использовать отладочную версию своих модулей — вы должны перекомпилировать их с нужными опциями.Примечание: есть тут ещё один тонкий момент. Если вы компилируете своё приложение с пакетами времени выполнения, то модули, для которых вы хотите включить/выключить отладку могут находиться в пакете, а не в программе. И на них эти опции влиять, разумеется, не будут. Возможно, вам придётся пересобрать свои пакеты или временно отключить компиляцию с пакетами.
Посмотрите на наш пример, когда мы говорили про окно "Call Stack". Мы заметили, что все процедуры ниже
Button2Click
имеют странный вид. Это как раз и происходило потому, что все эти процедуры являлись стандартными процедурами Delphi и поэтому размещались в модулях без отладочной информации. Если бы мы включили опцию "Use debug DCUs", то наш стек вызовов выглядел бы так:Стек вызова после включения опции "Use Debug DCUs" |
События
Следующее окно для ознакомления — "Event Log":Окно Events при запуске процесса |
Окно Events в процессе работы программы под отладкой |
OutputDebugString
(синий цвет). Для создания такой строчки, как на скриншоте, в программе была строка "OutputDebugString('Отладочный вывод от OutputDebugString.');". В-третьих, это различные сообщения, связанные с точками останова (светло-красный цвет), а также сообщения от точек станова (красный цвет) и стек вызовов от них же (оранжевый цвет). Чуть позже мы обсудим точки останова более подробно. Кроме того, в это окно можно добавлять строчки и вручную — выберите пункт "Add Comment..." из контекстного меню (чёрный цвет). Также сюда добавляются уведомления об исключениях, и ещё можно включить логгинг сообщений Windows.По-умолчанию, лог очищается при каждом запуске процесса. Вы также можете сохранить его в файл для анализа или очистить руками в середине работы — для этого воспользуйтесь соответствующими командами из контекстного меню. Кроме того, в опциях отладчика есть настройка окна "Event Log" (которая также доступна из контекстного меню окна "Event Log").
В частности, помимо настройки поведения и внешнего вида, здесь можно включить/отключить логгинг определённых типов событий. Если интересующее вас событие происходит редко и/или тонет в общей массе событий, можно просто выключить все другие типы событий. Именно это является причиной, почему по-умолчанию отключен логгинг сообщений Windows — их всегда бывает очень много. Кроме того, вероятно, вы захотите отключить опцию "Display process info with events" — она показывает дополнительную информацию о процессе, вызвавшем событие. Поскольку чаще всего вы будете отлаживать только один процесс, эта информация не несёт полезной нагрузки и только создаёт шум в логе. В случае отладки двух процессов эта опция позволит отличать события от разных процессов.
Расширенные точки останова
В самом начале этого пункта мы буквально краем коснулись точек останова с целью быстрее познакомить вас с возможностями отладчика, т.к. они (возможности) доступны только в режиме остановки программы, а точки останова являются основным средством для установки программы на паузу. Теперь мы рассмотрим их более подробно. И для этого сначала взглянем на окно "Breakpoints":Список точек останова в программе |
Кстати, включить/выключить точку останова, а также открыть окно её свойств вы можете, щёлкнув правой кнопкой мыши по красному кружку точки останова в левой части редактора кода:
Контекстное меню точки останова |
Свойства точки останова |
Boolean
.Строка "Pass Count" определяет, на который проход мимо точки останова отладчик остановит программу. 0 или 1 означает немедленную остановку. Например, если бы мы указали "Pass Count" равным двум в нашем примере, то мы бы пропустили первую итерацию цикла и остановились бы только на второй итерации. После срабатывания точки останова счётчик сбрасывается, и отсчёт начинается снова (поэтому, мы пропустили бы третью итерацию цикла и остановились бы на четвёртой, если бы она у нас была). Может комбинироваться с полем "Condition". В этом случае сперва высчитывается поле "Condition" и, если оно равно
True
, то проверяется/изменяется счётчик "Pass Count".Поле "Group" определяет группу, в которую входит точка останова. Обычно используется, если у вас много точек исключения. В этом случае их можно сгруппировать в группу и управлять всеми точками останова в группе (например, включать/выключать) одновременно как единым целым. Для включения точки останова в группу просто введите её имя в поле "Group". Если вы уже вводили название группы для другой точки останова, то вместо повторного ввода вы можете выбрать группу из раскрывающегося списка. Иногда имеет смысл включать в группу одну-единственную точку останова. Это бывает в случаях, когда вы создаёте сложные условия с помощью продвинутых (advanced) опций (описание чуть ниже).
Флажок "Keep existing breakpoint" (в старых Delphi его нет) служит для создания новой точки останова при модификации свойств уже существующей. Например, вы поставили точку останова, задали ей свойства, а потом решили поставить точно такую же точку останова, но чуть ниже, на другую строчку. Чтобы не создавать новую точку останова и не вводить все свойства заново, вы можете открыть свойства уже существующей точки останова (с проставленными свойствами), установить галочку "Keep existing breakpoint" и изменить поле "Line number" (разумеется, сначала вам нужно посмотреть в редакторе кода номер строки, на которую вы хотите установить новую точку останова).
В режиме "Advanced" (кнопка "Advanced" сворачивает или разворачивает нижнюю часть окна) вам доступны продвинутые режимы использования точек останова, которые используются значительно реже. Флажок "Break", если он установлен, определяет обычное поведение точки останова. Если вы его сбросите, то точка останова не будет приводить к остановке программы. Зачем, в таком случае, она нужна? Дело в том, что вы можете назначить некоторые события, которые будут выполняться при прохождении точки останова. Все опции в разделе "Advanced" делают именно это. Для многих из них вы, вероятно, захотите сбросить опцию "Break", т.к. вам нужно, чтобы просто сработало событие, но не нужно при этом останавливаться. В этом случае точка останова ведёт себя подобно триггеру на задаваемое действие.
Опции "Ignore subsequent exceptions" и "Handle subsequent exceptions" обычно работают парой. Если выполнение программы проходит мимо точки останова с установленной опцией "Ignore subsequent exceptions", то отладчик отключает свои уведомления об исключениях. Опция "Handle subsequent exceptions" действует ровно наоборот — она включает уведомления. Если вы отлаживаете код, в котором часто возникают исключения перед тем, как выполнение дойдёт до интересующего вас места, то вы можете установить точку останова до и после кода, возбуждающего исключения. Последовательно задавая этим точкам останова опции "Ignore subsequent exceptions" и "Handle subsequent exceptions" и сбрасывая опцию "Break", вы добьётесь игнорирования отладчиком исключений на проблемном участке кода.
Опция "Log message" заносит заданное сообщение в окно "Event Log" каждый раз, когда срабатывает точка останова.
Опция "Eval expression" вычисляет заданное выражение каждый раз при срабатывании бряка. Если при этом включена опция "Log result", то результат вычислений добавляется в "Event Log". Очень полезная функция (вместе с "Log message"), которую можно использовать для логгирования без модификации исходного кода (т.е. устанавливаются точки останова вместо
OutputDebugString
в коде, и логгинг работает сразу — нет необходимости перекомпилировать и перезапускать программу). Разумеется, в отличие от OutputDebugString
, логгинг средствами точек останова работает только при отладке из-под отладчика Delphi и не доступен при автономном прогоне программы (для OutputDebugString
при этом доступен вывод от программы DebugView). Удобно использовать эти опции для "лёгкого профайлинга" ('лёгкого' — в смысле примитивного): для замера времени выполнения какого-то кода, установите вокруг него две точки останова. В "Eval expression" впишите GetTickCount и сбросьте опцию "Break". После прогона разница значений в логе даст вам приближённое время выполнения участка кода в миллисекундах."Enable/Disable group" включает или выключает группу брейкпойнтов при срабатывании текущей точки останова. Используются довольно редко, т.к. необходимы для задания довольно сложного поведения точек останова. Один из вероятных сценариев использования этих опций — отладка двух разных потоков сразу. Например, при достижении точки останова в первом потоке отключаются все точки останова во втором потоке и наоборот. Таким образом, начав отладку одного потока (первого, в котором сработает точка останова), наш процесс отладки не прервётся другим потоком. Это избавляет вас от ручного включения и выключения точек останова при нескольких проходах отладки. Другой вариант использования этих опций — отладка системных модулей. Например, вы расставили точки останова в коде VCL. Но нужный вам код VCL выполняется при запуске приложения, а вам нужно, чтобы точки останова срабатывали только после, например, нажатия на кнопку. Поэтому можно отключить точки останова, а в нужное место (например, в dpr-файле после создания форм) поставить пустую точку останова, указав, что при проходе она должна включать все точки останова. Тогда получится, что, во время загрузки приложения точки останова будут молчать, а как только пойдёт работать ваш код — тут они и сработают.
"Log Call Stack" (нет в старых Delphi) заносит в "Event Log" стек вызовов при прохождении точки останова. Например, установив бряк с этой опцией (и без опции "Break") на начало функции, можно логгировать, кто вызывает эту функцию. Опции "Entire stack" и "Partial stack" переключают логгинг всего стека или только первых "Number of frames" записей. Это невероятно удобная опция, если у вас нет под рукой готового инструмента типа JCL или EurekaLog. Поставив точки останова с этой опцией на конструкторы класса Exception (разумеется, только с включённой опцией "Use Debug DCUs", т.к. класс Exception сидит в стандартном модуле SysUtils.pas), вы во многих случаях можете упростить отладку, т.к. при возникновении исключения в "Event Log" будет попадать стек вызова для возникшего ислючения.
Практический пример
Комплексный пример использования средств отладчика для нахождения решения проблемы можно увидеть здесь.Управлением временем жизни
Первая проблема, которой мне хотелось бы коснуться - это управление временем жизни объектов в нашей системе. Поскольку мы реализуем систему на базе интерфейсов, а интерфейсы относятся к авто-управляемым типам данным, то, к счастью, обычно нам не нужно делать никаких специальных действий: RTL Delphi позаботится обо всех вопросах управления памятью.Тем не менее, если бы это было так просто - этого раздела тут не было бы :)
А дело тут в том, что в нашей программе у нас есть две вещи, которые существенно осложняют нашу жизнь:
- Явная выгрузка плагинов
- Использование неуправляемых объектов (например, формы)
Собственно, несложно сообразить, что проблем может быть всего две:
- Слишком раннее удаление (до обнуления ссылок)
- Слишком позднее удаление (не удаляется вообще)
Двойное удаление
Что означает эта проблема? Она означает, что мы удаляем объект раньше положенного времени - до того, как обнулится его счётчик ссылок. Иными словами, в момент вызова деструктора объекта счётчик ссылок интерфейса будет больше нуля. Это может происходить когда мы удаляем объект с интерфейсом через его объектную ссылку (вызовомDestroy
/Free
/FreeAndNil
). Чем вообще это плохо? Несложно сообразить, что плохо это тем, что когда счётчик ссылок упадёт до нуля - деструктор объекта будет вызван повторно: для уже удалённого объекта!Поскольку такая проверка потребуется нам везде - и в ядре и в плагинах (мы же всюду используем эту функциональность), то нам нужно поместить этот код в общую для всех папку. Однако, существующие PluginAPI.pas/PluginAPI_TLB.pas для этого не подходят - потому что они содержит заголовочники, а мы хотим написать код поддержки, вспомогательный код. Поэтому создайте новый модуль (File/New/Unit) и сохраните его как \PluginAPI\Headers\Helpers.pas, после чего измените его следующим образом:
unit Helpers; interface uses Windows, SysUtils, Classes; type ECheckedInterfacedObjectError = class(Exception); ECheckedInterfacedObjectDeleteError = class(ECheckedInterfacedObjectError); TDebugName = String[99]; TCheckedInterfacedObject = class(TInterfacedObject) private FName: TDebugName; protected procedure SetName(const AName: String); public constructor Create; procedure BeforeDestruction; override; end; implementation uses PluginAPI; resourcestring rsInvalidDelete = 'Попытка удалить объект %s при активной интерфейсной ссылке; счётчик ссылок: %d'; { TCheckedInterfacedObject } constructor TCheckedInterfacedObject.Create; begin FName := TDebugName(Format('[$%s] %s', [IntToHex(PtrUInt(Self), SizeOf(Pointer) * 2), ClassName])); inherited; end; procedure TCheckedInterfacedObject.SetName(const AName: String); begin FillChar(FName, SizeOf(FName), 0); FName := TDebugName(AName); end; procedure TCheckedInterfacedObject.BeforeDestruction; begin if FRefCount <> 0 then raise ECheckedInterfacedObjectDeleteError.CreateFmt(rsInvalidDelete, [String(FName), FRefCount]); inherited; end; end.В этом коде мы создали наследника от
TInterfacedObject
потому что мы хотим иметь всю функциональность TInterfacedObject
, просто добавив к ней дополнительные проверки. Сама проверка сидит в служебном методе BeforeDestruction
, который вызывается непосредственно перед уничтожением объекта. Вот в нём мы и вставили нашу проверку: если счётчик ссылок отличен от нуля, то это значит, что на текущий объект ещё ссылаются какие-то интерфейсы, а мы его удаляем раньше времени - через ручной вызов деструктора по объектной ссылке.Чтобы как-то идентифицировать объект, с которым происходит такая плохая вещь, мы ввели свойство Name. Оно не выставляется наружу, потому что это внутреннее дело объекта - для отладочных целей. Предполагается, что вы вызовите метод
SetName
в конструкторе своего объекта, передав туда какую-то строку, однозначно идентифицирующую объект. Если вы это не сделаете, то имя объекта по умолчанию будет содержать hex-представление адреса объекта и имя класса объекта.Странный, на первый взгляд, выбор типа строки для отладочного имени и загадочный вызов
FillChar
я поясню чуть позже, где будет наглядно видно, зачем нам потребовалось делать именно так.Вы можете проверить работу этого кода, сэмулировав "плохую" ситуацию:
var C: TCheckedInterfacedObject; I: IInterface; begin C := TCheckedInterfacedObject.Create; I := C; C.Free; // <- попытка удалить объект возбудит исключение, // потому что у нас есть активная интерфейсная ссылка I end;С другой стороны:
var C: TCheckedInterfacedObject; I: IInterface; begin C := TCheckedInterfacedObject.Create; I := C; ... I := nil; // <- уменьшает счётчик ссылок до нуля и удаляет объект, нет проблем end;Итак, теперь всюду в нашем коде, где у нас есть
TInterfacedObject
мы должны заменить его на TCheckedInterfacedObject
(не забыв вписать в uses
модуль Helpers
, конечно же), опционально задать в конструкторе уникальное имя - и теперь весь наш код будет защищён от подобной ситуации. Если теперь вы допустите ошибку в коде, наш проверочный код её увидит и возбудит исключение. Вы можете остановиться в отладчике по исключению (как я описывал в начале статьи) и исследовать ситуацию: проверить стек вызовов, чтобы узнать где происходит неверное удаление, и анализом переменных выяснить, кто и почему удаляется.Однако, если вы попробуете запустить такой тестовый пример (от самого первого примера он отличается тем, что я поменял местами удаление объекта через интерфейс и через объект):
var C: TCheckedInterfacedObject; I: IInterface; begin C := TCheckedInterfacedObject.Create; I := C; ... I := nil; C.Free; // <- добавили end;то увидите, что наша реализация не отлавливает такие проблемы. Вместо этого объект будет удалён дважды и, в итоге, свалится с Invalid pointer operation при попытке повторного освобождения памяти.
Мы можем улучшить наш объект следующим образом:
type ... ECheckedInterfacedObjectDoubleFreeError = class(ECheckedInterfacedObjectError); TCheckedInterfacedObject = class(TInterfacedObject) private ... function GetRefCount: Integer; protected ... public ... property RefCount: Integer read GetRefCount; end; implementation ... resourcestring ... rsDoubleFree = 'Попытка повторно удалить уже удалённый объект %s'; { TCheckedInterfacedObject } ... procedure TCheckedInterfacedObject.BeforeDestruction; begin if FRefCount < 0 then raise ECheckedInterfacedObjectDoubleFreeError.CreateFmt(rsDoubleFree, [String(FName)]) else if FRefCount <> 0 then raise ECheckedInterfacedObjectDeleteError.CreateFmt(rsInvalidDelete, [String(FName), FRefCount]); inherited; FRefCount := -1; end; function TCheckedInterfacedObject.GetRefCount: Integer; begin if FRefCount < 0 then Result := 0 else Result := FRefCount; end; ...Задача этого кода - предотвратить повторное выполнение деструктора. Для этого мы при первом удалении объекта "скидываем" счётчик ссылок в -1 - это специальное значение, которое я выбрал "от балды" (лишь бы оно не попадало в диапазон допустимых значений счётчика: 0 и положительные). Специальное значение счётчика ссылок используется в качестве маркера "объект уже удалён". Таким образом, если объект будет удалять кто-то ещё - вы увидите сообщение о попытке повторного удаления.
Примечание: повторное выполнение деструктора возможно по той причине, что при освобождении памяти, она просто помечается как "свободная", но не удаляется на самом деле. Поэтому в ней остаётся её старое содержание, к ней также можно обратиться (здесь: "можно" = "не приведёт к исключению", а не "допустимо так поступать"). Вот почему второй вызов деструктора начнёт своё выполнение без проблем. Мы исправили эту ситуацию, записав в эту "освобождаемую" память специальное значение.
Конечно же, помимо этого сценария возможны ещё два случая: память может быть действительно освобождена, а не просто помечена, как свободная, и память может быть повторно использована при последующем выделении памяти. В первой ситуации наша проверка не нужна - вы поймаете Access Violation в момент вызова деструктора. Во второй ситуации... что ж, там может быть различное поведение. И наш код как может защитить от такой ситуации, так и нет. Но чаще всего вы также схлопочете Access Violation при попытке вызова деструктора.
Итого, из трёх возможных ситуаций (память помечается "свободной", но не используется; память помечается "свободной", а затем повторно используется; память действительно освобождается) наш код проверки помогает с первым случаем, а второй и третий в наших проверках не нуждаются - они и так выбросят исключение. И во всех трёх случаях деструктор не будет выполняться повторно - что мы и хотели.
Тогда:
var C: TCheckedInterfacedObject; I: IInterface; begin C := TCheckedInterfacedObject.Create; I := C; ... I := nil; // <- удалит объект C.Free; // <- даст по рукам за повторное удаление end;И даже:
var C: TCheckedInterfacedObject; begin C := TCheckedInterfacedObject.Create; ... C.Free; // <- удалит объект C.Free; // <- даст по рукам за повторное удаление end;И, наконец, последняя аналогичная проблема тут - повторный вызов деструктора через интерфейсную ссылку. Это более хитрая ситуация и обычно она выглядит так: счётчик ссылок объекта падает до нуля и вызывается деструктор этого объекта. Во время выполнения деструктора какой-то из методов объекта передаёт ссылку объекта куда-то. Это приводит к увеличению счётчика ссылок с 0 до 1. Когда вызванный метод вернёт управление, счётчик ссылок уменьшится с 1 до 0, что приведёт к повторному вызову деструктору - и это прямо в самом деструкторе этого же объекта! В итоге вы получите самое разное поведение - Invalid pointer operation, Access Violation и даже зависание.
А решение этой проблемы достаточно просто: нужно возбудить ошибку при попытке увеличения счётчика ссылок с 0 до 1:
type ... ECheckedInterfacedObjectUseDeletedError = class(ECheckedInterfacedObjectError); TCheckedInterfacedObject = class(TInterfacedObject, IInterface) private ... protected ... function _AddRef: Integer; stdcall; public ... end; implementation ... resourcestring ... rsUseDeleted = 'Попытка использовать уже удалённый объект %s'; { TCheckedInterfacedObject } ... function TCheckedInterfacedObject._AddRef: Integer; begin if FRefCount < 0 then raise ECheckedInterfacedObjectUseDeletedError.CreateFmt(rsUseDeleted, [String(FName)]); Result := inherited; end; ...Достаточно просто. В этой реализации мы учли тот факт, что у удалённого объекта счётчик ссылок будет меньше нуля, а не ноль (как мы изменили это чуть выше).
Эту новую проверку можно проверить таким образом:
var C: TCheckedInterfacedObject; I: IInterface; begin C := TCheckedInterfacedObject.Create; I := C; ... I := nil; // <- удаление объекта I := C; // <- по рукам за использование уже удалённого объекта end;
Утечки памяти
Итак, с первой проблемой (неверное удаление) мы разобрались. Давайте теперь посмотрим на вторую (противоположную) проблему - пропуск удаления объектов (утечки памяти).Как я уже не раз упоминал в предыдущих статьях этой серии, стандартно вам не нужно волноваться о проблемах утечек, потому что интерфейсы относятся к авто-управляемым типам данных и мы не делаем низкоуровнего изменения счётчика ссылок или приведений типов. Поэтому практически единственная проблема, которая может при этом возникнуть - циклические ссылки: когда два объекта ссылаются друг на друга, и поэтому счётчик обоих больше нуля, так что оба объекта не могут быть удалены. Цепочка связи может быть косвенной и включать в себя более двух объектов.
Стандартное решение проблемы циклических ссылок - явно попросить любой из объектов отпустить ссылку на второй объект. Мы уже много раз делали это через интерфейс
IDestroyNotify
. Но если вы допустите тут ошибку (забудете какой-то вызов), то вы получите утечку памяти. В итоге это может привести к Access Violation, если вы выгрузите плагин, у которого ещё остались висячие ссылки. Первый шаг в диагностике утечек - определить, что они вообще есть. Делается это ровно так же, как я рассказывал в статье про утечки памяти (дополнение): для старых Delphi вы подключаете специальный модуль с проверкой
AllocMemCount
, а для новых Delphi достаточно просто включить ReportMemoryLeaksOnShutdown
. Это даст вам простую проверку "да/нет" на наличие утечек. Конечно же, включать её нужно как для главной программы (ядра), так и для каждого плагина.Если утечек нет - хорошо, вы нигде не ошиблись. Если же они есть - вам нужно понять, как они возникли.
Первое, что вы можете попробовать - подключить отладочный менеджер памяти. Я буду использовать FastMM как стандартный вариант для локальной отладки. Подробно его использование описано в только что упомянутой статье.
Итак, пусть у нас есть утечка памяти. Если её у вас нет, то для проверке описываемых техник на практике вы можете руками вызвать
_AddRef
на какой-нибудь объект. Например, на плагин:
procedure TMainForm.FormCreate(Sender: TObject); ... begin ... Plugins[0]._AddRef; // <- без парного вызова это - утечка end;И тогда запуск программы с FastMM в отладочном режиме даст нам такое сообщение при выходе:
Сообщение отладочного режима FastMM о найденных утечках |
--------------------------------2012/2/24 16:32:17-------------------------------- A memory block has been leaked. The size is: 52 This block was allocated by thread 0x1B0C, and the stack trace (return addresses) at the time was: 4043E2 4E227F [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259] 4E2553 [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugins][299] 4E7298 [remain.pas][remain][remain.TMainForm.FormCreate][467] 4D3B53 [Forms.pas][Forms][Forms.TCustomForm.DoCreate][3319] 4D3713 [Forms.pas][Forms][Forms.TCustomForm.Create][3189] 4DDF85 [Forms.pas][Forms][Forms.TApplication.CreateForm][9879] 4EFD31 [richedit.dpr][richeditdemo][richeditdemo.richeditdemo][18] 74FB339A [BaseThreadInitThunk] 77539EF2 [Unknown function at RtlInitializeExceptionChain] 77539EC5 [Unknown function at RtlInitializeExceptionChain] The block is currently used for an object of class: UnicodeString The allocation number is: 7093 Current memory dump of 256 bytes starting at pointer address 7EE69870: B0 04 02 00 01 00 00 00 13 00 00 00 4D 00 65 00 6E 00 75 00 20 00 44 00 65 00 6D 00 6F 00 20 00 70 00 6C 00 75 00 67 00 69 00 6E 00 20 00 23 00 31 00 00 00 16 94 80 12 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 53 1E 00 00 E2 43 40 00 02 B1 43 00 69 B2 43 00 B0 DC 46 00 F7 32 4B 00 F7 32 4B 00 71 34 4B 00 53 5F 4B 00 19 3F 4B 00 55 4D 4D 00 73 F2 4A 00 0C 1B 00 00 0C 1B 00 00 FE 43 40 00 02 B1 43 00 69 B2 43 00 B0 DC 46 00 F7 32 4B 00 F7 32 4B 00 71 34 4B 00 53 5F 4B 00 19 3F 4B 00 55 4D 4D 00 73 F2 4A 00 34 00 00 00 B0 04 02 00 3B EB 26 85 68 A2 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 C4 14 D9 7A 00 00 00 00 71 96 E6 7E ° . . . . . . . . . . . M . e . n . u . . D . e . m . o . . p . l . u . g . i . n . . # . 1 . . . . ” Ђ . . . . . . . . . . . . . . . . . . . . . . . . . S . . . в C @ . . ± C . i І C . ° Ь F . ч 2 K . ч 2 K . q 4 K . S _ K . . ? K . U M M . s т J . . . . . . . . . ю C @ . . ± C . i І C . ° Ь F . ч 2 K . ч 2 K . q 4 K . S _ K . . ? K . U M M . s т J . 4 . . . ° . . . ; л & … h ў O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . Д . Щ z . . . . q – ж ~ --------------------------------2012/2/24 16:32:17-------------------------------- A memory block has been leaked. The size is: 36 This block was allocated by thread 0x1B0C, and the stack trace (return addresses) at the time was: 4043E2 4E227F [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259] 4E2553 [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugins][299] 4E7298 [remain.pas][remain][remain.TMainForm.FormCreate][467] 4D3B53 [Forms.pas][Forms][Forms.TCustomForm.DoCreate][3319] 4D3713 [Forms.pas][Forms][Forms.TCustomForm.Create][3189] 4DDF85 [Forms.pas][Forms][Forms.TApplication.CreateForm][9879] 4EFD31 [richedit.dpr][richeditdemo][richeditdemo.richeditdemo][18] 74FB339A [BaseThreadInitThunk] 77539EF2 [Unknown function at RtlInitializeExceptionChain] 77539EC5 [Unknown function at RtlInitializeExceptionChain] The block is currently used for an object of class: UnicodeString The allocation number is: 7094 Current memory dump of 256 bytes starting at pointer address 7EE72890: B0 04 02 00 01 00 00 00 07 00 00 00 31 00 2E 00 30 00 2E 00 30 00 2E 00 30 00 00 00 52 47 80 12 5C A5 4F 00 5C A5 4F 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 58 1E 00 00 E2 43 40 00 02 B1 43 00 69 B2 43 00 B0 DC 46 00 F7 32 4B 00 F7 32 4B 00 71 34 4B 00 53 5F 4B 00 19 3F 4B 00 55 4D 4D 00 73 F2 4A 00 0C 1B 00 00 0C 1B 00 00 FE 43 40 00 02 B1 43 00 69 B2 43 00 B0 DC 46 00 F7 32 4B 00 F7 32 4B 00 71 34 4B 00 53 5F 4B 00 19 3F 4B 00 55 4D 4D 00 73 F2 4A 00 1E 00 00 00 B0 04 02 00 3A 7B 27 85 68 A2 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 C5 84 D8 7A 4F 00 5C A5 4F 00 00 00 00 00 61 27 E7 7E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 D5 1B 00 00 E2 43 40 00 7F 22 4E 00 53 25 4E 00 ° . . . . . . . . . . . 1 . . . 0 . . . 0 . . . 0 . . . R G Ђ . \ Ґ O . \ Ґ O . . . . . . . . . . . . . . . . . . . . . . . . . X . . . в C @ . . ± C . i І C . ° Ь F . ч 2 K . ч 2 K . q 4 K . S _ K . . ? K . U M M . s т J . . . . . . . . . ю C @ . . ± C . i І C . ° Ь F . ч 2 K . ч 2 K . q 4 K . S _ K . . ? K . U M M . s т J . . . . . ° . . . : { ' … h ў O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ Е „ Ш z O . \ Ґ O . . . . . a ' з ~ . . . . . . . . . . . . . . . . Х . . . в C @ . " N . S % N . --------------------------------2012/2/24 16:32:17-------------------------------- A memory block has been leaked. The size is: 212 This block was allocated by thread 0x1B0C, and the stack trace (return addresses) at the time was: 4043E2 4E7298 [remain.pas][remain][remain.TMainForm.FormCreate][467] 4D3B53 [Forms.pas][Forms][Forms.TCustomForm.DoCreate][3319] 4D3713 [Forms.pas][Forms][Forms.TCustomForm.Create][3189] 4DDF85 [Forms.pas][Forms][Forms.TApplication.CreateForm][9879] 4EFD31 [richedit.dpr][richeditdemo][richeditdemo.richeditdemo][18] 74FB339A [BaseThreadInitThunk] 77539EF2 [Unknown function at RtlInitializeExceptionChain] 77539EC5 [Unknown function at RtlInitializeExceptionChain] The block is currently used for an object of class: UnicodeString The allocation number is: 7046 Current memory dump of 256 bytes starting at pointer address 7EEC1890: B0 04 02 00 01 00 00 00 58 00 00 00 43 00 3A 00 5C 00 55 00 73 00 65 00 72 00 73 00 5C 00 10 04 3B 04 35 04 3A 04 41 04 30 04 3D 04 34 04 40 04 5C 00 44 00 6F 00 63 00 75 00 6D 00 65 00 6E 00 74 00 73 00 5C 00 52 00 41 00 44 00 20 00 53 00 74 00 75 00 64 00 69 00 6F 00 5C 00 50 00 72 00 6F 00 6A 00 65 00 63 00 74 00 73 00 5C 00 50 00 6C 00 75 00 67 00 69 00 6E 00 73 00 5C 00 45 00 78 00 61 00 6D 00 70 00 6C 00 65 00 35 00 5C 00 50 00 6C 00 75 00 67 00 69 00 6E 00 73 00 5C 00 44 00 61 00 74 00 65 00 50 00 6C 00 75 00 67 00 69 00 6E 00 2E 00 72 00 65 00 70 00 00 00 7C 51 6A 1B 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 5C A5 4F 00 00 00 00 00 30 15 EC 7E 00 00 00 00 00 00 00 00 3C E8 40 00 00 00 00 00 87 1B 00 00 E2 43 40 00 7F 22 4E 00 53 25 4E 00 ° . . . . . . . X . . . C . : . \ . U . s . e . r . s . \ . . . ; . 5 . : . A . 0 . = . 4 . @ . \ . D . o . c . u . m . e . n . t . s . \ . R . A . D . . S . t . u . d . i . o . \ . P . r . o . j . e . c . t . s . \ . P . l . u . g . i . n . s . \ . E . x . a . m . p . l . e . 5 . \ . P . l . u . g . i . n . s . \ . D . a . t . e . P . l . u . g . i . n . . . r . e . p . . . | Q j . O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . \ Ґ O . . . . . 0 . м ~ . . . . . . . . < и @ . . . . . ‡ . . . в C @ . " N . S % N . --------------------------------2012/2/24 16:32:17-------------------------------- A memory block has been leaked. The size is: 212 This block was allocated by thread 0x1B0C, and the stack trace (return addresses) at the time was: 4043E2 4E227F [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259] 4E2553 [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugins][299] 4E7298 [remain.pas][remain][remain.TMainForm.FormCreate][467] 4D3B53 [Forms.pas][Forms][Forms.TCustomForm.DoCreate][3319] 4D3713 [Forms.pas][Forms][Forms.TCustomForm.Create][3189] 4DDF85 [Forms.pas][Forms][Forms.TApplication.CreateForm][9879] 4EFD31 [richedit.dpr][richeditdemo][richeditdemo.richeditdemo][18] 74FB339A [BaseThreadInitThunk] 77539EF2 [Unknown function at RtlInitializeExceptionChain] 77539EC5 [Unknown function at RtlInitializeExceptionChain] The block is currently used for an object of class: TPlugin The allocation number is: 7047 Current memory dump of 256 bytes starting at pointer address 7EEC19F0: EC 1D 4E 00 01 00 00 00 78 1C 40 00 3F 54 50 6C 75 67 69 6E 28 44 61 74 65 50 6C 75 67 69 6E 2E 72 65 70 29 3A 20 7B 36 44 43 32 34 34 35 31 2D 36 43 37 33 2D 34 37 45 35 2D 39 33 39 37 2D 42 42 37 34 39 38 46 36 38 36 42 44 7D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 BC 09 4E 00 F0 F2 F6 7E 9C 18 EC 7E 00 00 C9 02 38 B1 D3 02 94 B1 D3 02 FF FF FF FF 00 00 00 00 51 44 C2 6D 73 6C E5 47 93 97 BB 74 98 F6 86 BD 7C 98 E6 7E 9C 28 E7 7E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 70 1C 4E 00 80 1C 4E 00 94 1C 4E 00 A4 1C 4E 00 B4 1C 4E 00 00 00 00 00 37 08 CE 1A 5C A5 4F 00 00 00 00 00 D1 25 EC 7E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 9A 1B 00 00 E2 43 40 00 24 8B 4E 00 A8 AC D3 02 м . N . . . . . x . @ . ? T P l u g i n ( D a t e P l u g i n . r e p ) : { 6 D C 2 4 4 5 1 - 6 C 7 3 - 4 7 E 5 - 9 3 9 7 - B B 7 4 9 8 F 6 8 6 B D } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ј . N . р т ц ~ њ . м ~ . . Й . 8 ± У . ” ± У . я я я я . . . . Q D В m s l е G “ — » t ц † Ѕ | ж ~ њ ( з ~ . . . . . . . . . . . . . . . . p . N . Ђ . N . ” . N . ¤ . N . ґ . N . . . . . 7 . О . \ Ґ O . . . . . С % м ~ . . . . . . . . . . . . . . . . љ . . . в C @ . $ ‹ N . Ё ¬ У . --------------------------------2012/2/24 16:32:17-------------------------------- This application has leaked memory. The small block leaks are (excluding expected leaks registered by pointer): 21 - 36 bytes: UnicodeString x 1 37 - 52 bytes: UnicodeString x 1 181 - 212 bytes: TPlugin x 1, UnicodeString x 1 Note: Memory leak detail is logged to a text file in the same folder as this application. To disable this memory leak check, undefine "EnableMemoryLeakReporting".Как мы видим, у нас утекло 3 строки и 1 объект класса
TPlugin
.Как правило, более легковесные типы в логе являются "наведёнными" утечками. Т.е. если у вас есть объект с тремя строковыми полями и этот объект утёк, то в утечках появится отчёт о 4-х утечках: объекте и трёх строках. Даже хотя строки сами по себе не утекают: если вы устраните утечку объекта, то это также устранит и "утечку" строк.
По этим соображениям, если у вас в лог-файле есть много утечек - имеет смысл начинать расследование с самых крупных типов данных.
В нашем случае таковым у нас выступает объект класса
TPlugin
. Уже только эта информация сообщает нам первый кусок головоломки: теперь мы знаем, что у нас где-то осталась ссылка на объект типа TPlugin
. В случае если утёк не класс и не строка, вы можете определить эту часть по стеку вызова в лог-файле. В нашем случае стек выглядит так:
4E227F [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugin][259] 4E2553 [PluginAPI\Core\PluginManager.pas][PluginManager][PluginManager.TPluginManager.LoadPlugins][299] 4E7298 [remain.pas][remain][remain.TMainForm.FormCreate][467] 4D3B53 [Forms.pas][Forms][Forms.TCustomForm.DoCreate][3319] 4D3713 [Forms.pas][Forms][Forms.TCustomForm.Create][3189] 4DDF85 [Forms.pas][Forms][Forms.TApplication.CreateForm][9879] 4EFD31 [richedit.dpr][richeditdemo][richeditdemo.richeditdemo][18] 74FB339A [BaseThreadInitThunk]Где строка 259 в LoadPlugin выглядит так:
Result := TPlugin.Create(Self, AFileName);В нашем случае это снова подтверждает нам, что у нас утёк управляющий плагином объект. В других случаях анализ стека даст вам понятие о типе утёкших данных.
Итак, сейчас мы получили представление о том, какого рода данные у нас утекли. Теперь нам нужно их идентифицировать - ведь этих данных у нас в программе много: кто именно их них утёк? Конкретно в нашем случае у нас есть несколько
TPlugin
- по одному на каждый плагин.Чтобы идентифицировать объект мы можем посмотреть на дамп памяти объекта:
Current memory dump of 256 bytes starting at pointer address 7EEC19F0: EC 1D 4E 00 01 00 00 00 78 1C 40 00 3F 54 50 6C 75 67 69 6E 28 44 61 74 65 50 6C 75 67 69 6E 2E 72 65 70 29 3A 20 7B 36 44 43 32 34 34 35 31 2D 36 43 37 33 2D 34 37 45 35 2D 39 33 39 37 2D 42 42 37 34 39 38 46 36 38 36 42 44 7D 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 BC 09 4E 00 F0 F2 F6 7E 9C 18 EC 7E 00 00 C9 02 38 B1 D3 02 94 B1 D3 02 FF FF FF FF 00 00 00 00 51 44 C2 6D 73 6C E5 47 93 97 BB 74 98 F6 86 BD 7C 98 E6 7E 9C 28 E7 7E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 70 1C 4E 00 80 1C 4E 00 94 1C 4E 00 A4 1C 4E 00 B4 1C 4E 00 00 00 00 00 37 08 CE 1A 5C A5 4F 00 00 00 00 00 D1 25 EC 7E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 9A 1B 00 00 E2 43 40 00 24 8B 4E 00 A8 AC D3 02 м . N . . . . . x . @ . ? T P l u g i n ( D a t e P l u g i n . r e p ) : { 6 D C 2 4 4 5 1 - 6 C 7 3 - 4 7 E 5 - 9 3 9 7 - B B 7 4 9 8 F 6 8 6 B D } . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ј . N . р т ц ~ њ . м ~ . . Й . 8 ± У . ” ± У . я я я я . . . . Q D В m s l е G “ — » t ц † Ѕ | ж ~ њ ( з ~ . . . . . . . . . . . . . . . . p . N . Ђ . N . ” . N . ¤ . N . ґ . N . . . . . 7 . О . \ Ґ O . . . . . С % м ~ . . . . . . . . . . . . . . . . љ . . . в C @ . $ ‹ N . Ё ¬ У .Достаточно легко увидеть в нём такой блок:
... T P l u g i n ( D a t e P l u g i n . r e p ) : { 6 D C 2 4 4 5 1 - 6 C 7 3 - 4 7 E 5 - 9 3 9 7 - B B 7 4 9 8 F 6 8 6 B D } ...Или:
TPlugin(DatePlugin.rep): {6DC24451-6C73-47E5-9397-BB7498F686BD}Вот теперь совершенно очевидно, что у нас проблема с плагином DatePlugin.rep (ну, в данном случае он просто оказался загруженным первым).
Откуда в дампе взялась эта строка? А вот откуда:
constructor TPlugin.Create(const APluginManger: TPluginManager; const AFileName: String); ... begin OutputDebugString(PChar('TPlugin.Create: ' + ExtractFileName(AFileName))); SetName(Format('TPlugin(%s)', [ExtractFileName(AFileName)])); inherited Create; ... FID := FPlugin.ID; ... SetName(Format('TPlugin(%s): %s', [ExtractFileName(AFileName), GUIDToString(FID)])); end;Т.е. это отладочное имя, которое мы ввели выше в
TCheckedInterfacedObject
. Сейчас вам должно быть уже понятно, почему в качестве типа данных для FName
была выбрана короткая строка: потому что она статическая - т.е. размещается непосредственно в поле данных. Если бы мы использовали бы динамические строки (String
, AnsiString
, WideString
, UnicodeString
, PChar
, PAnsiChar
, PWideChar
), то вместо содержания строки мы бы увидели в дампе адрес - указатель на данные строки, которые в дамп как раз не попали бы. Не очень полезно, да. Это же объясняет и вызов FillChar
сделано это было, чтобы было удобнее читать строку в дампе. Удобно, когда неиспользуемые данные обнулены, а не заполнены мусором, который мы по ошибке можем принять за строку.Итак, с помощью подсказки FastMM (имя класса), стека вызовов и дампа памяти мы сумели точно идентифицировать проблемный объект: это управляющий класс-обёртка
TPlugin
, созданный для плагина DatePlugin.rep.Хорошо, шаг первый (идентификация ресурса) мы сделали. Теперь шаг два - найти место предполагаемого удаления и выяснить, почему это там не произошло удаления.
Вот тут начинаются более интересные вещи. Если бы у нас был объект или любой другой управляемый вручную тип данных, шаг два был бы тривиален:
O := TSomeObject.Create; ... FreeAndNil(O);Всё, что нам нужно было бы сделать - остановиться на строке "O := TSomeObject.Create" и протрассировать программу по шагам до строки "FreeAndNil(O)", следя не теряем ли мы в пути ссылку и доходим ли мы вообще до строки удаления. Всё.
Но поскольку мы имеем интерфейс (авто-управляемый тип данных), то шаг два будет выглядеть существенно сложнее. Не, место предполагаемого удаления найти не проблема - очевидно, что это вызов
_Release
: ведь объект должен удаляться, когда счётчик ссылок опускается до 0. Вызов деструктора происходит именно внутри _Release
. А проблема тут в том, что _Release
вызывается не только для удаления объекта, но и вообще весьма даже часто - для учёта ссылок. Более того, сам его вызов не имеет значения - важно, насколько парным он будет для вызова _AddRef
. Иными словами, в правильной ситуации у нас должно быть равное число вызовов _AddRef
и _Release
(на каждый _AddRef
приходится вызов _Release
). В неправильной (есть утечка) - один из вызовов _Release
был пропущен. Ситуация с испорченной ссылкой на интерфейс теоретически возможна, но крайне маловероятна - в таком случае мы в 99.99% случаев получим Access Violation в первый же момент вызова _Release
для испорченной ссылки.Итак, в нашем случае шаг два сводится к тому, чтобы определить пропущенный вызов
_Release
.Для новых Delphi сделать это достаточно просто:
- Продублируйте метод
TInterfacedObject._Release
вTCheckedInterfacedObject._Release
. - Установите на
TCheckedInterfacedObject._AddRef
иTCheckedInterfacedObject._Release
две non-breaking точки останова, указав обеим условие "Self.FName = 'TPlugin(DatePlugin.rep): {6DC24451-6C73-47E5-9397-BB7498F686BD}'" (без кавычек, естественно), введя выражение для логгинования "Self.FRefCount" (и снова - без кавычек), а также включив логгирование стека вызовов (можно полностью, а можно только N записей, где N - по вкусу). - Отключите запись всех событий, кроме Breakpoint Messages (и, по вкусу, Output Messages).
- Запустите программу, а после её выполнения сохраните содержимое окна Events в лог-файл.
Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) PluginManager.TPluginManager.LoadPlugins(???,'.rep') remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 2 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) PluginManager.TPluginManager.LoadPlugins(???,'.rep') remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.LoadPlugin('C:\Users\Александр\Documents\RAD Studio\Projects\Plugins\Example5\Plugins\ExportRTF.rep') PluginManager.TPluginManager.LoadPlugins(???,'.rep') remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.BuildFilterList remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.BuildFilter((1820700819, 31407, 20208, (185, 142, 217, 219, 222, 149, 7, 24))) remain.TMainForm.BuildFilterList remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.BuildFilterList remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.BuildFilter((155353976, 13498, 17190, (133, 80, 191, 28, 167, 47, 223, 83))) remain.TMainForm.BuildFilterList remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) PluginManager.TPluginManager.DoLoaded remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 5 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 6 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.PopulatePlugins remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 7 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.PopulatePlugins remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 6 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.PopulatePlugins remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 5 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfCopy(???,???) PluginManager.TPluginManager.GetItem(???) remain.PopulatePlugins remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef remain.TMainForm.FormCreate(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfClear(???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 5 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.TObject.GetInterface((3037498046, 41226, 19417, (170, 23, 3, 17, 50, 111, 225, 166)),(no value)) System.TInterfacedObject.QueryInterface((3037498046, 41226, 19417, (170, 23, 3, 17, 50, 111, 225, 166)),(no value)) PluginManager.TPlugin.QueryInterface((3037498046, 41226, 19417, (170, 23, 3, 17, 50, 111, 225, 166)),(no value)) SysUtils.Supports(Pointer($7EEC1AB8) as IInterface,(3037498046, 41226, 19417, (170, 23, 3, 17, 50, 111, 225, 166)),(no value)) PluginManager.NotifyRelease PluginManager.TPluginManager.UnloadAll remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfClear(???) PluginManager.NotifyRelease PluginManager.TPluginManager.UnloadAll remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 5 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release PluginManager.TPlugin.ReleasePlugin PluginManager.TPlugin.Delete PluginManager.NotifyRelease PluginManager.TPluginManager.UnloadAll remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 4 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfClear(???) PluginManager.TPluginManager.UnloadAll remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3 Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfClear(???) remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 2Во-первых, мы видим, что первая запись в логе начинается с числа 2 - это означает, что было сделано два вызова
_AddRef
, которые не попали в лог - т.е. они были вызваны до установки отладочного имени объекта. Хорошо, учтём это. Теперь считаем, сколько в логе есть вызовов. В этом логе есть 12 (+ 2) вызовов
_AddRef
и 13 вызовов _Release
. Т.е. 14 вызовов _AddRef
и 13 вызовов _Release
. Одного вызова _Release
не хватает. Это же подтверждается счётчиком ссылок: он равен 2 в последней записи лога. Поскольку точка останова стоит на начало метода, то это значение - до выполнения метода _Release
. После его выполнения 2 станет 1. И никаких больше вызовов у нас нет. Т.е. финальное значение счётчика ссылок - 1. Это также подтверждается дампом памяти от FastMM, где счётчик ссылок лежит первым полем:
1C 1E 4E 00 01 00 00 00 ...Здесь 1C 1E 4E 00 - развёрнутый указатель на VMT объекта (т.е. $004E1E1C), а 01 00 00 00 - первое поле (т.е. $00000001 или просто 1).
Итак, мы подтвердили утечку объекта из-за отсутствия одного парного вызова
_Release
к какому-то из _AddRef
. Теперь нам осталось всего-лишь найти этот вызов _AddRef
. Для этого нам нужно изучить лог-файл и каждую запись _AddRef
в нём. Для каждой записи мы должны найти исходный код (к сожалению, в логе нет номеров строк, но, тем не менее, это можно сделать), затем найти парный вызов _Release
к этому месту и проверить его наличие в логе.К примеру, возьмём первые три записи в логе. Мы видим два
_AddRef
и один _Release
. Все они вызваны из системной функции копирования интерфейсов (IntfCopy
), которая в свою очередь вызвана из TPluginManager.LoadPlugins
. Отсюда следует, что в TPluginManager.LoadPlugins
была сделана копия ссылки на интерфейс. Но если вы посмотрите на исходный код TPluginManager.LoadPlugins
, то увидите, что там нет таких мест. Зато там есть вызов TPluginManager.LoadPlugin
. Мы делаем предположение, что вызов TPluginManager.LoadPlugin
не попал в стек вызова. Такое бывает. Посмотрим, согласуется ли это с нашим логом. В TPluginManager.LoadPlugin
есть два места по увеличению ссылок:
- Запись ссылки в
Result
:Result := TPlugin.Create(Self, AFileName);
- Запись ссылки в
FItems
:FItems[FCount] := Result;
TPluginManager.LoadPlugins
:
LoadPlugin(Path + SR.Name); // LoadPlugin - функция, возвращает IPluginИтак, вроде всё согласуется. Будем считать, что наша гипотеза верна и мы реконструировали ситуацию:
LoadPlugin
создаёт плагин, записывает ссылку в Result, который затем будет освобождёт в LoadPlugins
как ненужный. Кроме того, эта ссылка записывается в массив FItems
. Итого: +2 -1 - и у нас осталась ссылка в FItems
.Если вы не можете высказать гипотезу или проверить её - просто включите остановку на точках останова и задайте условия так, чтобы остановиться в нужный момент (на нужной записи). Например, задав Pass Count. После этого выполните пошаговую трассировку, поднимаясь из вызова
_AddRef
или _Release
к вызывающему - это даст вам полную картину происходящего.Так где же должна удаляться ссылка на объект в
FItems
? FItems
удаляется в UnloadAll
. Есть у нас такой вызов? Есть:
Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._Release PluginManager.TPlugin._Release System.@IntfClear(???) PluginManager.TPluginManager.UnloadAll remain.TMainForm.FormDestroy(???) Forms.TCustomForm.DoDestroy Forms.TCustomForm.Destroy Classes.TComponent.DestroyComponents Forms.DoneApplication SysUtils.DoExitProc System.@Halt0 richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3Окей, +2 -2. Т.е. мы можем вычеркнуть из лога эти 4 блока. Останется 10 (+2)
_AddRef
и 11 _Release
.Смотрим дальше. После вычёркивания первые 4 вызова в новом (подправленном) логе являются парными (происходят в
BuildFilterList
/BuildFilter
: +2 -2 - и потому тоже могут быть вычеркнуты. Остаётся 8 (+2) вызовов _AddRef
и 9 вызовов _Release
.Следующие два вызова - снова парные. На этот раз - внутри
DoLoaded
. Вычёркиваем. Осталось 7 (+2) и 8.Дальше идёт 4 вызова
_AddRef
подряд и 4 вызова _Release
подряд. Хотя стек для _AddRef
снова не полон, но стек для _Release
даёт нам подсказку, что это происходит в PopulatePlugins
. И, действительно:
procedure PopulatePlugins; var X: Integer; MI: TMenuItem; begin for X := 0 to Plugins.Count - 1 do begin MI := TMenuItem.Create(MainMenu); MI.Caption := Format('%d: %s', [Plugins[X].Index, Plugins[X].Name]); // раз и два MI.Hint := Format('ID: %s', [GUIDToString(Plugins[X].ID)]); // три MI.Tag := Plugins[X].Index; // четыре MI.OnClick := PluginClick; miLoaded.Add(MI); end; end;Получается, что все вызовы - парные и, значит, могут быть вычеркнуты. Останется 5 (+2) и 6.
Следующий вызов - как раз проблемный:
Breakpoint Call Stack: Helpers.TCheckedInterfacedObject._AddRef PluginManager.TPlugin._AddRef System.@IntfCopy(???,???) Forms.TCustomForm.DoCreate Forms.TCustomForm.Create(???) Forms.TApplication.CreateForm(???,(no value)) richeditdemo.richeditdemo Breakpoint Expression Self.FRefCount: 3Для этого вызова нам не удастся найти не то что пару, а даже место в коде, где мы предполагаем очистить ссылку - что и будет говорить о баге в этом месте.
На практике такое вряд ли у вас будет - вы же будете так делать специально. Гораздо вероятнее такая ситуация: мы нашли место, где планируете освободить ссылку (скажем, в
FormDestroy
), но в логе такого вызова нет. Это означает, что у нас нет пары к этому вызову _AddRef
. Вот вам и проблема. Теперь, всё что вам осталось сделать - пошаговую трассировку, чтобы выяснить почему это был пропущен (гипотетический в данном случае) вызов _Release
в предполагаемом месте.Если вы не уверены насчёт своих размышлений по поводу этого вызова - можете пока оставить его в логе. Просто продолжайте вычёркивать парные записи и дальше, пока у вас не останется три записи - два
_Release
для тех вызовов _AddRef
, что произошли до присвоения отладочного имени. И один вызов _AddRef
, который не имеет пары. Вот вам и проблема (найденная другим способом).Если же у вас старые версии Delphi, то вы не сможете построить вышеуказанный лог. В этом случае просто сделайте точки останова обычными (Break). Запустите программу - и следите за стеком вызовов в окне Call Stack каждый раз, когда вы останавливаетесь на точках останова. Ищите парные вызовы, игнорируйте их, а то, что останется (не парные) - это и будет проблемой.
Вот, собственно, и всё.
Замечу только, что вместо того, чтобы искать утечки памяти "в лоб" - можно попробовать решить проблему пересмотром кода. Иногда это может быть быстрее, иногда - нет. Просто просмотрите свой код на предмет сохраняемых в поля объекта интерфейсный ссылок. Убедитесь, что каждая такая ссылка обнуляется в уведомлении от
IDestroyNotify
. Если нет - добавьте.Access Violation
Итак, Access Violation. Самая популярная ошибка в Delphi программах (и не только Delphi программах). Причина для неё всего одна - вы обратились к памяти, которая недоступна. Но происходить это может из-за совершенно разных ошибок.Во-первых, я замечу, что если вы получаете в программе исключение Access Violation, то в первую очередь вам нужно:
- Включить отладочные опции (в первую очередь - Range Check Errors и Assertions).
- Просмотреть код и убедиться, что вы не нарушаете правила.
- Убедиться, что в коде нет проблем с временем жизни (см. пункт 2 в этой статье).
Неверные сигнатуры
Одна из проблем, специфичная для разработки библиотек - несовпадение сигнатур (прототипов) функций. Например:// DLL: function DoSomething(A: Integer): Integer; stdcall; // .exe: var DoSomething: function(A: Integer): Integer;Как видите, функция объявлена по-разному в DLL и .exe (во втором случае пропущен
stdcall
). Несложно сообразить, что попытка вызвать функцию приведёт к всяческим плохим вещам (хотя и не всегда).К сожалению, подобные ошибки не могут быть пойманы компилятором, потому что они включают в себя две стороны - вас и вторую сторону, которая не является частью проекта.
Чтобы исправить подобную ошибку вам нужно просто внимательно просмотреть свой код и проверить, что все сигнатуры совпадают. Крайне полезно не дублировать объявления, а использовать общие файлы. На эту ошибку стоит грешить в первую очередь, если ваша программа вылетает при вызове внешней функции или непосредственно в момент возврата из неё.
Передача данных
Вторая проблема, специфичная для библиотек - неверное управление памятью. Я подробно пояснял это в отдельной статье. В DLL и в .exe есть свои собственные менеджеры памяти. Это - два разных, раздельных менеджера памяти. Поэтому, память, выделенную в одном исполняемом модуле (DLL/.exe), нельзя освободить в другом модуле (.exe/DLL) и наоборот. А если вы попытаетесь это сделать - вы испортите состояние менеджера памяти, что приведёт к Access Violation, вылетам, порче данных или зависанию программы.В нашей схеме мы легко обходим эту проблему, потому что мы обмениваемся только интерфейсами, для данных мы используем потоки данных или аналогичные обёртки, а для строк у нас используется
WideString
- это специальный тип данных, который всегда использует системный менеджер памяти, так что его можно безболезненно передавать между модулями.Однако, если вы вздумаете передавать в плагин (или, наоборот, в ядро) какие-то данные напрямую - вам лучше бы продумать управление памятью иначе у вас будут происходить плохие вещи.
Ещё раз уточню, что при чтении и записи данных в блок памяти другого модуля проблем нет. Весь вопрос тут заключается только в выделении и высвобождении памяти.
В принципе, менеджер памяти в отладочном режиме должен помочь решить эту проблему. При попытке сделать что-то глупое вроде удаления блока памяти из чужого исполняемого модуля он немедленно возбудит ошибку типа Invalid Pointer Operation, так что вы сможете найти источник проблемы.
Многопоточность
Следующая проблема не специфична для библиотек, да и в нашей системе мы пока с ней не сталкивались. Тем не менее, я упомяну её тут отдельным пунктом, потому что тут есть один не совсем очевидный момент.Итак, если в вашей программе используется несколько потоков и вы получаете совершенно случайные Access Violation в разных, не связанных местах, то ошибки организации многопоточности - первые подозреваемые. При этом проблемы происходят из-за ошибок в синхронизации доступа к общим ресурсам (не обязательно глобальным). Частным случаем этих проблем является неверно установленный режим работы менеджера памяти.
Напомню, что по умолчанию менеджер памяти работает в режиме одного потока. Это значит, что он не выполняет блокировку при доступе к внутренним данным учёта. Такой режим включен по умолчанию по соображениям производительности - большинство программ однопоточны, а отсутствие необходимости выполнять синхронизацию позволяет работать с полной скоростью. Но если вы будете использовать этот режим в многопоточном приложении, то получите проблему одновременного, не синхронизированного доступа двух потоков к глобальным данным учёта. Понятно, что ни к чему хорошему это не приведёт.
Менеджер памяти переводится в многопоточный режим работы (в котором он выполняет дополнительную синхронизацию доступа) использованием
TThread
, BeginThread
или вручную - через IsMultiThreaded
.Проблемы возникнут, если вы забудете это сделать для многопоточного приложения. Особенно коварна эта ситуация тем, если, к примеру, вы в своём плагине не используете потоки, а ваши функции ядро будет вызывать из разных потоков. Таким образом, даже хотя ваш код потоки не создаёт, но в результате вам всё равно нужно использовать многопоточный режим работы.
Вообще, подобные вещи (кто кого и из какого потока вызывает) должны быть прописаны в документации к плагинам. Если это не указано, то следует подразумевать, что вся работа с плагином должна происходить в контексте одного и того же потока. Как правило, так и поступают - просто не указывают требования к потокам, подразумевая именно такое поведение. А там, где поведение иное (скажем, какая-то функция, которую можно вызывать из любого потока) - в документации явно прописывают эти особые условия.
Итак, решение проблемы: просто вставьте
IsMultiThreaded := True
в код инициализации программы и/или плагинов. Чем раньше он будет выполняться - тем лучше.Обработка ошибок в плагинах
Фух, я сказал УЖАСНО МНОГО всего про отладку плагинов и ядра. Но отладка - это лишь часть методов поиска проблем. Было бы неплохо, если бы сама система помогала бы нам в поиске причин ошибок. Особенно это справедливо для случаев, когда отладку применить не получится. Для этого нам необходимо, чтобы система сообщала нам об ошибках, приводя при этом максимум деталей.Давайте посмотрим, что там у нас с обработкой ошибок в программе. Как вы помните из первой части, мы ввели такие правила: всё строим на интерфейсах, а методы интерфейса имеют тип вызова
safecall
.Safecall
Заметим, что исключения не должны пересекать границы модулей. Это связано с тем, что исключение представляется объектом Delphi. Соответственно, программа на, скажем, C++ ничего не знает про объекты Delphi. Соответственно, она не сможет правильно обработать исключение Delphi и правильно освободить ресурсы. Вот почему исключение должно обрабатываться в том же модуле, где оно было возбуждено. За вычетом исключений остаётся только обработка ошибок на базе кодов ошибок, что является достаточно неудобным делом. Практически все проблемы новичков "этот код не работает" сводятся к одну наивному программисту, не умеющему делать правильную обработку ошибок для подхода с кодами ошибок.Итак, в Delphi есть понятие SafeCall-вызова. Он характеризуется тем, что сам компилятор следит за тем, чтобы исключение не вышло за пределы метода в явном виде, а только в виде кода ошибки. Любое исключение, пересекающее границу SafeCall-метода таким образом, иногда называют safecall-исключением. По сути же оно ничем не отличается от остальных исключений. Для примера рассмотрим, например, такой простой объект:
type ETestException = class(Exception); TTestObject = class(TObject) function TestMe: Integer; safecall; end; function TTestObject.TestMe: Integer; safecall; begin raise ETestException.Create('Тестовое исключение.'); Result := -1; end;Здесь мы самым нахальным образом нарушаем требование о том, что метод обязан ловить все исключения.
Но из-за того, что метод помечен как
safecall
, компилятор предпринимает дополнительные действия. Во-первых, несмотря на то, что мы объявили метод как возвращающий значение типа Integer
, компилятор воспринимает его, как возвращающий тип HRESULT
. Посмотрим на скомпилированный код в виде псевдокода:
function TTestObject.TestMe(out AResult: Integer): HResult; stdcall; begin try // Начало кода функции raise ETestException.Create('Тестовое исключение.'); AResult := -1; // Конец кода функции Result := S_OK; except on E: Exception do Result := HandleAutoException(E); end; end;Как видим, кроме модификаций прототипа (заголовка), компилятор обернул тело функции в try-except.
Грубо говоря,
HandleAutoException
делает две вещи: вызывает виртуальную функцию TObject.SafeCallException
и удаляет объект исключения E. Назначение этой функции просто: вы должны конвертировать в ней исключение в код ошибки. Поскольку TObject
ничего не знает о том, как вы хотите обрабатывать исключения, ни о том, какие исключения могут возникнуть в ваших методах, то его умалчиваемая реализация предельно проста:
function TObject.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; begin Result := HResult($8000FFFF); { E_UNEXPECTED } end;Чуть позже мы приведём пример своей реализации этого метода, а пока посмотрим, как работает магия компилятора при вызове SafeCall-метода. Код "I := TestObject.TestMe;" компилятор компилирует в:
CheckAutoResult(TestObject.TestMe(I));Где
CheckAutoResult
проверяет возвращаемое значение и, если оно неуспешно (в смысле HRESULT
), то вызывает функцию SafeCallErrorProc
, а если она не назначена — то возбуждает run-time ошибку номер 229, которая при подключенном SysUtils
преобразуется в ESafeCallException
.Стандартные средства
Итак, использованиеsafecall
означает обработку ошибок на базе исключений, а не кодов ошибок. Откуда следует:
- Вам не нужно вставлять явные проверки успешности вызова;
- Ситуация по умолчанию - реакция на ошибку, а не скрытие;
- По умолчанию исключения всплывают до глобального обработчика, который показывает сообщение об ошибке;
- Вы можете передавать с исключениями дополнительные данные;
- Вы можете использовать наследование;
TObject
исключения в safecall-методах перехватываются и конвертируются в код ошибки E_UNEXPECTED
, на вызывающей стороне при отключенном SysUtils
это приводит к возникновению runtime-ошибки 229:Обработка SafeCall-исключения модулем System |
System
. При подключенном SysUtils
мы получаем ESafeCallException
("Исключение в SafeCall-методе"):Обработка SafeCall-исключения модулем SysUtils |
ComObj
подключается пользовательская процедура SafeCallErrorProc
, которая возбуждает EOleException
, которое, в отличие от ESafeCallException
, уже учитывает некоторую информацию об исключении (напомним, что E_UNEXPECTED
— это ошибка типа "Разрушительный сбой"):Обработка SafeCall-исключения модулем ComObj |
HRESULT
и затем, после пересечения границы, собирается обратно (кстати, учитывайте этот момент, если будете делать трассировку стека для исключений). Просто эта реализация скрыта компилятором. Заметьте, что для этого мы не пишем ни единой строчки кода по управлению исключениями — все действия выполняются автоматически компилятором. Плюс ещё в нагрузку мы получаем возможность использовать функции как функции (из-за того, что HRESULT
скрыт из кода).Но этого всё ещё недостаточно.
Как вы уже поняли, много полезных функций для SafeCall-методов находятся именно в модуле
ComObj
. Например, там есть функция HandleSafeCallException
, позволяющая передать вместе с кодом ещё и дополнительную информацию об исключении. При этом используются стандартные способы ОС, поэтому этой информацией может воспользоваться вызывающий код. К сожалению, доступно только ограниченное количество полей для передачи. Во-первых, это код ошибки. Если возникшее исключение будет класса EOleSysError
, то код ошибки возьмётся из свойства ErrorCode
объекта исключения, для всех прочих исключений это будет E_UNEXPECTED
. Во-вторых, это само сообщение исключения (свойство Message исключения). В-третьих, это GUID объекта, возбудившего исключение. Может быть пустым GUID или вы можете сгенерировать GUID для своего объекта и указать его. Только не забудьте, что GUID должен быть уникальным — не следует использовать один и тот же GUID для двух разных классов. Далее, это идентификатор места возникновения ошибки — произвольная строка (иногда здесь удобно передавать имя класса исключения). И, наконец, имя файла справки, ассоциированного с исключением. Если в вашем приложении есть файл справки с описанием ошибок, то в этом поле должно идти полное имя этого файла справки. Конкретный контекст (раздел справки) берётся из свойства HelpContext
объекта исключения. Несмотря на то, что у HandleSafeCallException
есть параметр ExceptAddr
, в текущей реализации под Windows он не используется. Заметим, что его обычно и не нужно передавать. Дело в том, что исходное исключение всё равно заканчивает свою жизнь на границе метода, поэтому обычно этот адрес лишён смысла для клиентской (вызывающей) стороны.Кроме того, следует сказать, что, чтобы использовать этот механизм в COM-объектах, объект должен ещё реализовывать интерфейс
ISupportErrorInfo
. Вызывая ISupportErrorInfo.InterfaceSupportsErrorInfo
, клиентская сторона может определить, что объект поддерживает дополнительную информацию. Но для обычных объектов (не являющихся COM-объектами) этого делать, разумеется, не обязательно. Ведь достаточно просто указать в документации к своему коду, как его нужно использовать. Например, такие слова: Delphi-программисты — используйте SafeCall; все остальные — используйте IErrorInfo
, который передаётся с использованием функций SetErrorInfo
/GetErrorInfo
.Итак, с учётом сказанного, мы можем дописать наш тестовый пример так:
const TestObjGUID: TGUID = '{9044E2E9-B9D9-4E03-9264-8CB0BFB65FD0}'; type ETestException = class(Exception); TTestObject = class(TObject) function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; override; function TestMe: Integer; safecall; end; function TTestObject.TestMe: Integer; safecall; begin raise ETestException.Create('Тестовое исключение.'); Result := -1; end; function TTestObject.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; begin // Здесь Result - это код ошибки, вы можете вернуть свой код // в зависимости от типа исключения. // HandleSafeCallException релизует стандартное добавление информации к исключению Result := HandleSafeCallException(ExceptObject, ExceptAddr, TestObjGUID, String(ExceptObject.ClassName), '' { файл справки }); end;Теперь при вызове метода TestMe мы получим более подробное сообщение об ошибке (разумеется, только при подключенном модуле
ComObj
):Обработка SafeCall-исключения с дополнительной информацией |
Forms
. Но при этом на вызывающей стороне возбуждается исключение типа EOleException
, у которого заполнены свойства ErrorCode
(для нашего примера это E_UNEXPECTED
), Message
('Тестовое исключение.'), Source
('ETestException'), HelpFile
('') и HelpContext
(0). GUID объекта в нашей реализации никуда не сохраняется.Дополнение стандартных средств
Это - уже гораздо лучше, но всё ещё не достаточно хорошо. Давайте перечислим проблемы:- Невозможно определить тип исключения по коду исключения
- Не совпадают классы исключений на вызываемой (класс исходного исключения) и вызывающей стороне (
EOleException
); - Не работает наследование (следствие из предыдущего пункта);
- Нет передачи дополнительной информации;
- Место возникновения ошибки не идентифицируется;
Далее, я буду предполагать, что все дополнения мы будем вносить в класс
TCheckedInterfacedObject
, который мы ввели в разделе 2 этой статьи выше. Так уж получилось, что этот класс является у нас базой для всех объектов с интерфейсами, так что внеся правки в него, мы дадим нашу дополнительную функциональность всем интерфейсам в программе. Если в коде будут использоваться объекты, реализующие интерфейсы, но не являющиеся наследниками TCheckedInterfacedObject
, то вам нужно будет скопировать наш код в эти классы.Актуальные коды исключений
Итак, реализация по умолчанию всегда возвращает код ошибкиE_UNEXPECTED
- что бесполезно, если мы хотим уметь отличать одно исключение от другого. Простое решение этой проблемы заключается в наследовании наших классов исключений от EOleSysError
. Конструктор класса будет формировать свой собственный код исключения, который затем подцепится стандартным кодом и передастся вызывающему. Альтернативное решение - оставить классы как есть, но переопределить стандартный код, чтобы он строил код исключения по классу. В принципе, эти два подхода можно совместить, что я и сделаю. Мне кажется это удобным, потому что вы сможете использовать однотипный код проверки на коды исключений и в вызываемом и в вызывающем, но при этом вы также будете иметь коды исключений для всех прочих классов.Сразу замечу, что особняком здесь будут стоять системные коды ошибок. В Delphi они представлены
EOSError
.Итак, вопрос - как нам создавать свои собственные коды исключений? Да ещё делать это так, чтобы код существовал для любого наперёд заданного класса.
Напомню, что для "своих" кодов в
HRESULT
предназначен "поставщик" FACILITY_ITF
. Эти коды выделяются программистом, а не системой. Причём эти коды зависят от компонента. Т.е. у вас может быть два класса с одинаковыми кодами исключений. А разными их делает тот факт, что возвращаются они разными классами. Сам источник ошибки идентифицируется по GUID. Для системных кодов ошибок используется нулевой GUID. А под непосредственно код ошибки отводится 16 бит (Word).16 бит - этого достаточно, чтобы разместить в них CRC-код (CRC16). Откуда следует простой алгоритм создания кодов исключений: просто возьмите CRC от имени класса исключения и сохраните полученное значение в HRESULT с
FACILITY_ITF
.Итак, давайте напишем функцию получения кода по исключению:
function HResultFromException(const E: Exception): HRESULT; begin // Базовый класс Exception - кода стандартен if E.ClassType = Exception.ClassType then Result := E_UNEXPECTED else // Для EOleSysError у нас уже есть код if E is EOleSysError then Result := EOleSysError(E).ErrorCode else // Для EOSError код можно получить if E is EOSError then Result := HResultFromWin32(EOSError(E).ErrorCode) else // Для всех прочих - строим код сами Result := MakeResult(SEVERITY_ERROR, FACILITY_ITF, CalcCRC16(E.ClassName)); end;Тогда, базовый класс для всех исключений в системе плагинов будет выглядеть так:
type EBaseException = class(EOleSysError) private function GetDefaultCode: HRESULT; public constructor Create(const Msg: string); constructor CreateFmt(const Msg: string; const Args: array of const); constructor CreateRes(Ident: Integer); overload; constructor CreateRes(ResStringRec: PResStringRec); overload; constructor CreateResFmt(Ident: Integer; const Args: array of const); overload; constructor CreateResFmt(ResStringRec: PResStringRec; const Args: array of const); overload; constructor CreateHelp(const Msg: string; AHelpContext: Integer); constructor CreateFmtHelp(const Msg: string; const Args: array of const; AHelpContext: Integer); constructor CreateResHelp(Ident: Integer; AHelpContext: Integer); overload; constructor CreateResHelp(ResStringRec: PResStringRec; AHelpContext: Integer); overload; constructor CreateResFmtHelp(ResStringRec: PResStringRec; const Args: array of const; AHelpContext: Integer); overload; constructor CreateResFmtHelp(Ident: Integer; const Args: array of const; AHelpContext: Integer); overload; end; ... { EBaseException } function EBaseException.GetDefaultCode: HRESULT; begin Result := MakeResult(SEVERITY_ERROR, FACILITY_ITF, CalcCRC16(ClassName)); end; constructor EBaseException.Create(const Msg: string); begin inherited Create(Msg, GetDefaultCode, 0); end; constructor EBaseException.CreateFmt(const Msg: string; const Args: array of const); begin inherited Create(Format(Msg, Args), GetDefaultCode, 0); end; // ... и т.д.Смысл кода в том, что каждый конструктор теперь устанавливает код исключения, возвращаемый нашей функцией.
Теперь нам нужно во всех наших исходниках заменить строчку "class(Exception)" на "class(EBaseException)", например:
ECheckedInterfacedObjectError = class(EBaseException);Хотя, специальных классов у нас пока что никаких и нет - только стандартные.
Ладно, в любом случае нам осталось добавить код, строящий коды для произвольных классов исключений. Делается это так:
const // GUID, обозначающий "нас" GUID_DefaultErrorSource: TGUID = '{EFA9AA52-4A4E-4007-85D8-5F46CB65C426}'; type TCheckedInterfacedObject = class(TInterfacedObject, IInterface) private ... protected ... public ... function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; override; end; ... function TCheckedInterfacedObject.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; var E: TObject; CreateError: ICreateErrorInfo; ErrorInfo: IErrorInfo; Source: WideString; begin // Значение по умолчанию Result := E_UNEXPECTED; // Получаем информацию E := ExceptObject; if Succeeded(CreateErrorInfo(CreateError)) then begin // Источник - ProgID приложения или класса, вызвавшего ошибку Source := 'pluginsystem.' + ClassName; CreateError.SetSource(PWideChar(Source)); // Дополнительные данные для исключений if E is Exception then begin // Сообщение CreateError.SetDescription(PWideChar(WideString(Exception(E).Message))); // ИД темы в справке CreateError.SetHelpContext(Exception(E).HelpContext); // Путь к справке никак не задаётся, но при желании вы можете сделать: // CreateError.SetHelpFile(PWideChar(WideString(Application.HelpFile))); // Код ошибки Result := HResultFromException(Exception(E)); end; // Для наших ошибок - указываем GUID источника, для всех прочих - пустой if HResultFacility(Result) = FACILITY_ITF then CreateError.SetGUID(GUID_DefaultErrorSource) else CreateError.SetGUID(GUID_NULL); // Устанавливаем настроенные дополнительные данные if CreateError.QueryInterface(IErrorInfo, ErrorInfo) = S_OK then SetErrorInfo(0, ErrorInfo); end; end;Поскольку мы не используем COM, то ProgID и GUID не имеют большого смысла - тем не менее, мы устанавливаем в них значения "похожие на правду". Быть может, они пригодятся вызывающей стороне.
Тогда, если у нас в плагине возбуждается какое-то исключение, то:
// Плагин: raise ECheckedInterfacedObjectDeleteError.Create('Error Message');
// Ядро: try Plugin[X].DoSomething; except on E: Exception do begin if E is EOleException then Application.MessageBox(PChar(Format( 'Класс: %s' + sLineBreak + 'Сообщение: %s' + sLineBreak + 'Код: %s' + sLineBreak + 'Источник (GUID): %s' + sLineBreak + 'Источник (ProgID): %s' + sLineBreak + 'Файл справки: %s' + sLineBreak + 'Номер темы: %d', [E.ClassName, E.Message, IntToHex(EOleException(E).ErrorCode, 8), 'не сохраняется в EOleException', EOleException(E).Source, EOleException(E).HelpFile, EOleException(E).HelpContext])), 'Исключение', MB_OK or MB_ICONERROR) else raise; end; end;
Исключение с custom-кодом, как оно видимо вызывающему |
Чтобы облегчить работу для программистов на других языках, вы можете выписать в заголовочники коды исключений для основных классов. Для этого, во-первых, создайте такую вспомогательную программу:
procedure TForm1.Button1Click(Sender: TObject); begin Label1.Caption := IntToHex(MakeResult(SEVERITY_ERROR, FACILITY_ITF, CalcCRC16(Edit1.Text)), 8); Clipboard.AsText := Label1.Caption; end;Запустите её и вводите в Edit имена классов исключений. А в буфере обмена (и в метке) вы получите их код. Останется только записать это в заголовочник, например:
const E_CheckedInterfacedObjectError = HRESULT($80044383); E_CheckedInterfacedObjectDeleteError = HRESULT($80045A95); E_CheckedInterfacedObjectDoubleFreeError = HRESULT($80048672); E_CheckedInterfacedObjectUseDeletedError = HRESULT($8004D50D);Заметьте, что классы исключений и
TCheckedInterfacedObject
- это вещи, специфичные именно для Delphi. Ими могут воспользоваться программисты на Delphi. Все прочие же их использовать, понятно, не смогут. Но благодаря тому, что мы использовали языко-независимый подход - они смогут работать в рамках нашей системы. В частности, вместо классов исключений и вспомогательной обёртки TCheckedInterfacedObject
программисты на других языках будут использовать коды ошибок в стиле COM (IErrorInfo
/GetErrorInfo
/SetErrorInfo
/HRESULT
) - вот и всё. Просто мы сделали удобнее жизнь программистов Delphi.Примечание: по этой причине в заголовочниках на самом деле вовсе не нужны коды для EChecked-исключений. Это - наши, внутренние исключения, они не имеют смысла для вызывающего. Я показал их только для примера. У нас, пока, нет никаких своих классов исключений, которые имели бы специальный смысл для вызывающего. Вот когда в будущем такие исключения появятся - вот тогда в заголовочники нужно будет вписать коды для них - на манер того, как я это только что показал.
Использование оригинальных классов исключений
Теперь, когда у нас есть уникальные коды ошибок, сделать передачу классов исключений становится очень просто:type OleSysErorClass = class of EOleSysError; OleExceptionClass = class of EOleException; // Наш обработчик safecall-ошибок procedure CustomSafeCallError(ErrorCode: HResult; ErrorAddr: Pointer); // Строит исключение по коду и дополнительной информации function CreateExceptionFromCode(ACode: HRESULT): Exception; var ExceptionClass: ExceptClass; ErrorInfo: IErrorInfo; Source, Description, HelpFile: WideString; HelpContext: Longint; begin // Определяем класс: // а). системные ошибки if HResultFacility(ACode) = FACILITY_WIN32 then ExceptionClass := EOSError else // б). наши ошибки case HRESULT(ErrorCode) of E_CheckedInterfacedObjectError: ExceptionClass := ECheckedInterfacedObjectError; E_CheckedInterfacedObjectDeleteError: ExceptionClass := ECheckedInterfacedObjectDeleteError; E_CheckedInterfacedObjectDoubleFreeError: ExceptionClass := ECheckedInterfacedObjectDoubleFreeError; E_CheckedInterfacedObjectUseDeletedError: ExceptionClass := ECheckedInterfacedObjectUseDeletedError; else // в). все прочие - общий класс ExceptionClass := EOleException; end; // Получаем дополнительную информацию if GetErrorInfo(0, ErrorInfo) = S_OK then begin ErrorInfo.GetSource(Source); ErrorInfo.GetDescription(Description); ErrorInfo.GetHelpFile(HelpFile); ErrorInfo.GetHelpContext(HelpContext); end else begin Source := ''; Description := ''; HelpFile := ''; HelpContext := 0; end; // Создаём объект исключения с информацией if ExceptionClass.InheritsFrom(EOleException) then Result := OleExceptionClass(ExceptionClass).Create(Description, ACode, Source, HelpFile, HelpContext) else if ExceptionClass.InheritsFrom(EOleSysError) then Result := OleSysErorClass(ExceptionClass).Create(Description, ACode, HelpContext) else begin Result := ExceptionClass.Create(Description); if Result is EOSError then EOSError(Result).ErrorCode := HResultCode(ACode); end; end; var E: Exception; begin E := CreateExceptionFromCode(HRESULT(ErrorCode)); raise E at ErrorAddr; end; // Установка и снятие обработчика initialization SafeCallErrorProc := CustomSafeCallError; finalization SafeCallErrorProc := nil; end.И снова: я использовал EChecked-исключения только ради примера. Вам нужно будет помещать в этот список коды и классы исключений, участвующих в вашей системе.
Теперь, предыдущий пример даст нам:
// Плагин: raise ECheckedInterfacedObjectDeleteError.Create('Error Message');
// Ядро: try Plugin[X].DoSomething; except on E: Exception do begin if E is EOleException then Application.MessageBox(PChar(Format( 'Класс: %s' + sLineBreak + 'Сообщение: %s' + sLineBreak + 'Код: %s' + sLineBreak + 'Источник (GUID): %s' + sLineBreak + 'Источник (ProgID): %s' + sLineBreak + 'Файл справки: %s' + sLineBreak + 'Номер темы: %d', [E.ClassName, E.Message, IntToHex(EOleException(E).ErrorCode, 8), 'не сохраняется в EOleException', EOleException(E).Source, EOleException(E).HelpFile, EOleException(E).HelpContext])), 'Исключение', MB_OK or MB_ICONERROR) else if E is EOleSysError then Application.MessageBox(PChar(Format( 'Класс: %s' + sLineBreak + 'Сообщение: %s' + sLineBreak + 'Код: %s', [E.ClassName, E.Message, IntToHex(EOleSysError(E).ErrorCode, 8)])), 'Исключение', MB_OK or MB_ICONERROR) else raise; end; end;
Исключение с custom-классом, как оно видно вызывающему |
Конечно же, набор классов, которые передаются "прозрачно" жёстко зафиксировано в нашем обработчике - это классы из
case
. Но это не является проблемой. Когда вы пишете программу и хотите обрабатывать какой-то класс - вы всегда можете добавить его в case. А если почему-то вам лень это делать - вы всегда сможете обработать исключение как EOleException
.Наследование
Ну, раз уж мы передаём класс исключения, то с наследованием проблемы не возникает - класс эквивалентен источнику, так что вы можете использовать обычные проверки наследования.Дополнительная информация
Возможно, вам захочется передавать с исключением дополнительную информацию, не подходящую для сообщения об ошибке.К сожалению, в стандартном механизме нет поля, предназначенного для дополнительной информации. У вас есть три варианта:
- Не передавать дополнительную информацию
- Реализовать свой аналог
SetErrorInfo
/GetErrorInfo
- Упаковывать информацию в "свободное" поле -
Source
Идентификация точки возбуждения
Проблема этого пункта связана с тем, что в процессе "путешествия" от точки возбуждения к обработчику (от вызываемого к вызывающему) исключение будет удалено и пересоздано. Т.е. фактически, в программе будет два исключения вместо одного - даже хотя для нашего кода это скрыто под капотом языка. В связи с этим и возникает проблема: в обработчике мы увидим лишь второе исключение, которое возбуждается в нашем обработчике 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 нет каких-то классов - просто удалите те строки, которые генерируют ошибку "Неизвестный идентификатор").Читать далее.
Александр, а нет ли у Вас такой возможности (желания) сконвертировать 5 ваших последних постов в pdf с возможностью публичного скачивания? Это было бы реализацией возможности создания "настольного" варианта, т.к. доступ к интернету ограничен корпоративной логикой инф. безопасности.
ОтветитьУдалитьНаверное так и надо будет сделать, но только когда я закончу серию. У меня в планах ещё минимум три поста.
ОтветитьУдалитьГрандиозно! Спасибо!
ОтветитьУдалить+1 за pdf версию.
ОтветитьУдалитьСпрошу заранее: а нужен ли бумажный вариант? Я тут подумал, может имеет смысл в итоге сделать PDF + вариант физического носителя. На каком-нибудь сервисе print-on-demand.
ОтветитьУдалить>а нужен ли бумажный вариант?
ОтветитьУдалитьДа, почему бы и нет.
>Спрошу заранее: а нужен ли бумажный вариант?
ОтветитьУдалитьВ век айпадов? =)
Может лучше, чисто теоретически, задуматься и выпустить через iBooks Author
А когда будет продолжение ? ;)
ОтветитьУдалить"We'll release it when it'll be ready" (C) Blizzard.
ОтветитьУдалитьЕсли вы заметили, то у меня в секции переводов сейчас идёт подготовительный материал.
Александр, спасибо, за эту серию статей. Это лучшее что я видел по данной тематике.
ОтветитьУдалитьРаздел "Обработка ошибок в плагинах" действительно очень полезен, а все что до него - это всего лишь повторение статей в блоге и королевстве. Можно было просто дать ссылки на эти статьи и все. Также хотелось бы увидеть пример с использованием трейсера исключений и передачей дополнительной информации, раз уж об этом зашла речь.
ОтветитьУдалить> Это всего лишь повторение статей в блоге и королевстве
ОтветитьУдалитьНе все читали сей мега-труд, поэтому я привёл адаптированную вырезку. Тем более, что Королевство временами штормит и оно выпадает из поиска Google/Яндекс.
Насчёт трейсера исключений - материал будет. Будет отдельная статья.