В предыдущей статье мы посмотрели на то, как published поля используются IDE и VCL, чтобы сделать простыми работу с компонентными ссылками и нахождение классовых ссылок по именам типов. Сейчас мы заглянем глубже в детали реализации published полей.
Начав с анализа ассемблерного кода
TObject.FieldAddress, мне удалось реконструировать примерный вид внутренних структур на Pascal-е:
type
TPublishedField = packed record
Offset: Integer;
Filler: word; // ??
Name: {packed} Shortstring; // в действительности string[Length(Name)]
end;
PPft = ^TPft;
TPft = packed record
Count: Word;
Filler: LongWord; // ??
Fields: array[0..High(Word)-1] of TPublishedField; // в действительности [0..Count-1]
end;
PVmt = ^TVmt;
TVmt = packed record
// ...
FieldTable : PPft;
// ...
end;
Поле FieldTable в записи TVmt, которую мы разбирали ранее, теперь стало более конкретизированным: типа PPft - указателем на таблицу published полей. Pft начинается с 2-байтового поля - счётчика элементов, за которыми идёт 4 неизвестных мне байта (они пропускаются функцией TObject.FieldAddress), после которых расположен массив переменной длины из записей типа TPublisedField. Как и в прочих структурах RTTI, поля типа ShortString упакованы до их реальной длины, поэтому все записи имеют переменный размер. Запись TPublishedField включает в себя поле Offset - смещение поля от начала экземпляра объекта, 2 байта неизвестных данных, а также ShortString с именем класса поля. Мы скоро разберёмся с этими неизвестными данными.К счастью, функция
GetFieldClassTable в секции implementation модуля Classes (которую мы обсуждали в предыдущем посте), явно указывает, что поле Filler записи TPft указывает на список классов. Имея на руках эту информацию, мы можем обновить наши определения выше:
type
PClass = ^TClass;
PPublishedFieldTypes = ^TPublishedFieldTypes;
TPublishedFieldTypes = packed record
TypeCount: word;
Types: array[0..High(Word)-1] of PClass; // на самом деле: [0..TypeCount-1]
end;
TPft = packed record
Count: Word;
FieldTypes: PPublishedFieldTypes;
Fields: TPublishedFields; // на самом деле: [0..Count-1]
end;
Теперь мы идентифицировали поле FieldTypes, которое указывает на запись со счётчиком TypeCount и массивом ссылок на классы. Заметьте, что ссылки на классы имеют дополнительный уровень косвенности. TClass сам по себе является указателем, но массив, фактически, содержит указатели, которые указывают на ссылки TClass. Причина подобной конструкции "ссылка на ссылку на класс" заключается в поддержке RTTI информации и VMT TClass, которые находятся в другом исполняемом модуле (случай с пакетами - BPL packages). Мы видим, что подобное же косвенное указание через дополнительный указатель используется в модуле TypInfo для указателей PTypeInfo, в реализации глобальных переменных, а также полях InstanceSize и Parent TVmt. Код поддержки пакетов в Delphi, генерируемый компоновщиком (linker) автоматически исправляет эти указатели после загрузки статически связанных пакетов.У нас пока осталось неизвестное поле в записи
TPublishedField. Когда я только начал писать тестовый код и делать дамп таблицы полей из выбранных классов тестового кода, мне показалось, что там использовалась последовательная индексация с нуля. Но когда я добавил ещё одно published поле типа TObject, нового элемента не появилось. Хммм. Добавив сюда отсутствующую связь с массивом FieldTypes, я быстро сообразил, что неизвестное поле в TPublishedField было индексом типа в массиве ссылок на классы.Это подтверждается тем, что массив
FieldTypes содержит только уникальные ссылки на классы. Если у вас есть 10 published полей типа TLabel, у вас будет только одна ссылка на TLabel в массиве FieldTypes. Для больших форм, где есть много компонентов, но нет разнообразия их типов, это экономит немного места в записи TPublishedField - каждый индекс занимает 2 байта, а ссылка на TClass - 4. Что более важно, массив FieldTypes теперь может быть использован, чтобы быстро транслировать имя в ссылку, не тратя время на сканирование таблицы.После опознания назначения всех полей, у нас теперь есть такие объявления типов:
type
PClass = ^TClass;
PPublishedField = ^TPublishedField;
TPublishedField = packed record
Offset: Integer;
TypeIndex: word; // Индекс в массиве FieldTypes ниже
Name: {packed} Shortstring; // string[Length(Name)]
end;
PPublishedFieldTypes = ^TPublishedFieldTypes;
TPublishedFieldTypes = packed record
TypeCount: word;
Types: array[0..High(Word)-1] of PClass; // [0..TypeCount-1]
end;
TPublishedFields = packed array[0..High(Word)-1] of TPublishedField;
PPft = ^TPft;
TPft = packed record
Count: Word;
FieldTypes: PPublishedFieldTypes;
Fields: TPublishedFields; // [0..Count-1]
end;
Не считая массива FieldTypes и поля TypeIndex, эти структуры выглядят весьма похоже на RTTI структуры published методов. Чтобы быстро собрать подпрограммы-утилиты для работы с этими структурами, я использовал народные методы copy-and-paste и search-and-replace:
function GetPft(AClass: TClass): PPft;
var
Vmt: PVmt;
begin
Vmt := GetVmt(AClass);
if Assigned(Vmt) then
Result := Vmt.FieldTable
else
Result := nil;
end;
function GetPublishedFieldCount(AClass: TClass): integer;
var
Pft: PPft;
begin
Pft := GetPft(AClass);
if Assigned(Pft) then
Result := Pft.Count
else
Result := 0;
end;
Загадочно названная функция GetPft возвращает указатель на таблицу published полей по данной ей ссылке на класс. Она использует функцию GetVmt для получения указателя на "волшебную" часть таблицы виртуальных методов (VMT), а затем просто возвращает значение поля FieldTable. Функция GetPublishedFieldCount возвращает число published полей по ссылке на класс (не считая полей предков класса).Функции для итерации по таблице published полей класса, используя как индексный доступ, так и итераторы, также конвертируются без проблем:
function GetNextPublishedField(AClass: TClass; PublishedField: PPublishedField): PPublishedField;
begin
Result := PublishedField;
if Assigned(Result) then
Inc(PChar(Result), SizeOf(Result.Offset)
+ SizeOf(Result.TypeIndex)
+ SizeOf(Result.Name[0])
+ Length(Result.Name));
end;
function GetPublishedField(AClass: TClass; TypeIndex: integer): PPublishedField;
var
Pft: PPft;
begin
Pft := GetPft(AClass);
if Assigned(Pft) and (TypeIndex < Pft.Count) then
begin
Result := @Pft.Fields[0];
while TypeIndex > 0 do
begin
Result := GetNextPublishedField(AClass, Result);
Dec(TypeIndex);
end;
end
else
Result := nil;
end;
function GetFirstPublishedField(AClass: TClass): PPublishedField;
begin
Result := GetPublishedField(AClass, 0);
end;
Единственное отличие здесь - запись TPublishedField не содержит явного поля размера (как это было в случае с TPublishedMethod). Вместо этого нам приходится самим вычислять размер элемента, путём сложения размера фиксированной части с переменным размером поля имени, чтобы перемещать текущую позицию к следующей записи в массиве. И снова, как и ранее, ответственность за нужное количество вызовов GetNextPublishedField лежит на вызывающем (используя GetPublishedFieldCount).Затем мы вводим подпрограммы поиска, которые находит заданное published поле по признакам вроде имени поля, смещению поля или его адресу. Эти подпрограммы используют функции итерации, приведённые выше. При успехе они возвращают указатель на соответствующую запись
TPublishedField из RTTI информации, при ошибке же они возвращают nil:
function FindPublishedFieldByName(AClass: TClass; const AName: ShortString): PPublishedField;
var
i : integer;
begin
while Assigned(AClass) do
begin
Result := GetFirstPublishedField(AClass);
for i := 0 to GetPublishedFieldCount(AClass) - 1 do
begin
// Примечание: Length(ShortString) разворачивается в эффективный inline код
if (Length(Result.Name) = Length(AName)) and
(StrLIComp(@Result.Name[1], @AName[1], Length(AName)) = 0) then
Exit;
Result := GetNextPublishedField(AClass, Result);
end;
AClass := AClass.ClassParent;
end;
Result := nil;
end;
function FindPublishedFieldByOffset(AClass: TClass; AOffset: Integer): PPublishedField;
var
i : integer;
begin
while Assigned(AClass) do
begin
Result := GetFirstPublishedField(AClass);
for i := 0 to GetPublishedFieldCount(AClass) - 1 do
begin
if Result.Offset = AOffset then
Exit;
Result := GetNextPublishedField(AClass, Result);
end;
AClass := AClass.ClassParent;
end;
Result := nil;
end;
function FindPublishedFieldByAddr(Instance: TObject; AAddr: Pointer): PPublishedField;
begin
Result := FindPublishedFieldByOffset(Instance.ClassType, PChar(AAddr) - PChar(Instance));
end;
Прямая работа с записями TPublishedField достаточно трудоёмка, поэтому я также написал несколько функций-оболочек (wrappers), которые просто возвращают информацию напрямую по данной им ссылке на класс:
function FindPublishedFieldOffset(AClass: TClass; const AName: ShortString): integer;
var
Field: PPublishedField;
begin
Field := FindPublishedFieldByName(AClass, AName);
if Assigned(Field) then
Result := Field.Offset
else
Result := -1;
end;
function FindPublishedFieldAddr(Instance: TObject; const AName: ShortString): PObject;
var
Offset: integer;
begin
Offset := FindPublishedFieldOffset(Instance.ClassType, AName);
if Offset >= 0 then
Result := PObject(PChar(Instance) + Offset)
else
Result := nil;
end;
function FindPublishedFieldName(AClass: TClass; AOffset: integer): ShortString; overload;
var
Field: PPublishedField;
begin
Field := FindPublishedFieldByOffset(AClass, AOffset);
if Assigned(Field) then
Result := Field.Name
else
Result := '';
end;
function FindPublishedFieldName(Instance: TObject; AAddr: Pointer): ShortString; overload;
var
Field: PPublishedField;
begin
Field := FindPublishedFieldByAddr(Instance, AAddr);
if Assigned(Field) then
Result := Field.Name
else
Result := '';
end;
Наконец, я написал подпрограммы для возврата типа, адреса и значения published поля по имеющемуся у вас на руках указателю на TPublishedField. Они пригодятся, если вы будете писать свои собственные функции по работе с published полями класса:
function GetPublishedFieldType(AClass: TClass; Field: PPublishedField): TClass;
var
Pft: PPft;
begin
Pft := GetPft(AClass);
if Assigned(Pft) and Assigned(Field) and (Field.TypeIndex < Pft.FieldTypes.TypeCount) then
Result := Pft.FieldTypes.Types[Field.TypeIndex]^
else
Result := nil;
end;
function GetPublishedFieldAddr(Instance: TObject; Field: PPublishedField): PObject;
begin
if Assigned(Field) then
Result := PObject(PChar(Instance) + Field.Offset)
else
Result := nil;
end;
function GetPublishedFieldValue(Instance: TObject; Field: PPublishedField): TObject;
var
FieldAddr: PObject;
begin
FieldAddr := GetPublishedFieldAddr(Instance, Field);
if Assigned(FieldAddr) then
Result := FieldAddr^
else
Result := nil;
end;
Фух! Куча однообразного и скучного кода. Но теперь мы можем написать функцию, которая дампит информацию о published полях класса, реконструируя его объявление:
procedure DumpPublishedFields(AClass: TClass); overload;
var
i : integer;
Count: integer;
Field: PPublishedField;
FieldType: TClass;
ParentClass: string;
begin
while Assigned(AClass) do
begin
Count := GetPublishedFieldCount(AClass);
if Count > 0 then
begin
if AClass.ClassParent <> nil then
ParentClass := '(' + AClass.ClassParent.ClassName + ')'
else
ParentClass := '';
WriteLn('type');
WriteLn(' ', AClass.ClassName, ' = class', ParentClass);
WriteLn(' published');
Field := GetFirstPublishedField(AClass);
for i := 0 to Count-1 do
begin
FieldType := GetPublishedFieldType(AClass, Field);
WriteLn(Format(' %s: %s; // Offs=%d, Index=%d', [Field.Name, FieldType.ClassName, Field.Offset, Field.TypeIndex]));
Field := GetNextPublishedField(AClass, Field);
end;
WriteLn(' end;');
WriteLn;
end;
AClass := AClass.ClassParent;
end;
end;
Я также добавил подпрограмму, которая выводит реальное значение каждого поля – она более-менее эквивалентна коду выше, но только с добавленным вызовом GetPublishedFieldValue для получения значения поля. Чтобы протестировать этот код, я написал такой тестовый пример:
type
{$M+}
TMyClass = class
published
A: TObject;
LongName: TComponent;
B: TObject;
C: TList;
A2: TObject;
L2ongName: TComponent;
B2: TObject;
C2: TList;
end;
procedure Test;
begin
DumpPublishedFields(TMyClass);
end;
И получил такой вывод:
type
TMyClass = class(TObject)
published
A: TObject; // Offs=4, Index=0
LongName: TComponent; // Offs=8, Index=1
B: TObject; // Offs=12, Index=0
C: TList; // Offs=16, Index=2
A2: TObject; // Offs=20, Index=0
L2ongName: TComponent; // Offs=24, Index=1
B2: TObject; // Offs=28, Index=0
C2: TList; // Offs=32, Index=2
end;
Ну, это было очень весело! :-)К этому моменту мы документировали три из самых интересных недокументированных полей таблицы VMT, которые указывают на RTTI информацию, генерируемую компилятором:
TVmt = packed record
// ..
FieldTable : PPft;
MethodTable : PPmt;
DynamicTable : PDmt;
// ..
end;
А вот поля, которые мы ещё не рассматривали:
TVmt = packed record
// ..
IntfTable : Pointer;
AutoTable : Pointer;
InitTable : Pointer;
TypeInfo : Pointer;
// ..
end;
Если позволят время и мой интерес, я напишу про них в следующих статьях этой серии.
Комментариев нет:
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.