Теперь, когда я рассказал, что такое published методы, как IDE и VCL используют их при сохранении/загрузке .DFM и как использовать их полиморфно, мы готовы погрузиться глубже в их детали реализации под капотом языка.
Если вы следовали за мной через эту серию постов о полиморфных возможностях языка Delphi, то вы уже заметили, что VMT классов содержит поле
MethodTable
, которое мы пока определили как нетипизированный указатель (Pointer
). После тщательного анализа методов TObject
, работающих с этой таблицей (MethodName
и MethodAddress
), мне удалось написать приблизительное объявление структуры MethodTable
на Pascal-е:
type PPublishedMethod = ^TPublishedMethod; TPublishedMethod = packed record Size: word; Address: Pointer; Name: {packed} ShortString; // на самом деле string[Length(Name)] end; TPublishedMethods = packed array[0..High(Word)-1] of TPublishedMethod; PPmt = ^TPmt; TPmt = packed record Count: Word; Methods: TPublishedMethods; // на самом деле [0..Count-1] end; PVmt = ^TVmt; TVmt = packed record // … MethodTable : PPmt; // … end;Как вы можете видеть выше, таблица published методов теперь имеет тип
PPmt
. Это указатель на запись, которая содержит число published методов в классе, за которым следует массив из этого количества записей TPublishedMethod
. Каждая запись содержит размер (используется для перехода к следующему элементу), указатель на точку входа метода и ShortString
, содержащую имя метода.Заметьте, что поле
Size
избыточно: во всех случаях значение Size
равно:
Size := SizeOf(Size) + SizeOf(Address) + SizeOf(Name[0]) + Length(Name);Другими словами, следующая запись
TPublishedMethod
начинается прямо за последним байтом текущей записи (т.е. последним байтом имени метода). Я не уверен, почему Borland решила добавить поле Size
, но возможной причиной может быть расширение записи TPublishedMethod
в будущем. Естественное расширение - добавить информацию по количеству и типам параметров, а также соглашению вызова метода. Тогда поле Size
было бы увеличено, а старый код, который не в курсе новых возможностей, продолжал работать бы дальше (см. также заметку о дополнительных данных published методов в конце поста).Теперь, когда у нас есть структуры данных для работы, давайте напишем несколько вспомогательных подпрограмм:
function GetVmt(AClass: TClass): PVmt; begin Result := PVmt(AClass); Dec(Result); end; function GetPmt(AClass: TClass): PPmt; var Vmt: PVmt; begin Vmt := GetVmt(AClass); if Assigned(Vmt) then Result := Vmt.MethodTable else Result := nil; end; function GetPublishedMethodCount(AClass: TClass): integer; var Pmt: PPmt; begin Pmt := GetPmt(AClass); if Assigned(Pmt) then Result := Pmt.Count else Result := 0; end; function GetPublishedMethod(AClass: TClass; Index: integer): PPublishedMethod; var Pmt: PPmt; begin Pmt := GetPmt(AClass); if Assigned(Pmt) and (Index < Pmt.Count) then begin Result := @Pmt.Methods[0]; while Index > 0 do begin Inc(PChar(Result), Result.Size); Dec(Index); end; end else Result := nil; end;Сначала мы просим нашего старого друга
GetVmt
получить указатель на волшебную часть VMT по ссылке на данный класс. Используя это и новый тип PPmt
, мы можем написать функцию GetPmt
выше - она возвращает указатель на таблицу published методов класса. Затем, есть две подпрограммы, которые возвращают число published методов и заданный published метод по индексу от 0 до Count
- 1. Используя эти служебные подпрограммы, мы можем написать тестовый код для дампа всех published методов класса (и его родительских классов).
procedure DumpPublishedMethods(AClass: TClass); var i : integer; Method: PPublishedMethod; begin while Assigned(AClass) do begin WriteLn('Published methods in ', AClass.ClassName); for i := 0 to GetPublishedMethodCount(AClass)-1 do begin Method := GetPublishedMethod(AClass, i); WriteLn(Format('%d. MethodAddr = %p, Name = %s', [i, Method.Address, Method.Name])); end; AClass := AClass.ClassParent; end; end;Этот код дампа работает отлично, но его производительность далека от идеальной. Методу
GetPublished
приходится делать поиск заново для каждого значения Index, что даёт подпрограмме Dump
сложность выполнения O(n^2) (где n - это число published методов в классе). Хотя большинство классов не имеют ужасно много published методов и эта работа во внутреннем цикле часто будет минимальной, так что на практике этот момент не должен стать проблемой.Однако моя одержимость производительностью обязует меня ускорить этот код, хотя бы в теоретическом плане. Массив из записей
TPublishedMethod
может быть рассмотрен как примитивный односвязный список (singly linked list): произвольный доступ для него является медленным, так что техника итератора должна улучшить производительность. Давайте напишем ещё вспомогательных подпрограмм:
function GetFirstPublishedMethod(AClass: TClass): PPublishedMethod; begin Result := GetPublishedMethod(AClass, 0); end; function GetNextPublishedMethod(AClass: TClass; PublishedMethod: PPublishedMethod): PPublishedMethod; begin Result := PublishedMethod; if Assigned(Result) then Inc(PChar(Result), Result.Size); end;Эти две подпрограммы являются типичной парой
GetFirst
/GetNext
итераторов. Первый метод возвращает ссылку на первый published метод, а второй метод возвращает ссылку на следующий published метод. Заметьте, что вызывать GetNextPublishedMethod
нужное число раз (используя GetPublishedMethodCount
) является ответственностью вызывающего. Теперь мы можем переписать метод дампа, делая его немного быстрее:
procedure DumpPublishedMethodsFaster(AClass: TClass); var i : integer; Method: PPublishedMethod; begin while Assigned(AClass) do begin WriteLn('Published methods in ', AClass.ClassName); Method := GetFirstPublishedMethod(AClass); for i := 0 to GetPublishedMethodCount(AClass) - 1 do begin WriteLn(Format('%d. MethodAddr = %p, Name = %s', [i, Method.Address, Method.Name])); Method := GetNextPublishedMethod(AClass, Method); end; AClass := AClass.ClassParent; end; end;Итерация по всем published методам класса или их дамп обычно не очень нужны на практике.
TObject
уже содержит методы, которые позволяют производить поиск published методов: MethodAddress
и MethodName
. Они написаны эффективно, на ассемблере, но это также делает их сложнее для чтения и понимания. Я использовал их для определения формата таблицы published методов выше. Вот они же, но уже на Pascal-е:
function FindPublishedMethodByName(AClass: TClass; const AName: ShortString): PPublishedMethod; var i : integer; begin while Assigned(AClass) do begin Result := GetFirstPublishedMethod(AClass); for i := 0 to GetPublishedMethodCount(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 := GetNextPublishedMethod(AClass, Result); end; AClass := AClass.ClassParent; end; Result := nil; end; function FindPublishedMethodByAddr(AClass: TClass; AAddr: Pointer): PPublishedMethod; var i : integer; begin while Assigned(AClass) do begin Result := GetFirstPublishedMethod(AClass); for i := 0 to GetPublishedMethodCount(AClass) - 1 do begin if Result.Address = AAddr then Exit; Result := GetNextPublishedMethod(AClass, Result); end; AClass := AClass.ClassParent; end; Result := nil; end; function FindPublishedMethodAddr(AClass: TClass; const AName: ShortString): Pointer; var Method: PPublishedMethod; begin Method := FindPublishedMethodByName(AClass, AName); if Assigned(Method) then Result := Method.Address else Result := nil; end; function FindPublishedMethodName(AClass: TClass; AAddr: Pointer): Shortstring; var Method: PPublishedMethod; begin Method := FindPublishedMethodByAddr(AClass, AAddr); if Assigned(Method) then Result := Method.Name else Result := ''; end;Первые две функции ищут published метод по имени или адресу и возвращают указатель на запись
TPublishedMethod
, описывающую метод. Иметь прямой доступ к этой записи может оказаться полезным при выполнении другой работы со структурами RTTI. В любом случае, последние две функции возвращают строку и адрес напрямую, соответствуя методам MethodName
и MethodAddress
.Наконец, мы можем написать класс для теста подпрограмм, которые мы написали:
type {$M+} TMyClass = class published procedure FirstPublished; procedure SecondPublished(A: integer); procedure ThirdPublished(A: integer); stdcall; function FourthPublished(A: TComponent): TComponent; stdcall; procedure FifthPublished(Component: TComponent); stdcall; function SixthPublished(A: string; Two, Three, Four, Five, Six: integer): string; pascal; end; procedure TMyClass.FirstPublished; begin end; procedure TMyClass.SecondPublished; begin end; procedure TMyClass.ThirdPublished; begin end; function TMyClass.FourthPublished; begin Result := nil; end; procedure TMyClass.FifthPublished; begin end; function TMyClass.SixthPublished; begin end; procedure DumpMethod(Method: PPublishedMethod); begin if Assigned(Method) then WriteLn(Format('%p=%s', [Method.Address, Method.Name])) else WriteLn('nil'); end; procedure Test; begin DumpPublishedMethods(TMyClass); DumpPublishedMethodsFaster(TMyClass); DumpMethod(FindPublishedMethodByName(TMyClass, 'FirstPublished')); DumpMethod(FindPublishedMethodByName(TMyClass, FindPublishedMethodName(TMyClass, @TMyClass.SecondPublished))); DumpMethod(FindPublishedMethodByAddr(TMyClass, @TMyClass.ThirdPublished)); DumpMethod(FindPublishedMethodByAddr(TMyClass, FindPublishedMethodAddr(TMyClass, 'FourthPublished'))); DumpMethod(FindPublishedMethodByAddr(TMyClass, FindPublishedMethodByName(TMyClass, 'FifthPublished').Address)); DumpMethod(FindPublishedMethodByAddr(TMyClass, @TMyClass.SixthPublished)); DumpMethod(FindPublishedMethodByName(TMyClass, 'NotThere')); DumpMethod(FindPublishedMethodByAddr(TMyClass, nil)); end; begin Test; ReadLn; end.Вывод этого тестового кода:
Published methods in TMyClass 0. MethodAddr = 00412BCC, Name = FirstPublished 1. MethodAddr = 00412BD0, Name = SecondPublished 2. MethodAddr = 00412BD4, Name = ThirdPublished 3. MethodAddr = 00412BDC, Name = FourthPublished 4. MethodAddr = 00412BE8, Name = FifthPublished 5. MethodAddr = 00412BF0, Name = SixthPublished Published methods in TObject Published methods in TMyClass 0. MethodAddr = 00412BCC, Name = FirstPublished 1. MethodAddr = 00412BD0, Name = SecondPublished 2. MethodAddr = 00412BD4, Name = ThirdPublished 3. MethodAddr = 00412BDC, Name = FourthPublished 4. MethodAddr = 00412BE8, Name = FifthPublished 5. MethodAddr = 00412BF0, Name = SixthPublished Published methods in TObject 00412BCC=FirstPublished 00412BD0=SecondPublished 00412BD4=ThirdPublished 00412BDC=FourthPublished 00412BE8=FifthPublished 00412BF0=SixthPublished nil nil
Поиск дополнительных данных published методов
Я добавил немного отладочного кода вGetNextPublishedMethod
, который пытается найти запись TPublishedMethod
, в которой поле Size
было бы больше, чем размер полей, включая имя, обсуждаемых выше:
function GetNextPublishedMethod(AClass: TClass; PublishedMethod: PPublishedMethod): PPublishedMethod; {$IFDEF DEBUG} var ExpectedSize: integer; {$ENDIF} begin Result := PublishedMethod; {$IFDEF DEBUG} ExpectedSize := SizeOf(Result.Size) + SizeOf(Result.Address) + SizeOf(Result.Name[0]) + Length(Result.Name); if Result.Size <> ExpectedSize then raise Exception.CreateFmt('RTTI for the published method "%s" of class "%s" has %d extra bytes of unknown data!', [Result.Name, AClass.ClassName, Result.Size-ExpectedSize]); {$ENDIF} if Assigned(Result) then Inc(PChar(Result), Result.Size); end;Во время моего тестирования published методов различных соглашений вызова и числа параметров, мне так и не удалось найти ни одного случая с дополнительными данными. Дайте мне знать, если вам это удастся. Прим.пер.: вообще-то, дополнительные данные генерируются при включении т.н. расширенной RTTI информации класса - о чём сам же Hallvard говорит позднее в продолжении серии.
Я смутно вспомнил, что Ray Lischner писал об этих дополнительных полях в своей замечательной книге "Delphi in a Nutshell". Фактически, я был одним из технических редакторов этой книги - так что я должен помнить :-) Как пишет Ray (см. стр. 74), Delphi 5 (и более ранние версии) будут кодировать параметры некоторых published методов - точнее методов stdcall, у которых параметры и возвращаемое значение имеют RTTI-информацию. Это половинчатое решение по кодированию параметров, видимо, является остатком каких-то экспериментальных версий RTTI кода в компиляторе, которые, кажется, были удалены из Delphi 7 и выше.
Хорошая статья. А что делать, если мне необходимо преобразовать строку с именем класса в тип класса?
ОтветитьУдалитьvar
Str: string[50];
Str:= 'TButton';
...
function StrToClass(S: string): TClass;
begin
Result:= ???
end;
Вы серьёзно? Первая же ссылка в поиске - описываются два метода: один для любых версий Delphi - через хранилище (список/массив) зарегистрированных классов; второй для Delphi 2010 и выше - используя расширенный RTTI (которого нет в более ранних версиях).
УдалитьУ кого-нибудь стоит Delphi 2010?
ОтветитьУдалитьПожалуйста выложите ссылку на модуль RTTI.pas