Chapter 3. Runtime Type Information

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.

Virtual Method Table

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.

Example 3-1. Structure of a VMT
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.

Published Declarations

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.

Published Methods

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.

Example 3-2. The Layout of the Published Method Table
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;

Published Fields and Field Types

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.

Example 3-3. Layout of the Published Field Table
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

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.

Tip

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

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.

Type Data

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.

Table 3-1. Type Kinds and Their Data

TTypeKind Literal

Associated Data

tkArray

No associated data.

tkChar

Limits of character subrange.

tkClass

Class reference, parent class, unit where class is declared, and published properties.

tkDynArray

No associated data for dynamic arrays.

tkEnumeration

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.

tkFloat

Floating-point type: currency, comp, single, double, or extended (but not Real48).

tkInt64

Limits of integer subrange.

tkInteger

Limits of integer subrange.

tkInterface

Base interface, unit where the interface is declared, and the GUID.

tkLString

No associated data for a long string (AnsiString).

tkMethod

Return type, kind of method, and parameter names and type names.

tkRecord

No associated data.

tkSet

Pointer to the enumerated type of the set elements.

tkString

Maximum length of a short string.

tkUnknown

No associated data.

tkVariant

No associated data.

tkWChar

Limits of wide character subrange.

tkWString

No associated data for a WideString.

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.

Published Properties

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.

Example 3-4. Setting an Ordinal Property
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.

Example 3-5. Setting Default Property Values
// 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.

GetEnumName function
function GetEnumName(TypeInfo: PTypeInfo; Value: Integer): string;

Returns the name of an enumerated literal or an empty string if Value is out of range.

GetEnumProp function
function GetEnumProp(Instance: TObject; PropInfo: PPropInfo):
    string; overload;
function GetEnumProp(Instance: TObject; const PropName: string):
    string; overload;

Returns the name of the enumerated literal that is the property’s value.

GetEnumValue function
function GetEnumValue(TypeInfo: PTypeInfo; const Name: string):Integer;

Returns the ordinal value of an enumerated literal or -1 if the type has no literal with the given name.

GetFloatProp function
function GetFloatProp(Instance: TObject; PropInfo: PPropInfo):
    Extended; overload;
function GetFloatProp(Instance: TObject; const PropName: string):
    Extended; overload;

Gets the value of a property with a floating-point type.

GetInt64Prop function
function GetInt64Prop(Instance: TObject; PropInfo: PPropInfo):
    Int64; overload;
function GetInt64Prop(Instance: TObject; const PropName: string):
    Int64; overload;

Gets the value of a property of type Int64 or any subrange that requires more than 32 bits to represent.

GetMethodProp function
function GetMethodProp(Instance: TObject; PropInfo: PPropInfo):
    TMethod; overload;
function GetMethodProp(Instance: TObject; const PropName: string):
    TMethod; overload;

Gets the value of an event property.

GetObjectProp function
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.

GetObjectPropClass function
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.

GetOrdProp function
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.

GetPropInfo function
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).

GetPropInfos procedure
procedure GetPropInfos(TypeInfo: PTypeInfo; PropList: PPropList);

Gets a list of all the PPropInfo pointers for an object, in declaration order. Use the class’s type data to learn how many published properties the class has, so you can allocate the PropList array.

GetPropList function
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.

GetPropValue function
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.

GetSetProp function
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.

GetStrProp function
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.

GetTypeData function
function GetTypeData(TypeInfo: PTypeInfo): PTypeData;

Returns a pointer to a type’s TTypeData record, given its PTypeInfo pointer.

GetVariantProp function
function GetVariantProp(Instance: TObject; PropInfo: PPropInfo):
    Variant; overload;
function GetVariantProp(Instance: TObject; const PropName: string):
    Variant; overload;

Gets the value of a Variant-type property.

IsPublishedProp function
function IsPublishedProp(Instance: TObject; const PropName: string):
    Boolean; overload;
function IsPublishedProp(AClass: TClass; const PropName: string):
    Boolean; overload;

Returns True if the class has a published property of the given name.

IsStoredProp function
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.

PropIsType function
function PropIsType(Instance: TObject; const PropName: string;
    TypeKind: TTypeKind): Boolean; overload;
function PropIsType(AClass: TClass; const PropName: string;
    TypeKind: TTypeKind): Boolean; overload;

Returns True if the named property exists and has the given type.

PropType function
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.

SetEnumProp procedure
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.

SetFloatProp procedure
procedure SetFloatProp(Instance: TObject; PropInfo: PPropInfo;
  Value: Extended); overload;
procedure SetFloatProp(Instance: TObject; const PropName: string;
  Value: Extended); overload;

Sets the value of a property with a floating-point type.

SetInt64Prop procedure
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.

SetMethodProp procedure
procedure SetMethodProp(Instance: TObject; PropInfo: PPropInfo;
  const Value: TMethod); overload;
procedure SetMethodProp(Instance: TObject; const PropName: string;
  const Value: TMethod); overload;

Sets the value of an event property.

SetObjectProp procedure
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.

SetOrdProp procedure
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.

SetPropValue procedure
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.

SetSetProp procedure
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.

SetStrProp procedure
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.

SetVariantProp procedure
procedure SetVariantProp(Instance: TObject; PropInfo: PPropInfo;
  const Value: Variant); overload;
procedure SetVariantProp(Instance: TObject; const PropName: string;
  const Value: Variant); overload;

Sets the value of a Variant-type property.

Virtual and Dynamic Methods

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.

Example 3-6. The Layout of a Dynamic Method Table
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.

Tip

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.

Initialization and Finalization

When Delphi constructs an object, it automatically initializes strings, dynamic arrays, interfaces, and Variants. When the object is destroyed, Delphi must decrement the reference counts for strings, interfaces, dynamic arrays, and free Variants 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.

Example 3-7. The Layout of the Initialization Table and Record
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:

tkArray

DataSize is the size of each array element, and the Count is the number of elements in the array. Every array element is the same, so the InitRecords array contains one TInitRecord that represents all the array elements. The Offset in the TInitRecord has no meaningful value.

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.

tkInterface

DataSize and Count are not meaningful. Delphi calls the _Release method, which frees the interface object 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.

tkWString

DataSize and Count are not meaningful. Delphi frees the string.

Automated Methods

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.

Example 3-8. The Layout of 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;

Interfaces

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.

Example 3-9. Type Declarations for the Interface Table
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.

Exploring RTTI

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.

Example 3-10. Declaration of the TFont Class
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.

Example 3-11. Runtime Type Information
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;

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset