Чтобы приспособить двоичный протокол COM в до-интерфейсной эпохе Delphi 2, все определяемые пользователем виртуальные методы имели положительные смещения VMT. Это также означает, что виртуальные методы, определенные самим TObject, имеют отрицательные смещения VMT. Кроме того, VMT также содержит ряд "магических" полей для поддержки таких возможностей, как ссылки на родительский класс, получение имени класса, таблицы динамических методов, таблицы опубликованных (published) методов, таблицы RTTI, таблицы инициализации волшебных полей, (устаревшей) таблицы OLE Automation и таблицы реализованных интерфейсов.
Есть целый ряд целочисленных смещений в константах vmtXXX в System.pas (многие из которых были отмечены устаревшими из-за директивы
VMTOFFSET
в BASM), которые документируют как компилятор раскладывает таблицу VMT в памяти. Если мы хотим написать код, который получает доступ к этим полям напрямую (в сравнении с использованием документированных интерфейсов API, состоящих из методов TObject
и процедур TypInfo
), то, вероятно, более полезно будет определить структуру записи, которая соответствует фиксированной части VMT. Ray Lischner написал такую запись в его книгах Secrets of Delphi 2 и Delphi in a Nutshell - а вот моя быстро сделанная версия (прим.пер.: структура этой таблицы сильно зависит от компилятора Delphi):
type PClass = ^TClass; PSafeCallException = function (Self: TObject; ExceptObject: TObject; ExceptAddr: Pointer): HResult; PAfterConstruction = procedure (Self: TObject); PBeforeDestruction = procedure (Self: TObject); PDispatch = procedure (Self: TObject; var Message); PDefaultHandler = procedure (Self: TObject; var Message); PNewInstance = function (Self: TClass) : TObject; PFreeInstance = procedure (Self: TObject); PDestroy = procedure (Self: TObject; OuterMost: ShortInt); PVmt = ^TVmt; TVmt = packed record SelfPtr : TClass; IntfTable : Pointer; AutoTable : Pointer; InitTable : Pointer; TypeInfo : Pointer; FieldTable : Pointer; MethodTable : Pointer; DynamicTable : Pointer; ClassName : PShortString; InstanceSize : PLongint; Parent : PClass; SafeCallException : PSafeCallException; AfterConstruction : PAfterConstruction; BeforeDestruction : PBeforeDestruction; Dispatch : PDispatch; DefaultHandler : PDefaultHandler; NewInstance : PNewInstance; FreeInstance : PFreeInstance; Destroy : PDestroy; { Виртуальные функции, определённые пользователем: array[0..999] of Pointer; } end;Исходя из этого определения VMT, мы можем написать следующие функции для получения PVmt из ссылки на класс или экземпляр:
function GetVmt(AClass: TClass): PVmt; overload; begin Result := PVmt(AClass); Dec(Result); end; function GetVmt(Instance: TObject): PVmt; overload; begin Result := GetVmt(Instance.ClassType); end;Очень просто. Давайте напишем тестовый код с использованием этих функций и записи
TVmt
. Сначала определим простой класс, который перекрывает все виртуальные функции TObject
и добавляет пару пользовательских виртуальных методов:
type TMyClass = class function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; override; procedure AfterConstruction; override; procedure BeforeDestruction; override; procedure Dispatch(var Message); override; procedure DefaultHandler(var Message); override; class function NewInstance: TObject; override; procedure FreeInstance; override; destructor Destroy; override; procedure MethodA(var A: integer); virtual; procedure Method; virtual; end;Реализации всех этих методов просто вызывают
WriteLn
для вывода имени класса и имени метода перед вызовом унаследованной реализации - и поэтому здесь не приводятся. Теперь мы можем написать тестовый метод, который вызывает все виртуальные методы явно через VMT получить указатель.procedure Test; var Instance: TMyClass; Instance2: TMyClass; Vmt: PVmt; Msg: Word; begin Instance := TMyClass.Create; Vmt := GetVmt(Instance); Writeln('Calling virtual methods explicitly through an obtained'+ ' VMT pointer (playing the compiler):'); writeln(Vmt.Classname^); Vmt^.SafeCallException(Instance, nil, nil); Vmt^.AfterConstruction(Instance); Vmt^.BeforeDestruction(Instance); Msg := 0; Vmt^.Dispatch(Instance, Msg); Vmt^.DefaultHandler(Instance, Msg); Instance2 := Vmt^.NewInstance(TMyClass) as TMyClass; Instance.Destroy; Vmt^.Destroy(Instance2, 1); readln; end;Запуск этого тестового кода даёт такие результаты:
TMyClass.NewInstance TMyClass.AfterConstruction Calling virtual methods explicitly through an obtained VMT pointer (playing the compiler): TMyClass TMyClass.SafeCallException TMyClass.AfterConstruction TMyClass.BeforeDestruction TMyClass.DefaultHandler TMyClass.Dispatch TMyClass.DefaultHandler TMyClass.NewInstance TMyClass.BeforeDestruction TMyClass.Destroy TMyClass.FreeInstance TMyClass.BeforeDestruction TMyClass.Destroy TMyClass.FreeInstanceИнтересно отметить, что явный вызов через полученный указатель VMT, на самом деле, немного меньше и быстрее, чем код компилятора. Причина в том, что мы в состоянии кэшировать указатель на VMT (потенциально - в регистре). Например, два последних вызова
Destroy
компилируются в следующий код:
Instance.Destroy; 00408781 B201 mov dl,$01 00408783 8BC6 mov eax,esi 00408785 8B08 mov ecx,[eax] 00408787 FF51FC call dword ptr [ecx-$04] Vmt^.Destroy(Instance2, 1); 0040878A B201 mov dl,$01 0040878C 8BC7 mov eax,edi 0040878E FF5348 call dword ptr [ebx+$48]Как вы можете видеть, компилятор вынужден получать указатель VMT (
MOV ECX, [EAX]
) для каждого вызова виртуального метода, а при явном вызове VMT мы уже имеем на руках этот указатель, так что последний вариант получается меньше и быстрее. В крайнем случае мы могли бы ускорить с помощью этой техники кэширования VMT цикл, который состоит из вызовов виртуальных вызовов.Более чистый подход заключается в использовании переменной указателя на процедуру - это можно сделать, если вызов виртуального метода производится на один и тот же экземпляр на каждой итерации цикла. Если экземпляр меняется через итерацию (например, вам нужно вызвать виртуальный метод всех экземпляров в списке), то для совершения вызова вам придётся пройти через получение VMT каждого экземпляра. Однако в частном случае, когда у вас есть гарантия того, что коллекция является однородной (все экземпляры объектов, содержащихся в ней, являются одного типа), вы можете использовать технику кэширования указателя VMT. Однако минимальный прирост производительности и значительное повышенной сложности, а также использование зависимых от версии компилятора хаков делают этот метод не практичным в реальных проектах.
Но, тем не менее, это же забавно - нырнуть в волшебные структуры данных и генерируемый код, который компилятор использует для реализации нашего любимого языка - вам так не кажется? :-)
Комментариев нет:
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.