Friday, March 24, 2006

Hack #8: Explicit VMT calls

To accommodate the COM binary protocol in the pre-interface Delphi 2 era, all user-defined virtual methods have positive VMT offsets. This also means that TObject-defined virtual methods have negative VMT offsets. In addition the VMT also contains a number of “magic” fields to support features such as parent class link, instance size, class name, dynamic method table, published methods table, published fields table, RTTI table, initialization table for magic fields, the deprecated OLE Automation dispatch table and implemented interfaces table.

There are a number of integer offset vmtXXX constants in System.pas (many of which has been marked deprecated due to the BASM VMTOFFSET directive) that document how the compiler lays out the VMT table in memory. If we want to write code that access these fields directly (as opposed to using the documented APIs consisting of TObject methods and TypInfo routines) it is probably more useful to define a record structure that matches the fixed part of the VMT. Ray Lischner wrote such a record for his Secrets of Delphi 2 and Delphi in a Nutshell books – here is my quickly hacked up version:

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;
{UserDefinedVirtuals: array[0..999] of procedure;}

Given this definition of the VMT, we can write the following functions to obtain a PVmt from a class or instance reference:

function GetVmt(AClass: TClass): PVmt; overload;
Result := PVmt(AClass);

function GetVmt(Instance: TObject): PVmt; overload;
Result := GetVmt(Instance.ClassType);

Very simple. Lets write some test code to exercise these functions and the TVmt record. First we define a simple class that overrides all TObject virtuals and adds a couple of user defined virtual methods:

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;

The implementation of these methods simply writeln the ClassName and method name before calling the inherited implementation, and is not included here. Now we can write a test method that calls all the virtual methods explicitly through the obtained VMT pointer.

procedure Test;
Instance: TMyClass;
Instance2: TMyClass;
Vmt: PVmt;
Msg: Word;
Instance := TMyClass.Create;
Vmt := GetVmt(Instance);
Writeln('Calling virtual methods explicitly through an obtained'+
' VMT pointer (playing the compiler):');
Vmt^.SafeCallException(Instance, nil, nil);
Msg := 0;
Vmt^.Dispatch(Instance, Msg);
Vmt^.DefaultHandler(Instance, Msg);
Instance2 := Vmt^.NewInstance(TMyClass) as TMyClass;
Vmt^.Destroy(Instance2, 1);

Running this test code produces the following output:
Calling virtual methods explicitly through an obtained VMT pointer (playing the compiler):

It is interesting to note that explicitly calling through the obtained VMT pointer is actually slightly smaller and faster than the code the compiler generates. The reason is that we’re able to cache the VMT pointer (potentially in a register). For instance the two last calls to Destroy compiles into the following code:

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]

As you can see, the compiler must retrieve the VMT pointer (mov ecx, [eax]) for each virtual method call, while for the explicit Vmt call we have already cached this pointer, so the latter is smaller and faster. In extreme cases you might be able to speed up a loop that contains virtual method calls by using this VMT caching technique.

A cleaner approach is probably to use a procedure pointer variable – this can be done if the virtual method call is on the same instance each time through the loop. If the instance varies through the loop (for instance you need to call the virtual method of all instances in a list), the call must go through each instance’s VMT to dispatch correctly. However, in the special case where you have a guarantee that the collection is homogenous (all the instances it contains is of exactly the same type), you could use the Vmt pointer caching technique. The minimal performance gains and the increased complexity and compiler-version specific hacks it uses, makes this technique not very practical in real-world projects, though.

But, nevertheless, its fun to spelunk in the magic data structures and code generation that compiler uses to implement our favourite language – don’t you think? :-)

[Updated: Delphi syntax highlighting provided by DelphiDabbler PasH]


McKeeth said...

This was referenced in a discussion on Stack Overflow.

masonwheeler said...

I've done some playing around with this. You did a very good job, but you made one mistake that became noticeable very quickly:

TVmt.InstanceSize is stored as a Longint, not a PLongint.

Aside from that, excellent work as usual. I wish you were still writing about cool tricks like this.

Copyright © 2004-2007 by Hallvard Vassbotn