Thursday, September 10, 2009

Delphi 2010 RTTI - The basics

In Delphi 2009 and Prior RTTI was limited to items in the published section.
You had access to pointers for properties, fields and Methods on objects. If you spent some time learning, the old RTTI system was powerful. However, the power of the RTTI in prior versions seem very small when compared to what is now available in Delphi 2010.

In Delphi 2010 you have the option to have RTTI information for almost everything. The choice of what to include is yours, it is controlled by the new $RTTI directive. The default behavior is defined in System.pas shows that, properties and methods are now available via RTTI in both public and published sections, and Fields are available in all of the sections.


Section of System.pas:
{ RTTI Visibility }
type
TVisibilityClasses = set of (vcPrivate, vcProtected, vcPublic, vcPublished);

const
{ These constants represent the default settings built into the compiler.
For classes, these settings are normally inherited from TObject. }
DefaultMethodRttiVisibility = [vcPublic, vcPublished];
DefaultFieldRttiVisibility = [vcPrivate..vcPublished];
DefaultPropertyRttiVisibility = [vcPublic, vcPublished];

type
{ Default RTTI settings }
{$RTTI INHERIT
METHODS(DefaultMethodRttiVisibility)
FIELDS(DefaultFieldRttiVisibility)
PROPERTIES(DefaultPropertyRttiVisibility)}


Raw RTTI Information would be worthless unless you have good way to access the information. The new Unit RTTI.Pas provides a simple and elegant way to access this data. Flexibility of classes, with out the headaches of Memory Management was a key concern of the new design. As such RTTI access is done through a context, once that context is freed all the RTTI objects created are freed.


var
c : TRttiContext;
begin
c := TRttiContext.Create;
try
// RTTI Access code here
finally
c.free;
end;
end;


If you open up RTTI.pas you will need notice that TRttiContext is not an object it is a Record, so don't get confused you should still call .Create and .Free as you would with an object. The reason for this is to free the pool of RTTI objects that may have been created. Atlhough the help file tells you not to free it, I personally like to clean up. Update:
I have had enough questions on this alone, I thought I would explain in more detail.

TRttiContext offers several key methods, which allow you to get access, to the types in the system.


function GetType(ATypeInfo: Pointer): TRttiType; overload;
function GetType(AClass: TClass): TRttiType; overload;
function GetTypes: TArray<TRttiType>;
function FindType(const AQualifiedName: string): TRttiType;


For example all of the following will return the TRttiType class representing, TButton.


var
c : TRttiContext;
t : TRttiType;
begin
c := TRttiContext.Create;
try
// Via a String
t := c.FindType('StdCtrls.TButton');

// Via the pTypeInfo Pointer
t := c.GetType(TButton.ClassInfo);

// Via the class type
t := c.GetType(TButton);
finally
c.Free;
end;
end;


The TRttiType has many functions that allow you to query the members of that type.


function GetMethods: TArray<TRttiMethod>; overload; virtual;
function GetFields: TArray<TRttiField>; virtual;
function GetProperties: TArray<TRttiProperty>; virtual;

function GetMethod(const AName: string): TRttiMethod; virtual;
function GetMethods(const AName: string): TArray<TRttiMethod>; overload; virtual;
function GetField(const AName: string): TRttiField; virtual;
function GetProperty(const AName: string): TRttiProperty; virtual;


So for example the following console application would show all of the methods of TButton.


program Project10;
{$APPTYPE CONSOLE}
uses
StdCtrls, TypInfo, Rtti;

var
c : TRttiContext;
m : TRttiMethod;
begin
c := TRttiContext.Create;
for m in c.GetType(TButton).GetMethods do
begin
Writeln(m.ToString);
end;
c.Free;
readln;
end.

Output:

constructor Create(AOwner: TComponent)
class destructor Destroy
procedure Click
... (Many Lines Removed) ...
procedure AfterConstruction
procedure BeforeDestruction
procedure Dispatch(var Message)
procedure DefaultHandler(var Message)
class function NewInstance: TObject
procedure FreeInstance
class destructor Destroy



Taking this to the next level the following code creates TStringList using the RTTI System, invokes the Add Method and accesses the Text Property. Granted this is not a practical example, it is just designed to show you some of the functionality available. You will notice that values are stored using the type TValue. TValue can store and retrieve any type.


program Project11;
{$APPTYPE CONSOLE}
uses
StdCtrls, TypInfo, Classes, Rtti;

var
c : TRttiContext;
m : TRttiMethod;
t : TRttiInstanceType;
SL : TValue;
Lines : TValue;
begin
c := TRttiContext.Create;
t := (c.FindType('Classes.TStringList') as TRttiInstanceType);
SL := t.GetMethod('Create').Invoke(t.MetaclassType,[]);
t.GetMethod('Add').Invoke(SL,['Hello Do you like my hat?']);
t.GetMethod('Add').Invoke(SL,['I like that hat, what a party hat!']);
Lines := t.GetProperty('Text').GetValue(SL.AsObject);
Writeln(Lines.ToString);
c.Free;
readln;
end.

Output:

Hello Do you like my hat?
I like that hat, what a party hat!



Although it appears that TValue may act like a variant, TValue is not a replacement for Variant. Specifically, If you assign a specific type to a TValue you must retrieve it as that specific type. For example you can't assign an Integer to a TValue and retrieve it as a String, doing so results in an Invalid Type Cast.

This is just taste, future articles will cover this in more detail.

RTTI Article List

8 comments:

  1. Hi Robert!

    Nice article, thank you for share with us.
    Keep posting.


    Cesar Romero

    ReplyDelete
  2. Hello Robert,

    I am coud not await the other articles. I experiment a littel with RTTI and like it very much. One Result is that there is no Parameter list for a function with an open array parameter like

    procedure MyProc(I: Integer; const Values: array of Integer);

    Okay, not a real problem. What I can not under stand is: How can I put an great feature like this in my Produkt and don't left a word about this in the help?

    To find out how to assign an interface to TValue I have to ask stackoverflow.com.

    Ciao Heinz Z.

    ReplyDelete
  3. There is Help documenation, but is far complete.

    I don't have access to Delphi 2010 to test right now. But the following should work.

    V := TValue.From < MyInterfaceType > (MyInterface);

    ReplyDelete
  4. FWIW you doesn't need to 'Create' the TRTTIContext instance. See here:

    http://wings-of-wind.com/2009/09/03/community-pulse-coderage-4-rtti-reloaded/

    ReplyDelete
  5. Actually you do need to call create if TRTTIContext is defined as a local variable as they are not initialized and you can't be sure the interface will be NIL, which could lead to unintended side-effects.

    ReplyDelete
  6. Hi Robert,

    I'm using Delphi 2010 and trying to traverse simple structure by using RTTI. Here is example:

    type
    R = record
    f1: array [0 .. 0] of byte;
    end;

    procedure Test;
    var
    context: TRttiContext;
    field: TRttiField;
    rttiType: TRttiType;
    begin
    context := TRttiContext.Create;

    rttiType := context.GetType(TypeInfo(R));
    for field in rttiType.GetFields do
    if field.FieldType = nil then
    Writeln('Type is nil.');
    context.Free;
    end;
    Do you know why RTTI cannot recognize array type and return nil instead of actual type?

    Thank you in advance,
    Andrey

    ReplyDelete
  7. Current Limitation:
    Any type that does not produce a single word token. Such as Array of Type will not have RTTI.

    You can get around this by using:

    Type
    TMyType = array of Type;
    var
    v : TMyType;

    so that you have a single word token. You can access the elements of an array if it declarded as dynamic or static.

    I noticed your using the trick where you declare the array [0..0] and the must allocate the memory for that space dynamically. This I don't believe is supported.

    ReplyDelete
  8. Robert, very much thanks for that article, it helped me started with the D2010 RTTI!

    BTW, I believe your Project11 should also destroy the dynamically created TStringList object.

    The following fragment would do the trick. Do you maybe know if there's a shorter way to do it?

    c.GetType(SL.TypeInfo).GetMethod('Destroy').Invoke(SL, []);

    ReplyDelete