пятница, 15 июля 2011 г.

Хак №8: явные вызовы VMT

Это перевод Hack #8: Explicit VMT calls. Автор: Hallvard Vassbotn.

Чтобы приспособить двоичный протокол 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 можно просто не указывать.

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

Примечание. Отправлять комментарии могут только участники этого блога.