воскресенье, 24 июля 2011 г.

Published поля

Это перевод Published fields. Автор: Hallvard Vassbotn.

В нашей небольшой серии по обратной разработке (reverse engineering) недокументированных полей VMT классов Delphi, мы видели поле FieldTable. Это поле указывает на структуры, которые описывают published поля класса. В Delphi published поля должны быть ссылками на объекты и в основном используются формами (form) и модулями данных (datamodule) для хранения ссылок на свои компоненты в логически именованных полях для удобства доступа (потому что альтернативой к полям является использование массива Components, что включает в себя поиск и преобразования типов).

Delphi RTL содержит всего один общедоступный метод, который получает доступ к таблице полей: TObject.FieldAddress. Этот метод возвращает адрес published поля по заданному имени:
function TObject.FieldAddress(const Name: ShortString): Pointer;
asm
        // ...
        MOV     ESI,[EAX].vmtFieldTable
        // ...
end;
Этот метод использует компонентной подсистемой в реализации private метода TComponent.SetReference, чтобы найти поле у владельца компонента, которое соответствует имени компонента. Если компонент находит правильно названное поле у своего владельца, то он присваивает себя в это поле (или записывает nil, если компонент удаляется):
procedure TComponent.SetReference(Enable: Boolean);
var
  Field: ^TComponent;
begin
  if FOwner <> nil then
  begin
    Field := FOwner.FieldAddress(FName);
    if Field <> nil then
      if Enable then Field^ := Self else Field^ := nil;
  end;
end;

procedure TComponent.InsertComponent(AComponent: TComponent);
begin
  // …
  AComponent.SetReference(True);
  // …
end;

procedure TComponent.RemoveComponent(AComponent: TComponent);
begin
  // …
  AComponent.SetReference(False);
  // …
end;

procedure TComponent.SetName(const NewName: TComponentName);
begin
  // …
    SetReference(False);
    ChangeName(NewName);
    SetReference(True);
  // …
end;
Вот как компонентные поля вашей формы автоматически получают свои значения. И если вы освобождаете компонент в run-time, соответствующее поле автоматически устанавливается в nil. Здорово, да?! :-)

Мы скоро посмотрим на структуру таблицы полей, а сейчас просто скажем, что таблица полей должна содержать имя каждого поля и смещение поля в экземпляре объекта, чтобы FieldAddress могла работать. Заметьте, что таблица полей является частью VMT и, значит, частью класса, а не отдельного экземпляра объекта. Вот почему таблица полей не может содержать прямой адрес поля, а только смещение - потому что адрес будет своим у каждого конкретного экземпляра класса (объекта). Смещение нужно скомбинировать с адресом объекта (добавить), чтобы получить реальный адрес поля в run-time.

Есть и другой, закрытый, метод, который также обращается к таблице полей. Модуль Classes содержит подпрограмму BASM в своей секции implementation, называемую GetFieldClassTable. Эта подпрограмма получает доступ к другой части таблицы полей - той, что содержит ссылки на классы:
function GetFieldClassTable(AClass: TClass): PFieldClassTable; 
asm
        MOV     EAX,[EAX].vmtFieldTable
        // …
end;
Эта подпрограмма является частью самой низкоуровневой внутренней части реализации сериализации компонентов в TReader. Вложенные вызовы, которые приводят к вызову GetFieldClassTable, начинаются с вызова общедоступного метода TReader.ReadComponent. А всё дерево вызовов выглядит примерно так:
TReader.ReadComponent
     CreateComponent (вложенная подпрограмма)
          FindComponentClass
               GetFieldClass
                    GetFieldClassTable
     FindExistingComponent (вложенная подпрограмма)
          FindComponentClass
               GetFieldClass
                    GetFieldClassTable
Логика FindExistingComponent обрабатывает формы с визуальным наследованием, модуля данных (datamodule) и фреймы (frame). CreateComponent создаёт новый компонент по описанию из DFM потока по строке имени класса компонента. FindComponentClass работает как локальное проецирование строк имён классов на ссылки на классы TClass. Вместо использования безотказной глобальной подпрограммы GetClass, которая используется Delphi в design-time, FindComponentClass сначала ограничивает свой поиск до уникального списка типов классов объявленных published полей. Видите ли, в дополнение к ассоциации имя-смещение таблица полей также содержит список всех типов классов, используемых published полями в классе.

Когда вы проектируете форму, то у вас есть малоизвестная возможность просто удалить поле компонента из формы, если вы никогда не обращаетесь к нему из кода. Кроме того, вы можете просто очистить свойство Name компонента. Это превратит компонент в безымянный и IDE сама удалит объявление поля. Всё это позволяет немного уменьшить размер DFM и немного улучшить производительность загрузки в run-time.

Но вам нужно быть осторожным с этим трюков. Если вы удалите все поля, имеющие тип определённого класса, загрузка компонента из DFM в run-time будет неудачной, вызывая ошибку вроде такой:
---------------------------
Debugger Exception Notification
---------------------------
Project richedit.exe raised exception class EClassNotFound with message 'Class TLabel not found'. Process stopped. Use Step or Run to continue.
---------------------------
OK   Help   
---------------------------
Или (вне отладчика):
---------------------------
Rich Edit Control Demo
---------------------------
Class TLabel not found.
---------------------------
OK   
---------------------------
Вы уже должны понимать, почему вы видите эту ошибку. Класс TReader использует список published полей, чтобы получить список классов компонентов по имени класса, чтобы можно было сконвертировать имя класса из DFM в ссылку на реальный класс TComponent. Если ссылка на какой-то класс не представлена в таблице полей класса, TReader не сможет создать компонент, так что он просто возбуждает исключение EClassNotFound, которое вы и видите выше. Заметьте, что TReader также откатывается к использованию (более медленной) функции GetClass, прежде чем окончательно возбудить исключение. Это означает, что если вы удаляете все published поля определённого класса, вы должны вызвать RegisterClass на этот класс в initialization секции своего проекта:
//…
initialization 
  RegisterClass(TLabel);
end.
И тогда вы можете безболезненно удалить все поля класса TLabel из класса формы.

Ok, этот рассказ должен дать вам основное представление о том, почему в Delphi есть published поля, что за RTTI информация хранится вместе с ними и как VCL её использует для выполнения магии сериализации DFM в design-time и run-time. Ассемблерный код TObject.FieldAddress и GetFieldClassTable вместе с полезными объявлениями типов модуля Classes дают нам несколько полезных подсказок к раскладке таблицы полей в памяти (структуре RTTI таблицы полей).

В следующем посте мы познакомимся поближе со структурой таблицы полей и напишем Pascal-код и вспомогательные методы для работы с ней. Следите за обновлениями!

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

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