Вероятно многие встречались с таким вот "партизаном" при старте или завершении приложения:
Очень информативное сообщение, сразу понятна причина ошибки, место и способ ее решения :)
Впрочем, если без шуток, что это вообще такое?
Конечно-же это исключение, но ни тип исключения, ни его описание нам не доступны - просто "Runtime error 217" и адрес, а дальше сами...
Если честно, раньше я как-то даже не задумывался по поводу данного исключения, т.к. в моих проектах оно явление редкое, пока однажды у целой череды пользователей не начала воспроизводится именно 217-я ошибка.
Впрочем, даже тогда я не пошел по правильному пути и просто добавил дополнительный уровень логирования в проект, по результатам которого достаточно оперативно нашел причину и исправил ее.
Но, по сути, я просто потратил свое время...
И тратил бы его в дальнейшем, если бы на днях со мной не связался Виктор Федоренков и не рассказал о своих мыслях по поводу ошибки за номером 217.
Без теории нам никуда, иначе можем уткнуться в пределы собственных знаний :)
Поэтому начнем, конечно, с теоретической части.
Для начала я немного освежил мои представления об ошибках в принципе, перечитав часть статьи "Обработка ошибок - глава 1.2.2" за авторством Александра Алексеева, откуда вынес информацию о том, что ошибка 217 будет отображена в том случае, если не инициализирован модуль SysUtils, причем это у Александра проиллюстрированно достаточно наглядно:
На основании данной картинки можно сделать грубый вывод: пока SysUtils жив - все исключения должны отображаться в нормальном виде, о чем идет отдельное упоминание:
Ну что-ж давайте проверим, пишем код, в котором SysUtils должна быть финализирована позже модуля Unit1, в котором искусственно генерируем исключение:
Билдим, запускаем, закрываем форму и... Runtime error 217.
Утверждение о том, что 217 отображается после финализации SysUtils полностью верное, но давайте-ка посмотрим на сам код финализации:
Смотрите что происходит: в процедуре FinalizeUnits вызываются все финализирующие процедуры, адреса которых расположены в массиве InitContext.InitTable^.UnitInfo в том порядке, в котором происходила их инициализация, т.е. самые первые расположены в начале массива (а финализация идет с конца).
Где-то в самом низу расположен и SysUtils + System, ну а мы, с нашим модулем Unit1 где-то в самом верху.
Но вдруг происходит исключение в нашем модуле и "бабах", порядок катарсиса нарушен.
После "бабах" FinalizeUnits вызывается повторно, пропуская наш модуль, вызвавший исключение, вследствие чего разрушается SysUtils и разные, встречающиеся по пути, class destructor-ы, до кучи грохается System с менеджером памяти (сидящий одним из первых в начале списка), после чего идет контрольный выстрел в лоб - RAISE, вот тут-то мы и приплыли - здравствуй 217.
А что если произойдет исключение в секции инициализации любого модуля?
Да все тоже самое:
Делаем вывод: любое необработанное исключение в секциях инициализации или финализации будет приводить к потере описания исключения и приводить к ошибке 217.
На этом с теорией, думаю, закончим.
Имея на руках понимание о причине возникновения Runtime error 217, попробуем получить на руки более привычный нам вариант сообщения об исключении.
В самом начале обсуждения Виктором был предложен достаточно эффективный способ обхода данной ошибки.
Его анализ заключался в следующем: общая инициализация обработчика исключений производится в процедуре InitExceptions модуля SysUtils, а финализация вызовом DoneExceptions.
Если каким либо образом отключить вызов DoneExceptions плюс не дать разрушиться менеджеру памяти, заблокировав вызов блока финализации System - на руки мы получим сообщение об исключении в приемлимом виде.
Как вариант решения был предложен следующий код, который нужно подключить к файлу проекта самым первым модулем (будет работать начиная с D2005 и выше):
Если честно - аплодировал стоя.
Вот он: хак в самом грязном виде как он есть - такие вещи могут делать только те, кто действительно понимает, чем это грозит :)
И данный модуль вывел работу нашего IT отдела примерно на три часа - это была жесткая дискуссия :)
Но, впрочем, давайте разберем логику работы данного кода:
Суть его проста, необходимо выйти на данные о загруженных модулях (включая BPL) в том виде, в котором их понимает Delphi приложение. Это было сделано посредством доступа к началу однонаправленного списка структур TLibModule. Первым элементом списка будет структура, описывающая текущий образ, откуда нам нужно всего-то и получить данные о структуре UnitInfo, которая содержит в себе данные как о количестве инициализированных модулей, так и об адресах их процедур инициализации и финализации в виде записи PackageUnitEntry.
Блокирование финализации модулей происходит посредством присвоения параметру FInit значения nil у каждой записи PackageUnitEntry.
При обниливании данного параметра FinalizeUnits не сможет произвести вызов обработчика и в итоге тот самый raise, о котором я писал выше, сможет достаточно корректно произвести отображение возникшего исключения.
Но вот дальше все сложнее.
Идея здравая и причины понятны, но вот как-же так, ресурсы все-же не освобождены, FastMem перестанет нормально работать (она собирает утечки как раз при финализации), да и совместимости маловато, к примеру, как я и сказал выше, под Delphi 7 данный код вообще работать не сможет.
После первого часа обсуждений в IT отделе мы даже умудрились прийти и к такому выводу: "да и хрен с ними с SysUtils и System - что-то критичного они за собой не несут".
А потом, опять начали спорить - ну не устраивал нас этот подход, вроде все хорошо, но не аккуратненько как-то :)
Рассматривались даже варианты прямого сплайсинга блоков финализации и до кучи деструктора Exception - но дополнительный хак, на уже существующий хак не устраивал вообще никого :)
И тут, сидя в отладчике и прогоняя код по 70-му разу пришла мысля.
Дык эта... а как вообще выводится сообщение о произошедшем исключении? :)
А выводится оно посредством передачи управления на ExceptHandler, в коде которого нет ничего секретного.
А что мы делаем убирая финализацию модулей?
Правильно, заставляем вызваться его-же.
Попробуем-ка проэмулировать вызов ExceptHandler.
Пишем тестовый юнит и подключаем его к проекту самым первым:
Запускаем на выполнение и...
Получилось.
Встроившись в цикл финализации, мы отобразили произошедшее исключение и продолжили финализацию дальше вызовом Halt(1).
В итоге задача решена, грамотно и документировано, и совместимо с Delphi 7, но...
Есть такое понятие, как "наведенные ошибки", т.е. ошибки произошедшие из-за того что перед ними тоже произошла ошибка.
Ну к примеру, функция А, которая должна возвращать экземпляр некоего класса и функция Б, использующая этот экземпляр в работе. К примеру в функции А произошло необработанное исключение (например нет доступа к файлу) и она не создала класс, а потом где-то гораздо позже по коду приложения процедура Б выполняет обращение к этому экземпляру и в итоге происходит Access Violation.
Тоже самое может произойти и в процедурах инициализации/финализации, причем исключение, произошедшее в финализации скроет от нас саму причину.
Для демонстрации напишем вот такой код, в котором при инициализации приложения будет создаваться некий логер, в который будут писаться этапы работы приложения, а в финализации будет писаться что приложение завершило работу.
Для генерации исключения заставим логер создаваться по несуществующему пути:
Мало у кого в системе присутствует диск "А" поэтому результатом этого кода будет либо "Runtime error 216" (именно 216, а не 217), либо, если подключим код из предыдущей главы:
Для того чтобы исправить эту несправедливость, можно немного причесать код и довести его до вот такого состояния:
Здесь идея проста, функция GetNextException по сути повторяет вызов AcquireExceptionObject, но после своего вызова не теряет ссылку на следующее в очереди исключение, а запоминает адрес следующего фрейма во внешней переменной.
После чего все исключения заносятся в список (самое последнее будет первым в списке) и выводятся программисту с соблюдением очередности, в результате чего нам будет сразу понятно, что сначала произошло вот это:
И уже только после него пошли всякие там AV.
Теперь по поводу остальных кодов ошибок.
Почему я начал именно с "Runtime error 217"?
Ну потому что она наиболее легко воспроизводима, а так технически, используя выше приведенный модуль, мы получим на руки вполне нормальное описание всех возможных Runtime ошибок, коих в наличии у нас вон сколько:
Вот таким небрежным кодом, мы можем получить то, о чем нам не хочет говорить ошибка под кодом 217.
Впрочем, я не думаю что этот подход будет незнаком опытным программистам.
Скорее всего это — здравствуй велосипед, ибо вероятнее всего данная проблема кем-то уже решалась ранее, но я просто не знал о данном решении :)
А если нет — значит буду вторым :)
Отдельный респект соавтору и вдохновителю данной статьи - Виктору Федоренкову.
Удачи.
Очень информативное сообщение, сразу понятна причина ошибки, место и способ ее решения :)
Впрочем, если без шуток, что это вообще такое?
Конечно-же это исключение, но ни тип исключения, ни его описание нам не доступны - просто "Runtime error 217" и адрес, а дальше сами...
Если честно, раньше я как-то даже не задумывался по поводу данного исключения, т.к. в моих проектах оно явление редкое, пока однажды у целой череды пользователей не начала воспроизводится именно 217-я ошибка.
Впрочем, даже тогда я не пошел по правильному пути и просто добавил дополнительный уровень логирования в проект, по результатам которого достаточно оперативно нашел причину и исправил ее.
Но, по сути, я просто потратил свое время...
И тратил бы его в дальнейшем, если бы на днях со мной не связался Виктор Федоренков и не рассказал о своих мыслях по поводу ошибки за номером 217.
Теория и анализ проблемы
Без теории нам никуда, иначе можем уткнуться в пределы собственных знаний :)
Поэтому начнем, конечно, с теоретической части.
Для начала я немного освежил мои представления об ошибках в принципе, перечитав часть статьи "Обработка ошибок - глава 1.2.2" за авторством Александра Алексеева, откуда вынес информацию о том, что ошибка 217 будет отображена в том случае, если не инициализирован модуль SysUtils, причем это у Александра проиллюстрированно достаточно наглядно:
На основании данной картинки можно сделать грубый вывод: пока SysUtils жив - все исключения должны отображаться в нормальном виде, о чем идет отдельное упоминание:
Например, если вы видите сообщение о runtime-ошибке, то, судя по приведённой схеме, маловероятно, чтобы ошибка возникла в обработчиках событий на форме. Зато гораздо вероятнее, что она возникает, скажем, в какой-то секции finalization (которая выполняется после секции finalization модуля SysUtils) или в назначенной процедуре ExitProcessProc. Но, разумеется, причина ошибки может сидеть где угодно — в том числе и в упоминаемых обработчиках событий.
Ну что-ж давайте проверим, пишем код, в котором SysUtils должна быть финализирована позже модуля Unit1, в котором искусственно генерируем исключение:
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs; type TForm1 = class(TForm) private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} initialization finalization raise Exception.Create('finalization exception'); end.
Билдим, запускаем, закрываем форму и... Runtime error 217.
Утверждение о том, что 217 отображается после финализации SysUtils полностью верное, но давайте-ка посмотрим на сам код финализации:
procedure FinalizeUnits; ... begin ... Count := InitContext.InitCount; Table := InitContext.InitTable^.UnitInfo; ... try while Count > 0 do begin Dec(Count); InitContext.InitCount := Count; P := Table^[Count].FInit; if Assigned(P) then ... TProc(P)(); ... end; end; except FinalizeUnits; { try to finalize the others } raise; end; end;
Смотрите что происходит: в процедуре FinalizeUnits вызываются все финализирующие процедуры, адреса которых расположены в массиве InitContext.InitTable^.UnitInfo в том порядке, в котором происходила их инициализация, т.е. самые первые расположены в начале массива (а финализация идет с конца).
Где-то в самом низу расположен и SysUtils + System, ну а мы, с нашим модулем Unit1 где-то в самом верху.
Но вдруг происходит исключение в нашем модуле и "бабах", порядок катарсиса нарушен.
После "бабах" FinalizeUnits вызывается повторно, пропуская наш модуль, вызвавший исключение, вследствие чего разрушается SysUtils и разные, встречающиеся по пути, class destructor-ы, до кучи грохается System с менеджером памяти (сидящий одним из первых в начале списка), после чего идет контрольный выстрел в лоб - RAISE, вот тут-то мы и приплыли - здравствуй 217.
А что если произойдет исключение в секции инициализации любого модуля?
Да все тоже самое:
procedure InitUnits; ... begin ... try ... except FinalizeUnits; raise; end; end;
Делаем вывод: любое необработанное исключение в секциях инициализации или финализации будет приводить к потере описания исключения и приводить к ошибке 217.
На этом с теорией, думаю, закончим.
Имея на руках понимание о причине возникновения Runtime error 217, попробуем получить на руки более привычный нам вариант сообщения об исключении.
Отключаем финализацию модулей
В самом начале обсуждения Виктором был предложен достаточно эффективный способ обхода данной ошибки.
Его анализ заключался в следующем: общая инициализация обработчика исключений производится в процедуре InitExceptions модуля SysUtils, а финализация вызовом DoneExceptions.
Если каким либо образом отключить вызов DoneExceptions плюс не дать разрушиться менеджеру памяти, заблокировав вызов блока финализации System - на руки мы получим сообщение об исключении в приемлимом виде.
Как вариант решения был предложен следующий код, который нужно подключить к файлу проекта самым первым модулем (будет работать начиная с D2005 и выше):
unit suShowExceptionsInInitializeSections; interface uses SysUtils; implementation uses Windows; //Получение структуры PackageInfo нашего приложения //В System она находится в переменной InitTable, но не видна из других модулей function GetInitTable: PackageInfo; var Lib: PLibModule; TypeInfo: PPackageTypeInfo; begin Result := nil; Lib := LibModuleList; if not Assigned(Lib) then Exit; //Если загружено несколько модулей (BPL пакетов), то выходим, //я не изучал как работает механизм загрузки/выгрузки BPL, поэтому на всякий //случай выходим if Assigned(Lib^.Next) then Exit; Typeinfo := Lib^.TypeInfo; if Assigned(TypeInfo) then begin //Мы имеем TPackageTypeInfo //Теперь по нему можно получить PackageInfo //Воспользуемся особенностями компилятора. //В IDA видно, что ссылка TypeInfo указывает на середину структуры //PackageInfo программы //Поэтому для того что бы вычислить PackageInfo нужно вычесть из адреса //TypeInfo смещение этого поля Result := PackageInfo(PByte(TypeInfo) - (LongWord(@PackageInfoTable(nil^).TypeInfo))); end; end; //Отключить секцию финализации для всех модулей procedure DisableAllFinalization; var Loop: Integer; OldProtect: LongWord; InitTable: PackageInfo; Table: PUnitEntryTable; begin InitTable := GetInitTable; if Assigned(InitTable) then begin Table := InitTable^.UnitInfo; if Assigned(Table) then //Разрешаем изменять структуру в которой хранятся ссылки на инициализаю/финализацию всех юнитов if VirtualProtect(Table, SizeOf(PackageUnitEntry) * InitTable^.UnitCount, PAGE_READWRITE, OldProtect) then for Loop := 0 to InitTable^.UnitCount - 1 do Table^[Loop].FInit := nil; end; end; initialization finalization //Сейчас идет финализация всех модулей, модуль SysUtils создан раньше, поэтому //он еще не финализирован. Наша задача здесь не дать ему финализироваться, //Как и другим модулям которые он использует (интересует только System), //это нужно для правильной отработки обработчиков исключений. //Сюда мы можем попасть по двум причинам //1. Произошел Exception во время инициализации каком-то модуля //2. Нормальное завершение программы // //Мы не будем определять причину, так как процесс все равно завершается, а ОС //сама освободит занятые ресурсы после смерти процесса. //Но нужно иметь ввиду, данную технику использовать в DLL нельзя, что бы не //допускать утечек памяти if IsLibrary then Exit; //Мы не можем выборочно заблокировать финализацию юнитов по их имени //так как нет соответствующих данных в RTTI. Тем не менее, мы можем отключить //финализацию всех юнитов, которые идут в списке до этого //модуля. Таким образом если данный модуль расположить первым в DPR файле, //то мы минимизируем утечки. //Вычислять адрес процедуры финализации данного юнита не обязательно, //ведь к моменту выполнения данного кода уже финализированы все следующие юниты. //Поэтому просто заблокируем финализцию всех оставшихся DisableAllFinalization; end.
Если честно - аплодировал стоя.
Вот он: хак в самом грязном виде как он есть - такие вещи могут делать только те, кто действительно понимает, чем это грозит :)
И данный модуль вывел работу нашего IT отдела примерно на три часа - это была жесткая дискуссия :)
Но, впрочем, давайте разберем логику работы данного кода:
Суть его проста, необходимо выйти на данные о загруженных модулях (включая BPL) в том виде, в котором их понимает Delphi приложение. Это было сделано посредством доступа к началу однонаправленного списка структур TLibModule. Первым элементом списка будет структура, описывающая текущий образ, откуда нам нужно всего-то и получить данные о структуре UnitInfo, которая содержит в себе данные как о количестве инициализированных модулей, так и об адресах их процедур инициализации и финализации в виде записи PackageUnitEntry.
Блокирование финализации модулей происходит посредством присвоения параметру FInit значения nil у каждой записи PackageUnitEntry.
При обниливании данного параметра FinalizeUnits не сможет произвести вызов обработчика и в итоге тот самый raise, о котором я писал выше, сможет достаточно корректно произвести отображение возникшего исключения.
Но вот дальше все сложнее.
Пытаемся причесать хорошую мысль
Идея здравая и причины понятны, но вот как-же так, ресурсы все-же не освобождены, FastMem перестанет нормально работать (она собирает утечки как раз при финализации), да и совместимости маловато, к примеру, как я и сказал выше, под Delphi 7 данный код вообще работать не сможет.
После первого часа обсуждений в IT отделе мы даже умудрились прийти и к такому выводу: "да и хрен с ними с SysUtils и System - что-то критичного они за собой не несут".
А потом, опять начали спорить - ну не устраивал нас этот подход, вроде все хорошо, но не аккуратненько как-то :)
Рассматривались даже варианты прямого сплайсинга блоков финализации и до кучи деструктора Exception - но дополнительный хак, на уже существующий хак не устраивал вообще никого :)
И тут, сидя в отладчике и прогоняя код по 70-му разу пришла мысля.
Дык эта... а как вообще выводится сообщение о произошедшем исключении? :)
А выводится оно посредством передачи управления на ExceptHandler, в коде которого нет ничего секретного.
А что мы делаем убирая финализацию модулей?
Правильно, заставляем вызваться его-же.
Попробуем-ка проэмулировать вызов ExceptHandler.
Пишем тестовый юнит и подключаем его к проекту самым первым:
unit Test; interface uses SysUtils; var E: Exception; implementation initialization finalization E := AcquireExceptionObject; if E <> nil then begin ShowException(E, ExceptAddr); E.Free; Halt(1); end; end.
Запускаем на выполнение и...
Получилось.
Встроившись в цикл финализации, мы отобразили произошедшее исключение и продолжили финализацию дальше вызовом Halt(1).
В итоге задача решена, грамотно и документировано, и совместимо с Delphi 7, но...
А не развить ли идею?
Есть такое понятие, как "наведенные ошибки", т.е. ошибки произошедшие из-за того что перед ними тоже произошла ошибка.
Ну к примеру, функция А, которая должна возвращать экземпляр некоего класса и функция Б, использующая этот экземпляр в работе. К примеру в функции А произошло необработанное исключение (например нет доступа к файлу) и она не создала класс, а потом где-то гораздо позже по коду приложения процедура Б выполняет обращение к этому экземпляру и в итоге происходит Access Violation.
Тоже самое может произойти и в процедурах инициализации/финализации, причем исключение, произошедшее в финализации скроет от нас саму причину.
Для демонстрации напишем вот такой код, в котором при инициализации приложения будет создаваться некий логер, в который будут писаться этапы работы приложения, а в финализации будет писаться что приложение завершило работу.
Для генерации исключения заставим логер создаваться по несуществующему пути:
uses Classes; var Logger: TFileStream; const StartLog: AnsiString = 'Начало работы приложения' + sLineBreak; EndLog: AnsiString = 'Работа приложения завершена' + sLineBreak; implementation initialization Logger := TFileStream.Create('A:\MyLog,txt', fmCreate); Logger.WriteBuffer(StartLog[1], Length(StartLog)); finalization Logger.WriteBuffer(EndLog[1], Length(EndLog)); Logger.Free; end.
Мало у кого в системе присутствует диск "А" поэтому результатом этого кода будет либо "Runtime error 216" (именно 216, а не 217), либо, если подключим код из предыдущей главы:
Exception EAccessViolation in module Project2.exe at 001B1593.А ведь причина то кроется в самом первом исключении, которое нами не отображается и с наскока разобраться в причине ошибки не получится.
Access violation at address 005B1593 in module 'Project2.exe'. Read of address 00000000.
Для того чтобы исправить эту несправедливость, можно немного причесать код и довести его до вот такого состояния:
unit ShowExceptSample; interface uses SysUtils, Classes; implementation type PRaiseFrame = ^TRaiseFrame; TRaiseFrame = packed record NextRaise: PRaiseFrame; ExceptAddr: Pointer; ExceptObject: TObject; ExceptionRecord: PExceptionRecord; end; var // Указатель на вершину списка исключений CurrentRaiseList: Pointer = nil; // Функция возвращяет текущее исключение со стека function GetNextException: Pointer; begin if CurrentRaiseList = nil then CurrentRaiseList := RaiseList; if CurrentRaiseList <> nil then begin Result := PRaiseFrame(CurrentRaiseList)^.ExceptObject; PRaiseFrame(CurrentRaiseList)^.ExceptObject := nil; CurrentRaiseList := PRaiseFrame(CurrentRaiseList)^.NextRaise; end else Result := nil; end; var ExceptionStack: TList; E: Exception; initialization finalization // Смотрим, есть ли вообще исключения? E := GetNextException; if E <> nil then begin ExceptionStack := TList.Create; try // если есть, собираем о них информацию while E <> nil do begin ExceptionStack.Add(E); E := GetNextException; end; // и отображаем их в том порядке, в котором они произошли while ExceptionStack.Count > 0 do begin E := ExceptionStack[ExceptionStack.Count - 1]; ExceptionStack.Delete(ExceptionStack.Count - 1); ShowException(E, ExceptAddr); E.Free; end; finally ExceptionStack.Free; end; // финализируем все что осталось Halt(1); end; end.
Здесь идея проста, функция GetNextException по сути повторяет вызов AcquireExceptionObject, но после своего вызова не теряет ссылку на следующее в очереди исключение, а запоминает адрес следующего фрейма во внешней переменной.
После чего все исключения заносятся в список (самое последнее будет первым в списке) и выводятся программисту с соблюдением очередности, в результате чего нам будет сразу понятно, что сначала произошло вот это:
И уже только после него пошли всякие там AV.
Теперь по поводу остальных кодов ошибок.
Почему я начал именно с "Runtime error 217"?
Ну потому что она наиболее легко воспроизводима, а так технически, используя выше приведенный модуль, мы получим на руки вполне нормальное описание всех возможных Runtime ошибок, коих в наличии у нас вон сколько:
reMap: array [TRunTimeError] of Byte = ( 0, { reNone } 203, { reOutOfMemory } 204, { reInvalidPtr } 200, { reDivByZero } 201, { reRangeError } { 210 Abstract error } 215, { reIntOverflow } 207, { reInvalidOp } 200, { reZeroDivide } 205, { reOverflow } 206, { reUnderflow } 219, { reInvalidCast } 216, { reAccessViolation } 218, { rePrivInstruction } 217, { reControlBreak } 202, { reStackOverflow } 220, { reVarTypeCast } 221, { reVarInvalidOp } 222, { reVarDispatch } 223, { reVarArrayCreate } 224, { reVarNotArray } 225, { reVarArrayBounds } { 226 Thread init failure } 227, { reAssertionFailed } 0, { reExternalException not used here; in SysUtils } 228, { reIntfCastError } 229, { reSafeCallError } 235, { reMonitorNotLocked } 236 { reNoMonitorSupport } {$IFDEF PC_MAPPED_EXCEPTIONS} { 230 Reserved by the compiler for unhandled exceptions } {$ENDIF PC_MAPPED_EXCEPTIONS} {$IF defined(PC_MAPPED_EXCEPTIONS) or defined(STACK_BASED_EXCEPTIONS)} { 231 Too many nested exceptions } {$ENDIF} {$IF Defined(LINUX) or Defined(MACOS)} { 232 Fatal signal raised on a non-Delphi thread } , 233 { reQuit } {$ENDIF LINUX or MACOS} {$IFDEF POSIX} , 234 { reCodesetConversion } {$ENDIF POSIX} , 237, { rePlatformNotImplemented } 238 { reObjectDisposed } );
Итог
Вот таким небрежным кодом, мы можем получить то, о чем нам не хочет говорить ошибка под кодом 217.
Впрочем, я не думаю что этот подход будет незнаком опытным программистам.
Скорее всего это — здравствуй велосипед, ибо вероятнее всего данная проблема кем-то уже решалась ранее, но я просто не знал о данном решении :)
А если нет — значит буду вторым :)
Отдельный респект соавтору и вдохновителю данной статьи - Виктору Федоренкову.
Удачи.
---
© Виктор Федоренков
© Александр (Rouse_) Багель
Январь, 2014
Круто! Большое спасибо за отличную статью! Обязательно возьму ваш модуль ShowExceptSample на вооружение. :)
ОтветитьУдалитьЭто всегда пожалуйста :)
УдалитьСпасибо за статью. Было бы еще здорово если бы такие вещи сами разработчики Delphi делали. Все таки показ последней ошибки с описанием ее гораздо лучше воспринимается чем невнятные Error.
ОтветитьУдалитьСпасибо за статью, отличный материал...
ОтветитьУдалитьСупер.
ОтветитьУдалитьАплодировал сидя.
ОтветитьУдалить