Delphi’s Integrated Development Environment (IDE) depends on information provided by the compiler. This information, called Runtime Type Information (RTTI), describes some aspects of classes and other types. It’s not a full reflection system such as you find in Java, but it’s more complete than type identifiers in C++. For ordinary, everyday use of Delphi, you can ignore the details of RTTI and just let Delphi do its thing. Sometimes, though, you need to look under the hood and understand exactly how RTTI works.
The only difference between a published declaration and a public declaration is RTTI. Delphi stores RTTI for published fields, methods, and properties, but not for public, protected, or private declarations. Although the primary purpose of RTTI is to publish declarations for the IDE and for saving and loading .dfm files, the RTTI tables include other kinds of information. For example, virtual and dynamic methods, interfaces, and automated declarations are part of a class’s RTTI. Most types also have RTTI called type information. This chapter explains all the details of RTTI.
The Virtual Method Table (VMT) stores pointers to all the virtual methods declared for a class and its base classes. The layout of the VMT is the same as in most C++ implementations (including Borland C++ and C++ Builder) and is the same format required for COM, namely a list of pointers to methods. Each virtual method of a class or its ancestor classes has an entry in the VMT.
Each class has a unique VMT. Even if a class does not define any of
its own virtual methods, but only inherits methods from its base
class, it has its own VMT that lists all the virtual methods it
inherits. Because each VMT lists every virtual method, Delphi can
compile calls to virtual methods as quick lookups in the VMT. Because
each class has its own VMT, Delphi uses the VMT to identify a class.
In fact, a class reference is really a pointer to a class’s
VMT, and the ClassType
method returns a pointer to
the VMT.
In addition to a table of virtual methods, the VMT includes other
information about a class, such as the class name, a pointer to the
VMT for the base class, and pointers to many other RTTI tables. The
other RTTI pointers appear before the first virtual method in the
VMT. Example 3-1 shows a record layout that is
equivalent to the VMT. The actual list of virtual methods begins
after the end of the TVmt
record. In other words,
you can convert a TClass
class reference to a
pointer to a TVmt
record by subtracting the size
of the record, as shown in Example 3-1.
type PVmt = ^TVmt; TVmt = record SelfPtr: TClass; // Points forward to the start // of the VMT // The following pointers point to other RTTI tables. If a class // does not have a table, the pointer is nil. Thus, most classes // have a nil IntfTable and AutoTable, for example. IntfTable: PInterfaceTable; // Interface table AutoTable: PAutoTable; // Automation table InitTable: PInitTable; // Fields needing finalization TypeInfo: PTypeInfo; // Properties & other info FieldTable: PFieldTable; // Published fields MethodTable: PMethodTable; // Published methods DynMethodTable: PDynMethodTable; // List of dynamic methods ClassName: PShortString; // Points to the class name InstanceSize: LongInt; // Size of each object, in bytes ClassParent: ^TClass; // Immediate base class // The following fields point to special virtual methods that // are inherited from TObject. SafeCallException: Pointer; AfterConstruction: Pointer; BeforeDestruction: Pointer; Dispatch: Pointer; DefaultHandler: Pointer; NewInstance: Pointer; FreeInstance: Pointer; Destroy: Pointer; // Here begin the virtual method pointers. // Each virtual method is stored as a code pointer, e.g., // VirtualMethodTable: array[1..Count] of Pointer; // But the compiler does not store the count of the number of // method pointers in the table. end; var Vmt: PVmt; begin // To get a PVmt pointer from a class reference, cast the class // reference to the PVmt type and subtract the size of the TVmt // record. This is easily done with the Dec procedure: Vmt := PVmt(SomeObject.ClassType); Dec(Vmt);
As you can see, the VMT includes pointers to many other tables. The following sections describe these tables in more detail.
The only difference between a published declaration and a public one is that a published declaration tells the compiler to store information in the VMT. Only certain kinds of information can be stored, so published declarations face a number of restrictions:
In order to declare any published fields, methods, or properties, a
class must have RTTI enabled by using the $M+
directive or by inheriting from a class that has RTTI. (See Chapter 8, for details.)
Fields must be of class type (no other types are allowed). The class type must have RTTI enabled.
Array properties cannot be published. The type of a published property cannot be a pointer, record, or array. If it is a set type, it must be small enough to be stored in an integer. In the current release of Delphi, that means the set can have no more than 32 members.
The published section cannot contain more than one overloaded method with each name. You can overload methods, but only one of the overloaded methods can be published.
The Classes
unit declares
TPersistent
with the $M+
directive. TPersistent
is usually used as a base
class for all Delphi classes that need published declarations. Note
that TComponent
inherits from
TPersistent
.
Delphi
stores the names and addresses of published methods in a
class’s RTTI. The IDE uses this information to store the values
of event properties in a .dfm file. In the IDE,
each event property is either nil
or contains a
method reference. The method reference includes a pointer to the
method’s entry point. (At design time, the IDE has no true
entry point, so it makes one up. At runtime, your application uses
the method’s real entry point.) To store the value of an event
property, Delphi looks up the method address in the class’s
RTTI, finds the corresponding method name, and stores the name in the
.dfm file. To load a .dfm
file, Delphi reads the method name and looks up the corresponding
method address from the class’s
RTTI.
A class’s RTTI stores only the published methods for that
class, and not for any ancestor classes. Thus, to look up a method
name or address, the lookup might fail for a derived class, in which
case, the lookup continues with the base class. The
MethodName
and MethodAddress
methods of TObject
do the work of searching a
class’s RTTI, then searching the base class’s RTTI, and
so on, up the inheritance chain. (See the TObject
type in Chapter 5, for details about these
methods.) The published method table contains only the method name
and address.
You can declare any method in the published section of a class
declaration. Usually, though, Delphi’s IDE creates the methods
for you. When you double-click an event property, for example, the
IDE creates a method in the initial, unnamed section of the form
class. Because a form class has RTTI enabled, the initial, unnamed
section is published. (Form classes have RTTI because
TPersistent
is an ancestor class.)
The method table starts with a 2-byte count of the number of published methods, followed by a record for each method. Each method record starts with a 2-byte size of the method record, followed by the method address (4 bytes), and then followed by the method name as a short string, that is, as a 1-byte string length followed by the text of the string.
Example 3-2 depicts the logical structure of the method table. Note that Delphi cannot use these declarations verbatim because the record size varies to fit the size of the strings.
type TMethodParam = packed record TypeInfo: PPTypeInfo; Name: ShortString; // The name is followed by a trailing #0 byte. end; TMethod = packed record Size: Word; // Size of the TVmtMethod record. Address: Pointer; // Pointer to the method entry point. Name: packed ShortString; // Name of the published method. // Some methods have an additional 4 zero bytes, which means the // method takes no parameters. // Some methods have an additional 6 bytes, followed by a series of // TMethodParam records, for each parameter. // It seems that only stdcall methods have this extra information. // You can identify the extra info by the TMethod.Size value being // too big for just the Size, Address, and Name members. The only // way to know how many parameters are stored here is to check // each parameter until you reach the record size. ExtraStuff: array[1..FourOrSix] of Byte; Params: array[1..ParamCount] of TMethodParam; end; { Published method table } TMethodTable = packed record Count: Word; Methods: array[1..Count] of TMethod; end;
Each published field has a name, a type, and an offset. The type is a class reference for the field’s type. (Published fields must be of class type.) The offset is an offset (in bytes) into the object’s storage, where the field is stored.
The published field table starts with a 2-byte count of the number of fields, followed by a 4-byte pointer to a class table, followed by the field definitions. Each field definition is a record containing a 4-byte offset, and a 2-byte index into the class table, followed by the field name as a short string.
The class table lists all the classes used by the published fields. Each field contains an index into this table. The class table starts with a 2-byte count, followed by a list of class references where each class reference is 4 bytes. A class reference is a pointer to the class’s VMT.
Example 3-3 shows the logical layout of the field table. Because the records are variable length, you cannot use these declarations in a Delphi program.
type { Field class table } PFieldClassTable = ^TFieldClassTable; TFieldClassTable = packed record Count: Word; Classes: packed array[1..Count] of ^TClass; end; { Published field record } TField = packed record Offset: LongWord; // Byte offset of field in the object. } ClassIndex: Word; // Index in the FieldClassTable of the // field's type. Name: packed ShortString; // Name of the published field. } end; { Published field table } TFieldTable = packed record Count: Word; FieldClassTable: PFieldClassTable; Fields: packed array [1..Count] of TField; end;
Published
properties have lots of information stored about them: name, type,
reader, writer, default value, index, and stored flag. The type is a
pointer to a TTypeInfo
record (discussed in the
next section). The reader and writer can be fields, methods, or
nothing. The default value is an ordinal value; non-ordinal
properties don’t have default values. The stored flag can be a
constant, a field, or a method reference. The Object Inspector in
Delphi’s IDE relies on published properties, and Delphi uses
the default and stored information when saving and loading
.dfm files.
The reader, writer, and stored fields can be pointers to static
methods, byte offsets of virtual methods, or byte offsets of fields.
Dynamic methods are not allowed, and static or virtual methods must
use the register
calling convention (which is the
default). Additionally, the stored
value can be a
constant True or False. Delphi stores these different kinds of values
as follows:
A constant True or False is stored as a literal zero or 1. Only the
stored
directive can have a constant True or
False. If a reader or writer is zero, that means the property does
not have that particular directive, that is, the property is
write-only or read-only, respectively.
A field offset is stored with $FF in the most significant byte. For example, a field stored at offset 42 ($2A) would have the value $FF00002A. Note that published fields are rarely used to store property values, so it is unlikely that you could look up the name of the field in the published field table.
A virtual method is stored with $FE in the most significant byte and
the byte offset of the method as a SmallInt
in the
low order 2 bytes. For example, the third virtual method is stored as
$FE000008. (The first virtual method has offset 0.)
A static method is stored as an address, e.g., $00401E42. The memory architecture of Windows prevents any method from having an address with $FF or $FE in the most significant byte, so there is no danger of conflicts or ambiguities.
The default value can be stored only for integer, character,
enumeration, or set types. If the programmer declares a property with
the nodefault
directive (which is the same as
omitting the default
directive), Delphi stores the
most negative integer ($80000000 or -2,147,483,648) as the default
value. In other words, you cannot have an integer property whose
default value is -2,147,483,648 because Delphi would interpret that
as being the same as
nodefault
.
String, floating-point, Int64
,
Variant
, and class-type properties cannot have
default values, and the nodefault
directive has no
effect. (Delphi always uses an empty string, zero,
Unassigned
, or nil
as the
default value for these types when reading and writing
.dfm files.) If you want the effect of defining
a default value for these kinds of properties, you can play a trick
in the class’s constructor: set the property’s value when
the user drops the component on a form, and not when Delphi loads the
component from the .dfm file. What makes this
tricky is that the ComponentState
property is not
set until after the constructor returns. (Read about
ComponentState
in Delphi’s help files.)
Thus, you need to test the owner’s
ComponentState
, as
follows:
constructor TStringDefault.Create(Owner:
TComponent);
begin
inherited;
if (Owner = nil) or
(([csReading, csDesigning] *
Owner.ComponentState) = [csDesigning])
then
StringProperty := 'Default value';
end;
This trick does not save any space in the .dfm file, but it achieves the goal of setting a default value for a property that does not ordinarily take a default value.
The primary purpose of a default value is to save space in a
.dfm file. If a property’s value is the
same as the default value, Delphi doesn’t store that value in
the .dfm. If a property does not have a default
value, Delphi always stores the value in the
.dfm. Note that inherited forms get their
default values from the ancestor form, which gets it from the
default
directive. It is the programmer’s
responsibility to initialize a property to its default value in the
class’s constructor—Delphi doesn’t do that
automatically. (See Example 3-5, later in this
chapter, for help setting default property
values.)
The index
directive stores the index value for an
indexed property. If the property is not indexed, Delphi stores the
most negative integer as the index value.
The published property information also stores the name index, that is, the ordinal position of the property in the class declaration. The Object Inspector can sort properties into alphabetical order, which scrambles the declared order of a class’s properties. The name index value gives you the original order.
Delphi makes it easy to access a class’s published property
information using the TypInfo
unit, which is the
subject of the next section.
The
TypInfo
unit declares several types and functions
that give you easy access to the published properties of an object
and other information. The Object Inspector relies on this
information to perform its magic. You can obtain a list of the
published properties of a class and get the name and type for each
property. Given an object reference, you can get or set the value of
any published property.
The TypeInfo
function returns a pointer to a type
information record, but if you don’t use the
TypInfo
unit, you cannot access anything in that
record and must instead treat the result as an untyped
Pointer
. The TypInfo
unit
defines the real type, which is PTypeInfo
, that
is, a pointer to a TTypeInfo
record. The type
information record contains a type kind and the name of the type. The
type kind is an enumerated value that tells you what kind of type it
is: integer, floating point, string, etc.
Some
types have additional type data, as returned by the
GetTypeData
function, which returns a
PTypeData
pointer. You can use the type data to
get the names of an enumerated literal, the limits of an ordinal
subrange, and more. Table 3-1 describes the data
for each type kind.
|
Associated Data |
|
No associated data. |
|
Limits of character subrange. |
|
Class reference, parent class, unit where class is declared, and published properties. |
|
No associated data for dynamic arrays. |
|
If the type is a subrange of another type, the data includes a pointer to the base type and the limits of the subrange; otherwise, the data includes the limits of the subrange and a packed list of counted strings for the names of the enumerated literals. |
|
Floating-point type: currency, comp, single, double, or extended (but
not |
|
Limits of integer subrange. |
|
Limits of integer subrange. |
|
Base interface, unit where the interface is declared, and the GUID. |
|
No associated data for a long string ( |
|
Return type, kind of method, and parameter names and type names. |
|
No associated data. |
|
Pointer to the enumerated type of the set elements. |
|
Maximum length of a short string. |
|
No associated data. |
|
No associated data. |
|
Limits of wide character subrange. |
|
No associated data for a |
Note that the primary purpose of type information is to support Delphi’s IDE and for reading and writing .dfm files. A secondary purpose is for initialization and finalization of managed types. It is not a general-purpose reflection system, as you find in Java, so information about records and arrays, for example, is limited.
Many of the functions in the TypInfo
unit make it
easy for you to access the published properties of an object. Instead
of accessing the type information directly, you can call some
functions to get or set a property value, determine whether the
property should be stored, get the property type and name, and so on.
To get or set a property value, you need to know what kind of
property type you are dealing with: ordinal, floating point, string,
Variant
, method, or Int64
. Each
kind of type has a pair of subroutines to get and set a property
value. If the property has methods for the reader or writer, the
TypInfo
routines call those methods, just as
though you were getting or setting the property in the usual manner.
Integer-, character-, enumeration-, set-, and class-type properties
are ordinal. They store their property values in an integer, so you
must use an integer to get or set the property value. When you get
the property value, cast it to the desired type. To set the property
value, cast the value to an integer. Example 3-4
shows a procedure that takes any component as an argument and tests
whether that component publishes a property called
Font
whose type is a class type. If so, the
procedure sets the component’s font to Arial, 10 pt.
procedure SetFontToArial10pt(Component: TComponent); var Font: TFont; PropInfo: PPropInfo; begin // First find out if the component has a Font property. PropInfo := GetPropInfo(Component, 'Font'), if PropInfo = nil then Exit; // Next see if the property has class type. if PropInfo.PropType^.Kind <> tkClass then Exit; Font := TFont.Create; try Font.Name := 'Arial'; Font.Size := 10; // Now set the component's Font property. SetOrdProp(Component, PropInfo, Integer(Font)); // SetOrdProp is just like Component.Font := Font except that // the compiler doesn't need to know about the Font property. // The component's writer copies the TFont object, so this // procedure must free its Font to avoid a memory leak. finally Font.Free; end; end;
You can get a list of PPropInfo
pointers if you
need to learn about all of an object’s properties, or you can
call GetPropInfo
to learn about a single property.
The GetPropList
function gets only properties
whose type kind matches a set of type kinds that you specify. You can
use this to learn about events (tkMethod
),
string-valued properties only (tkString
,
tkLString
, tkWString
), and so
on. Example 3-5 shows a procedure that takes an
object as an argument and looks up all the ordinal-type properties,
then gets the default values of those properties and sets the
property values to the defaults. You can call this function from a
constructor to guarantee that the properties are properly
initialized, thereby avoiding a possible error where the property
declaration has one default value, but the constructor has a
different one.
// Set the default value for all published properties. procedure SetDefaultValues(Obj: TObject); const tkOrdinal = [tkEnumeration, tkInteger, tkChar, tkSet, tkWChar]; NoDefault = Low(Integer); var PropList: PPropList; Count, I: Integer; Value: Integer; begin // Count the number of ordinal properties that can have // default values. Count := GetPropList(Obj, tkOrdinal, nil); // Allocate memory to store the prop info & get the real prop list. GetMem(PropList, Count * SizeOf(PPropInfo)); try GetPropList(Obj, tkOrdinal, PropList); // Loop through all the ordinal properties. for I := 0 to Count-1 do // If the property has a default value, set the property value // to that default. if PropList[I].Default <> NoDefault then SetOrdProp(Obj, PropList[I], PropList[I].Default) finally FreeMem(PropList); end; end;
The
routines in the TypInfo
unit, while not
documented, are straightforward and easy to use. The following list
describes all the subroutines in the TypInfo
unit,
for your convenience. Consult the TypInfo.pas
source file for further details (provided you have at least the
Professional edition of Delphi or C++ Builder). Note that these
functions perform little or no error checking. It is your
responsibility to ensure that you are calling the correct function
for the property and its type. Changing the value of a read-only
property or getting the value of a write-only property, for example,
results in an access violation.
function GetObjectProp(Instance: TObject; PropInfo: PPropInfo; MinClass: TClass = nil): TObject; overload; function GetObjectProp(Instance: TObject; const PropName: string; MinClass: TClass = nil): TObject; overload;
Gets the value of a class-type property.
MinClass
is the base class that you require for
the property value; if the result is not of type
MinClass
or a descendant,
GetObjectProp
returns nil
. The
default is to allow an object of any class.
function GetObjectPropClass(Instance: TObject; PropInfo: PPropInfo): TClass; overload; function GetObjectPropClass(Instance: TObject; const PropName: string): TClass; overload;
Gets the class type from the
property’s type data. Instance
is used only
to look up the property information, so the first version of this
function (which already has the property information in the
PropInfo
parameter) does not refer to
Instance
.
function GetOrdProp(Instance: TObject; PropInfo: PPropInfo): Longint; overload; function GetOrdProp(Instance: TObject; const PropName: string): Longint; overload;
Gets the value of any ordinal type property, or any property whose value fits in a 32-bit integer, e.g., object, set, character, enumerated, or integer subrange.
function GetPropInfo(TypeInfo: PTypeInfo; const PropName: string): PPropInfo; overload; function GetPropInfo(TypeInfo: PTypeInfo; const PropName: string; AKinds: TTypeKinds): PPropInfo; overload; function GetPropInfo(Instance: TObject; const PropName: string; AKinds: TTypeKinds = []): PPropInfo; overload; function GetPropInfo(AClass: TClass; const PropName: string; AKinds: TTypeKinds = []): PPropInfo; overload;
Returns
the PPropInfo
pointer for a published property or
nil
if the class does not have any such published
property or if the named property does not have the correct type. The
first argument can be an object reference, a class reference, or a
class’s type information (from the TypeInfo
function or ClassInfo
method).
function GetPropList(TypeInfo: PTypeInfo; TypeKinds: TTypeKinds; PropList: PPropList): Integer;
Gets an alphabetized list of PPropInfo
pointers
for the matching properties of an object and returns a count of the
number of properties stored in PropList
. Pass
nil
for the PropList
parameter
to get a count of the number of matching properties.
function GetPropValue(Instance: TObject; const PropName: string; PreferStrings: Boolean = True): Variant;
Gets
the value of a published property as a Variant
.
GetPropValue
incurs more overhead than the other
Get...
functions, but is easier to use. If
PreferStrings
is True,
GetPropValue
will store the property value as a
string, if this is possible.
function GetSetProp(Instance: TObject; PropInfo: PPropInfo; Brackets: Boolean = False): string; overload; function GetSetProp(Instance: TObject; const PropName: string; Brackets: Boolean = False): string; overload;
Gets the value of a set-type property and returns the value as a string. The format of the string is a list of enumerated literals, separated by commas and spaces. You can optionally include square brackets. The format is the same as that used in the Object Inspector.
function GetStrProp(Instance: TObject; PropInfo: PPropInfo): string; overload; function GetStrProp(Instance: TObject; const PropName: string): string; overload;
Gets the
value of a string-type property. The property type can be
tkString
, tkLString
, or
tkWString
. In all cases, the property value is
automatically converted to string
.
function IsStoredProp(Instance: TObject; PropInfo: PPropInfo): Boolean; overload; function IsStoredProp(Instance: TObject; const PropName: string): Boolean; overload;
Returns the value of the
stored
directive. If the stored
directive is a method, IsStoredProp
calls the
method; if it is a field, the field’s value is returned.
function PropType(Instance: TObject; const PropName: string): TTypeKind; overload; function PropType(AClass: TClass; const PropName: string): TTypeKind; overload;
Returns the type kind of a published property or raises an
EPropertyError
exception if the class does not
have a property with the given name.
procedure SetEnumProp(Instance: TObject; PropInfo: PPropInfo; const Value: string); overload; procedure SetEnumProp(Instance: TObject; const PropName: string; const Value: string); overload;
Sets the value of an enumerated-type property, given the name of an
enumerated literal. If the Value
is not the name
of an enumerated literal, SetEnumProp
raises the
EPropertyConvertError
exception. If you have the
ordinal value instead of the literal name, call
SetOrdProp
.
procedure SetInt64Prop(Instance: TObject; PropInfo: PPropInfo; const Value: Int64); overload; procedure SetInt64Prop(Instance: TObject; const PropName: string; const Value: Int64); overload;
Sets the value of a property whose type is Int64
or a subrange that is larger than 32 bits.
procedure SetObjectProp(Instance: TObject; PropInfo: PPropInfo; Value: TObject); overload; procedure SetObjectProp(Instance: TObject; const PropName: string; Value: TObject); overload;
Sets the value of a class-type property. If Value
is not of the correct type for the property,
SetObjectProp
silently ignores the attempt to set
the property value.
procedure SetOrdProp(Instance: TObject; PropInfo: PPropInfo; Value: Longint); overload; procedure SetOrdProp(Instance: TObject; const PropName: string; Value: Longint); overload;
Sets the value of any ordinal-type property, including sets, objects, characters, and enumerated or integer properties.
procedure SetPropValue(Instance: TObject; const PropName: string; const Value: Variant);
Sets the value of a property from a Variant
.
SetPropValue
must be able to convert the
Variant
value to the appropriate type for the
property, or else it raises an
EPropertyConvertError
exception.
procedure SetSetProp(Instance: TObject; PropInfo: PPropInfo; const Value: string); overload; procedure SetSetProp(Instance: TObject; const PropName: string; const Value: string); overload;
Sets the value of a set-type property by interpreting a string as a
list of enumerated literals. SetSetProp
recognizes
the format that GetSetProp
returns. If the format
of Value
is not valid,
SetSetProp
raises an
EPropertyConvertError
exception.
procedure SetStrProp(Instance: TObject; PropInfo: PPropInfo; const Value: string); overload; procedure SetStrProp(Instance: TObject; const PropName: string; const Value: string); overload;
Sets the value of a string-type property. The property type can be
tkString
, tkLString
, or
tkWString
.
The VMT stores a list of pointers for virtual methods and another table in the VMT, which this section refers to as the dynamic method table, lists both dynamic methods and message handlers.
The compiler generates a small negative number for each dynamic method. This negative number is just like a message number for a message handler. To avoid conflicts with message handlers, the compiler does not let you compile a message handler whose message number falls into the range of dynamic method numbers. Once the compiler has done its work, though, any distinction between dynamic methods and message handlers is lost. They both sit in the same table and nothing indicates whether one entry is for a dynamic method and another is for a message handler.
The dynamic method table lists only the dynamic methods and message handlers that a class declares; it does not include any methods inherited from ancestor classes. The dynamic method table starts with a 2-byte count of the number of dynamic methods and message handlers, followed by a list of 2-byte method numbers, followed by a list of 4-byte method pointers. The dynamic method table is organized in this fashion (instead of having a list of records, where each record has a method number and pointer) to speed up searching for a method number. Example 3-6 shows the logical layout of a dynamic method table. As with the other tables, you cannot compile this record, because it is not real Pascal, just a description of what a dynamic method table looks like.
type TDynMethodTable = packed record Count: Word; Indexes: packed array[1..Count] of SmallInt; Addresses: packed array[1..Count] of Pointer; end;
Dispatching a message or calling a dynamic method requires a lookup
of the method or message number in the Indexes
array. The table is not sorted and the lookup is linear. Once a match
is found, the method at the corresponding address is invoked. If the
method number is not found, the search continues with the immediate
base class.
The only time you should even consider using dynamic methods is when all of the following conditions apply:
You are creating a large framework of hundreds of classes.
You need to declare many virtual methods in the classes near the root of the inheritance tree.
Those methods will rarely be overridden in derived classes.
Those methods never need to be called when speed is important.
The tradeoff between virtual and dynamic methods is that virtual method tables include all inherited virtual methods, so they are potentially large. Dynamic method tables do not list inherited methods, so they can be smaller. On the other hand, calling a virtual method is a fast index into a table, but calling a dynamic method requires a search through one or more tables.
In the VCL, dynamic
methods are used only for methods that are called in response to user
interactions. Thus, the slower lookup for dynamic methods will not
impact overall performance. Also, the dynamic methods are usually
declared in the root classes, such as TControl
.
If you do not have a large class hierarchy, you will usually get smaller and faster code by using virtual methods instead of dynamic methods. After all, dynamic methods must store the method number in addition to the method address. Unless you have enough derived classes that do not override the dynamic method, the dynamic method table will end up requiring more memory than the virtual method table.
When Delphi constructs an object, it
automatically initializes strings, dynamic arrays, interfaces, and
Variant
s. When the object is destroyed, Delphi
must decrement the reference counts for strings, interfaces, dynamic
arrays, and free Variant
s and wide strings. To
keep track of this information, Delphi uses initialization records as
part of a class’s RTTI. In fact, every record and array that
requires finalization has an associated initialization record, but
the compiler hides these records. The only ones you have access to
are those associated with an object’s fields.
A VMT points to an initialization table. The table contains a list of
initialization records. Because arrays and records can be nested,
each initialization record contains a pointer to another
initialization table, which can contain initialization records, and
so on. An initialization table uses a TTypeKind
field to keep track of whether it is initializing a string, a record,
an array, etc.
An initialization table begins with the type kind (1 byte), followed by the type name as a short string, a 4-byte size of the data being initialized, a 4-byte count for initialization records, and then an array of zero or more initialization records. An initialization record is just a pointer to a nested initialization table, followed by a 4-byte offset for the field that must be initialized. Example 3-7 shows the logical layout of the initialization table and record, but the declarations depict the logical layout without being true Pascal code.
type { Initialization/finalization record } PInitTable = ^TInitTable; TInitRecord = packed record InitTable: ^PInitTable; Offset: LongWord; // Offset of field in object end; { Initialization/finalization table } TInitTable = packed record {$MinEnumSize 1} // Ensure that TypeKind takes up 1 byte. TypeKind: TTypeKind; TypeName: packed ShortString; DataSize: LongWord; Count: LongWord; // If TypeKind=tkArray, Count is the array size, but InitRecords // has only one element; if the type kind is tkRecord, Count is the // number of record members, and InitRecords[] has a // record for each member. For all other types, Count=0. InitRecords: array[1..Count] of TInitRecord; end;
The master
TInitRecord
for the class has an empty type name
and zero data size. The type kind is always
tkRecord
. The Count
is the
number of fields that need initialization, and the
InitRecords
array contains a
TInitRecord
for each such member. Each
initialization record points to an initialization table that contains
the type kind and type name for the associated member. This
organization seems a little strange, but you can soon grow accustomed
to it.
Most types do not need initialization or finalization, but the following types do:
tkDynArray
DataSize
and Count
are not
meaningful. Delphi decreases the reference count of the array and
frees the array’s memory if the reference count becomes zero.
tkLString
DataSize
and Count
are not
meaningful. Delphi decreases the reference count of the string and
frees the string’s memory if the reference count becomes zero.
tkRecord
DataSize
is the size of the record, and the
Count
is the number of members that need
initialization. The InitRecords
array contains a
TInitRecord
for each member that needs
initialization.
tkVariant
DataSize
and Count
are not
meaningful. Delphi frees any memory associated with the
Variant
data.
The automated section of a class declaration is now obsolete because it is easier to create a COM automation server with Delphi’s type library editor, using interfaces. Nonetheless, the compiler currently supports automated declarations for backward compatibility. A future version of the compiler might drop support for automated declarations.
The OleAuto
unit tells you the details of the
automated method table: The table starts with a 2-byte count,
followed by a list of automation records. Each record has a 4-byte
dispid (dispatch identifier), a pointer to a short string method
name, 4-bytes of flags, a pointer to a list of parameters, and a code
pointer. The parameter list starts with a 1-byte return type,
followed by a 1-byte count of parameters, and ends with a list of
1-byte parameter types. The parameter names are not stored. Example 3-8 shows the declarations for the automated
method table.
const { Parameter type masks } atTypeMask = $7F; varStrArg = $48; atByRef = $80; MaxAutoEntries = 4095; MaxAutoParams = 255; type TVmtAutoType = Byte; { Automation entry parameter list } PAutoParamList = ^TAutoParamList; TAutoParamList = packed record ReturnType: TVmtAutoType; Count: Byte; Types: array[1..Count] of TVmtAutoType; end; { Automation table entry } PAutoEntry = ^TAutoEntry; TAutoEntry = packed record DispID: LongInt; Name: PShortString; Flags: LongInt; { Lower byte contains flags } Params: PAutoParamList; Address: Pointer; end; { Automation table layout } PAutoTable = ^TAutoTable; TAutoTable = packed record Count: LongInt; Entries: array[1..Count] of TAutoEntry; end;
Any class can
implement any number of interfaces. The compiler stores a table of
interfaces as part of the class’s RTTI. The VMT points to the
table of interfaces, which starts with a 4-byte count, followed by a
list of interface records. Each interface record contains the GUID, a
pointer to the interface’s VMT, the offset to the
interface’s hidden field, and a pointer to a property that
implements the interface with the implements
directive. If the offset is zero, the interface property (called
ImplGetter
) must be non-nil
,
and if the offset is not zero, ImplGetter
must be
nil
. The interface property can be a reference to
a field, a virtual method, or a static method, following the
conventions of a property reader (which is described earlier in this
chapter, under Published Properties“). When an object
is constructed, Delphi automatically checks all the interfaces, and
for each interface with a non-zero IOffset
, the
field at that offset is set to the interface’s
VTable
(a pointer to its VMT). Delphi defines the
types for the interface table, unlike the other RTTI tables, in the
System
unit. These types are shown in Example 3-9.
type PInterfaceEntry = ^TInterfaceEntry; TInterfaceEntry = record IID: TGUID; VTable: Pointer; IOffset: Integer; ImplGetter: Integer; end; PInterfaceTable = ^TInterfaceTable; TInterfaceTable = record EntryCount: Integer; // Declare the type with the largest possible size, // but the true size of the array is EntryCount elements. Entries: array[0..9999] of TInterfaceEntry; end;
TObject
implements several methods for accessing
the interface table. See Chapter 5 for the details
of the GetInterface
,
GetInterfaceEntry
, and
GetInterfaceTable
methods.
This
chapter introduces you to a class’s virtual method table and
runtime type information. To better understand how Delphi stores and
uses RTTI, you should explore the tables on your own. The code that
accompanies this book on the O’Reilly web site includes the
Vmt.exe program. The VmtInfo
unit defines a collection of interfaces that exposes the structure of
all the RTTI tables. The VmtImpl
unit defines
classes that implement these interfaces. You can read the source code
for the VmtImpl
unit or just explore the
Vmt program. See the VmtForm
unit to add types that you want to explore, or to change the type
declarations.
You can also use the VmtInfo
interfaces in your
own programs when you need access to the RTTI tables. For example,
you might write your own object persistence library where you need
access to a field class table to map class names to class references.
The interfaces are self-explanatory. Because they use Delphi’s automatic reference counting, you don’t need to worry about memory management, either. To create an interface, call one of the following functions:
function GetVmtInfo(ClassRef: TClass): IVmtInfo; overload; function GetVmtInfo(ObjectRef: TObject): IVmtInfo; overload; function GetTypeInfo(TypeInfo: PTypeInfo): ITypeInfo;
Use the IVmtInfo
interface and its related
interfaces to examine and explore the rich world of Delphi’s
runtime type information. For example, take a look at the
TFont
class, shown in Example 3-10.
type TFont = class(TGraphicsObject) private FColor: TColor; FPixelsPerInch: Integer; FNotify: IChangeNotifier; procedure GetData(var FontData: TFontData); procedure SetData(const FontData: TFontData); protected procedure Changed; override; function GetHandle: HFont; function GetHeight: Integer; function GetName: TFontName; function GetPitch: TFontPitch; function GetSize: Integer; function GetStyle: TFontStyles; function GetCharset: TFontCharset; procedure SetColor(Value: TColor); procedure SetHandle(Value: HFont); procedure SetHeight(Value: Integer); procedure SetName(const Value: TFontName); procedure SetPitch(Value: TFontPitch); procedure SetSize(Value: Integer); procedure SetStyle(Value: TFontStyles); procedure SetCharset(Value: TFontCharset); public constructor Create; destructor Destroy; override; procedure Assign(Source: TPersistent); override; property FontAdapter: IChangeNotifier read FNotify write FNotify; property Handle: HFont read GetHandle write SetHandle; property PixelsPerInch: Integer read FPixelsPerInch write FPixelsPerInch; published property Charset: TFontCharset read GetCharset write SetCharset; property Color: TColor read FColor write SetColor; property Height: Integer read GetHeight write SetHeight; property Name: TFontName read GetName write SetName; property Pitch: TFontPitch read GetPitch write SetPitch default fpDefault; property Size: Integer read GetSize write SetSize stored False; property Style: TFontStyles read GetStyle write SetStyle; end;
Notice that one field is of type IChangeNotifier
.
The Changed
method is declared as dynamic in the
base class, TGraphicsObject
.
TFont
has no published fields or methods, but has
several published properties. Example 3-11 shows the
VMT and type information for the TFont
class. You
can see that the dynamic method table has one entry for
Changed
. The Size
property is
not stored, but the other published properties are. The
Vmt.exe program can show you the same kind of
information for almost any class or type.
Vmt: 40030E78 Destroy: 4003282C FreeInstance: 400039D8 NewInstance: 400039C4 DefaultHandler: 40003CAC Dispatch: 40003CB8 BeforeDestruction: 40003CB4 AfterConstruction: 40003CB0 SafeCallException: 40003CA4 Parent: 40030DA4 (TGraphicsObject) InstanceSize: 32 ClassName: 'TFont' Dynamic Method Table: 40030EE2 Count: 1 40032854 (-3) Method Table: 00000000 Field Table: 00000000 TypeInfo: 40030EF4 InitTable: 40030ED0 TypeName: TypeKind: tkRecord DataOffset: 0 Count: 1 RecordSize: 0 [1] InitTable: 40030E44 TypeName: IChangeNotifier TypeKind: tkInterface DataOffset: 28 AutoTable: 00000000 IntfTable: 00000000 type TFontCharset = 0..255; // otUByte type TColor = -2147483648..2147483647; // otSLong type Integer = -2147483648..2147483647; // otSLong type TFontName; // tkLString type TFontPitch = (fpDefault, fpVariable, fpFixed); // otUByte type TFontStyle = (fsBold, fsItalic, fsUnderline, fsStrikeOut); type TFontStyles = set of TFontStyle; // otUByte type TObject = class // unit 'System' end; type TPersistent = class(TObject) // unit 'Classes' end; type TGraphicsObject = class(TPersistent) // unit 'Graphics' end; type TFont = class(TGraphicsObject) // unit 'Graphics' published property Charset: TFontCharset read (static method 40032CD4) write (static method 40032CDC) nodefault stored True; // index 0 property Color: TColor read (field 20) write (static method 400329AC) nodefault stored True; // index 1 property Height: Integer read (static method 40032B8C) write (static method 40032B94) nodefault stored True; // index 2 property Name: TFontName read (static method 40032BBC) write (static method 40032BD4) nodefault stored True; // index 3 property Pitch: TFontPitch read (static method 40032CA4) write (static method 40032CAC) default 0 stored True; // index 4 property Size: Integer read (static method 40032C30) write (static method 40032C4C) nodefault stored False; // index 5 property Style: TFontStyles read (static method 40032C6C) write (static method 40032C78) nodefault stored True; // index 6 end;