Иногда (вроде, когда вам по той или иной причине нужно сохранить обратную совместимость с бинарным dcu) вам может быть необходимым использовать хак или два. Один такой хак заключается в изменении класса объекта в run-time. К примеру, это может понадобится для изменения виртуального, динамического или message-метода.
Vista и мерцание ProgressBar
Вот один из примеров. По какой-то причине (наиболее вероятно - исправление бага) Microsoft изменила поведение элемента управления ProgressBar в Windows Vista, так что он теперь отправляет сообщение
WM_ERASEBKGND
каждый раз, когда изменяется прогресс и элементу нужно себя перерисовать. Компонент TProgressBar
, который является обёрткой к родному контролу, никак не обрабатывает WM_ERASEBKGND
- ни явным переопределением message-метода, ни неявно в общем методе WndProc
(фактически, он даже не замещает WndProc
). Итоговой результат: каждый раз, когда меняется свойство Position
, вы можете увидеть весьма заметное мерцание, потому что компонент сперва полностью очищает фон и лишь затем рисует поверх полосу прогресса в нужном стиле. Jordan Russell сообщил о этой проблеме в Quality Central.Поскольку Delphi 2007 был выпуском Delphi с добавленной поддержкой Windows Vista, ему нужно было исправлять проблемы вроде этой. В обычных условиях вы бы исправили эту проблему простым добавлением обработчика
WM_ERASEBKGND
, который опускает логику по умолчанию (заливка области фоновым цветом):
type TProgressBar = class(TWinControl) private procedure WMEraseBkgnd(var Message: TWmEraseBkgnd); message WM_ERASEBKGND; end; procedure TProgressBar.WMEraseBkgnd(var Message: TWmEraseBkgnd); begin DefaultHandler(Message); end;Но, конечно, в этом случае проблема в том, что Delphi 2007 решено было сделать "non-breaking release" - т.е. все модуля должны были сохранить двоичную совместимость с предыдущей версией Delphi. Иными словами, интерфейсные секции модулей не должны были меняться. Но существует как минимум три разных способа решения этой проблемы. В этой статье мы рассмотрим простейшее и, вероятно, наименее надёжное решение: изменение класса всех экземпляров
TProgressBar
в run-time.Изменение класса
С первого взгляда, это может показаться вам невозможным - изменить класс уже существующего (созданного) экземпляра класса (объекта). Класс экземпляра определяется двумя вещами: типом класса
T
, который использовался при объявлении переменной для экземпляра для хранения объектной ссылки, и типом класса D
, который использовался при объявлении типа в design-time. Тип D
должен быть совместим с T
- т.е. быть его наследником:
type T = class end; D = class(T) end; procedure Foo; var Ref: T; begin Ref := D.Create; end;Хотя вы определённо не можете изменить тип переменной для хранения объектной ссылки (
T
), вы можете изменить в run-time тип экземпляра объекта. Почему это возможно? Ну, тип экземпляра объекта хранится в неявном поле экземпляра объекта в памяти - первые 4 байта экземпляра всегда содержат ссылку на TClass
(реализованную как указатель на VMT класса) класса, который был использован при создании объекта. Это зарезервированное поле инициализируется в классовом методе InitInstance
, определённым в TObject
:
class function TObject.InitInstance(Instance: Pointer): TObject; begin FillChar(Instance^, InstanceSize, 0); PInteger(Instance)^ := Integer(Self);Этот метод выполняет и другие задачи (вроде инициализации всех полей таблицы интерфейсных методов), но нам интересно только то, что до вызова параметр
Instance
содержит мусор (вы можете увидеть, что NewInstance
вызывает GetMem
, а затем InitInstance
). Этот блок памяти сначала очищается нулями вызовом FillChar
(именно это гарантирует нам нули по умолчанию во всех полях объекта) и затем перезаписывает первые 4 байта ссылкой на TClass
(которую можно взять из неявного параметра Self
у не статических классовых методов).Фух! Теперь, когда мы знаем, что де-факто класс экземпляра объекта в run-time явно хранится в поле с известным смещением (0), то изменить класс становится ужасно легко - мы можем просто перезаписать ссылку на
TClass
на другую. Но нам надо быть осторожными, чтобы программа не вылетела (ведь скомпилированный код делает предположения о смещениях полей, индексах виртуальных методов и т.п. - на основании исходного класса объекта) - мы должны перезаписывать класс только на тот, который унаследован от текущего. А поскольку экземпляр уже был создан (с фиксированным размером), мы не можем добавлять никаких дополнительных полей.Давайте посмотрим, как мы можем использовать эту технику в нашем случае с
TProgressBar
. Для начала давайте объявим и реализуем наследника TProgressBar
, который исправляет указанную проблему:
type TProgressBarVistaFix = class(TProgressBar) private procedure WMEraseBkgnd(var Message: TWmEraseBkgnd); message WM_ERASEBKGND; end; procedure TProgressBarVistaFix.WMEraseBkgnd(var Message: TWmEraseBkgnd); begin DefaultHandler(Message); end;Код практически идентичен чистому "interface-breaking" решению выше - мы только изменили имя класса и наследовали его от
TProgressBar
. Теперь нам надо написать код, который берёт существующий экземпляр TProgressBar
и изменяет его класс на TProgressBarVistaFix
:
procedure PatchProgressBar(ProgressBar: TProgressBar); type PClass = ^TClass; begin if ProgressBar.ClassType = TProgressBar then PClass(ProgressBar)^ := TProgressBarVistaFix; end;Заметьте, что код сначала проверяет, является ли класс экземпляра в действительности именно
TProgressBar
, а не чем-то ещё или унаследованным классом - иначе подобная замена привела бы к большому хаосу, поскольку TProgressBarVistaFix
унаследован именно от TProgressBar
и ни от кого иного.Одна из проблем с этой техникой хака - вам нужно явно патчить каждый создаваемый экземпляр. К примеру, вы могли бы делать это в
FormCreate
всех форм, содержащих хотя бы один progress bar. Если же мы являемся CodeGear (или ищем приключений на пятую точку) - мы могли бы применить этот хак в одном-единственном месте: конструкторе TProgressBar
:
constructor TProgressBar.Create(AOwner: TComponent); begin PatchProgressBar(Self); // ... end;Этот хак является полезным, если вам нужно быстро изменить класс экземпляра в run-time без изменения интерфейса. У него, однако, есть несколько недостатков:
- Он требует явного исправления каждого экземпляра индивидуально.
- Он изменяет класс экземпляра. К примеру, в нашем случае
ClassName
будет возвращать 'TProgressBarVistaFix'. Один из обходных путей - переместитьTProgressBarVistaFix
в отдельный модуль и назвать егоTProgressBar
. Но RTTI и классовая ссылка всё равно будут другими (это может иметь значение для кода, который явно проверяет равенство классов). - Хак не исправляет проблему для наследников класса. Но в случае с
TProgressBar
у нас их просто нет (по умолчанию). - Чтобы гарантировать изменение всех экземпляров - вам нужно вмешаться в код конструктора и менять класс в конструкторе.
- Компилятор создаёт новую VMT для хак-класса, что немного увеличивает размер.
Преимуществами же являются:
- Это очень простой хак, в том смысле, что он не требует записи в защищённые области (что потребовало бы использование
VirtualProtect
илиWriteProcessMemory
). - Его очень просто расширить на добавление замещения новых методов в классе (любого числа). Он также может быть использован для подъёма видимости свойств, так что для них будет генерироваться RTTI. Просто обновите хак-класс новыми определениями - и пусть компилятор делает всю работу по генерации новых VMT и RTTI для вас.
- Он гибок, в том смысле, что позволяет изменить лишь один выбранный экземпляр, вместо всех подряд.
Вывод
Представленный здесь хак по изменению класса объектной ссылки в run-time может быть полезен в некоторых частных случаях. Используйте его только для листовых классов и заменяйте класс на совместимый, не добавляющий новых возможностей (новые поля, методы). Для большинства других случае другие техники хака будут более полезны и/или надёжны. Мы посмотрим на некоторые из них в будущих постах. Оставайтесь с нами! ;)
Комментариев нет:
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.