Wednesday, October 14, 2009

RTTI & XmlSerial.pas Refactoring - SVN Changes

XmlSerial.pas started as really cool demo of what you can do with RTTI in Delphi 2010. Most of the time demo code is an example that should be looked at learned from and thrown away.

The problem I face is that I need XmlSerial to be far more than a Demo. I need a fully functional serializer that will inter-op with classes using the .NET XmlSerializer.

After committing a couple of my refactoring changes to make it more than just a demo. I realized that I did not want to keep committing all of my changes to the SVN
Trunk.
I really want to keep committing code each day that may not even remotely work. I have some major changes in design planned that will require that for a few days.

So as such I just created a branch for my RTTI work if you want to follow that work, while I refactor to provide an elegant and far more supportable framework. When this work stabilizes I will merge the changes back to Trunk.

Here are the few things that I intend to do to the code:

For example, I intend to create an Adapter for XML Parsing to allow current use of TxmlDocument to continue, but also allow for other types of Parsing that may not be DOM based to work. Specifically I am doing this because, I am concerned with the speed and memory footprint that is required by DOM.

I also want to create a set of adapters and factory that will allow things like TList, Dynamic Arrays and TDataset all to look the same to the serialization engine, so I don't have to write custom code in multiple places. Instead I can just delegate that functionality to a class that will hide the implementation details of how the data is stored. I can see this work being used in a variety of applications outside of XML Serialization.

Do you have specific needs for XML Serialization? I would like to know as now is the time to see if I can address them.

Thursday, October 8, 2009

Xml Serialization - Control via Attributes

The XmlSerial.pas unit now supports the Attributes. I had to fix a couple of bugs to get it working, just in case you grabbed the copy I posted earlier today.

Now you can use 4 Different attributes to control XML serialization process. Specifically XmlRoot, XmlElement, XmlAttribute, and XmlIgnore. I modified the original class I used in the previous XML serialization post to use these.


[XmlRoot('Person')]
TPerson = class(TObject)
private
FLastName: String;
FBirthday: TDateTime;
FMiddleName: String;
FFirstName: String;
function GetFullName: String;
published
public
[XmlAttribute('First_Name')]
property FirstName : String read FFirstName write FFirstName;
[XmlElement('LAST_NAME')]
property LastName : String read FLastName write FLastName;
[XmlIgnore]
property MiddleName : String read FMiddleName write FMiddleName;
property FullName : String read GetFullName;
property Birthday : TDateTime read FBirthday write FBirthday;
procedure Save(FileName : String);
class function Load(FileName : String) : TPerson;
end;

And now the XML that it outputs and imports is:

<Person First_Name="John">
<LAST_NAME>Doe</LAST_NAME>
<Birthday>34744</Birthday>
</Person>


So basically this mimics the behavior of the same attributes in the XML .NET Serialization. Although, it does not support namespaces yet.

RTTI Article List

Xml Serialization - Basic Usage

This post relates to XmlSerial.pas which provides XML serialization and de-serialization using the new RTTI in Delphi 2010.

This class has been built to serialize any public and published property or Field.
This was done to mimic the behavior found in the .NET Xml Serialization with the goal of having a set of objects that can serialize in .NET using Delphi Prism and Win32 using Delphi 2010. The complete goals of what I want to accomplish with XmlSerial.pas look at the source code, I detailed out what still needs to be done.

There are two ways to work with the Xml Serialization, one depends on a Pointer to the Type Information, the other uses Generics to get it form the type specified as a parameter.

// Method 1:
var
o : TypeIWantToSerailze;
s : TXmlTypeSerializer;
x : TXmlDocument;
v : TValue;
begin
s := TXmlTypeSerializer.create(TypeInfo(o));
x := TXmlDocument.Create(Self); // NEVER PASS NIL!!!
s.Serialize(x,o);
x.SaveToFile('FileName.txt');
v := s.Deserialize(x);
o := v.AsType<TypeIWantToSerailze>;
x.free;
s.free;
end;

// Method 2:
var
o : TypeIWantToSerailze;
s : TXmlSerializer<TypeIWantToSerailze>;
x : TXmlDocument;
begin
s := TXmlTypeSerializer<TypeIWantToSerailze>.create;
x := TXmlDocument.Create(Self); // NEVER PASS NIL!!!
s.Serialize(x,o);
x.SaveToFile('FileName.txt');
o := s.Deserialize(x);
x.free;
s.free;
end;

And here is the full code showing how to do this using the generic version.

unit uPerson;
interface

type
TPerson = class(TObject)
private
FLastName: String;
FBirthday: TDateTime;
FMiddleName: String;
FFirstName: String;
function GetFullName: String;
published
public
property FirstName : String read FFirstName write FFirstName;
property LastName : String read FLastName write FLastName;
property MiddleName : String read FMiddleName write FMiddleName;
property FullName : String read GetFullName;
property Birthday : TDateTime read FBirthday write FBirthday;
procedure Save(FileName : String);
class function Load(FileName : String) : TPerson;
end;


implementation
uses
XmlDoc,
Classes,
XmlSerial;

{ TPerson }

function TPerson.GetFullName: String;
begin
result := FFirstName + ' ' + FMiddleName + ' ' + FLastName;
end;

class function TPerson.Load(FileName: String): TPerson;
var
lSerialize : TXmlSerializer<TPerson>;
lOwner : TComponent;
lDoc : TxmlDocument;
begin
lOwner := TComponent.Create(nil); // Required to make TXmlDocument work!
try
lDoc := TXmlDocument.Create(lOwner); // will be freed with lOwner.Free
lDoc.LoadFromFile(FileName);
lSerialize := TXmlSerializer<TPerson>.Create;
try
result := lSerialize.Deserialize(lDoc);
finally
lSerialize.Free;
end;
finally
lOwner.Free;
end;
end;

procedure TPerson.Save(FileName: String);
var
lSerialize : TXmlSerializer<TPerson>;
lOwner : TComponent;
lDoc : TxmlDocument;
begin
lOwner := TComponent.Create(nil); // Required to make TXmlDocument work!
try
lDoc := TXmlDocument.Create(lOwner); // will be freed with lOwner.Free
lSerialize := TXmlSerializer<TPerson>.Create;
try
lSerialize.Serialize(lDoc,Self);
lDoc.SaveToFile(FileName);
finally
lSerialize.Free;
end;
finally
lOwner.Free;
end;
end;

end.

The project that shows how to use this object.

program Project12;

{$APPTYPE CONSOLE}

uses
SysUtils,
Windows,
XmlSerial,
Forms,
uPerson in 'uPerson.pas';

var
P : TPerson;

begin
Application.Initialize;
P := TPerson.Create;
try
P.FirstName := 'John';
P.MiddleName := 'C';
P.LastName := 'Doe';
P.Birthday := EncodeDate(1995,2,14);
P.Save('C:\test.xml');
finally
P.Free;
end;

p := TPerson.Load('C:\test.xml');
try
writeln(P.FullName);
Writeln(DateToStr(P.Birthday));
Readln;
finally
p.Free;
end;
end.

Output:

John C Doe
2/14/1995

Current output of the XML file, notice the Date, it's something I want to change, if you check the roadmap in the xmlserial.pas source.


<TPerson>
<FirstName>John</FirstName>
<LastName>Doe</LastName>
<MiddleName>C</MiddleName>
<Birthday>34744</Birthday>
</TPerson>


Hopefully this covers enough of how to use this to get you started.
I will show how to customize the behavior using attributes in a future post.

RTTI Article List

INI persistence the RTTI way

This post is based around the IniPersit.pas code that I just released.

I commonly create configuration classes to create a common and easy way to access information stored in INI, Registry, or XML. In these examples I will show how I used the new RTTI and Attributes in Delphi 2010 to provide a new way of creating a configuration class that access information stored in an INI file.

Lets first start off showing how to use the New Unit, then we can pull back the covers and show how it works.


unit ConfigSettings;
interface
uses
IniPersist;

type
TConfigSettings = class (TObject)
private
FConnectString: String;
FLogLevel: Integer;
FLogDirectory: String;
FSettingsFile: String;
public
constructor create;
// Use the IniValue attribute on any property or field
// you want to show up in the INI File.
[IniValue('Database','ConnectString','')]
property ConnectString : String read FConnectString write FConnectString;
[IniValue('Logging','Level','0')]
property LogLevel : Integer read FLogLevel write FLogLevel;
[IniValue('Logging','Directory','')]
property LogDirectory : String read FLogDirectory write FLogDirectory;

property SettingsFile : String read FSettingsFile write FSettingsFile;
procedure Save;
procedure Load;

end;

implementation
uses SysUtils;

{ TApplicationSettings }

constructor TConfigSettings.create;
begin
FSettingsFile := ExtractFilePath(ParamStr(0)) + 'settings.ini';
end;

procedure TConfigSettings.Load;
begin
// This loads the INI File Values into the properties.
TIniPersist.Load(FSettingsFile,Self);
end;

procedure TConfigSettings.Save;
begin
// This saves the properties to the INI
TIniPersist.Save(FSettingsFile,Self);
end;

end.


program Project13;

{$APPTYPE CONSOLE}

uses
SysUtils,
IniPersist,
ConfigSettings;

var
Settings : TConfigSettings;

begin
Settings := TConfigSettings.Create;
try
Settings.ConnectString := '\\127.0.0.1\DB:2032';
Settings.LogLevel := 3;
Settings.LogDirectory := 'C:\Log';
Settings.Save;
finally
Settings.Free;
end;

Settings := TConfigSettings.Create;
try
Settings.Load;
WriteLn(Settings.ConnectString);
Writeln(Settings.LogLevel);
Writeln(Settings.LogDirectory);
finally
Settings.Free;
end;
Readln;
end.

Output:

\\127.0.0.1\DB:2032
3
C:\Log

Resulting INI File:

[Database]
ConnectString=\\127.0.0.1\DB:2032
[Logging]
Level=3
Directory=C:\Log


As you can see by the above code there really is not much too it, if you want a field or a property to be stored in the INI File, you just need to add the IniValue Attribute.

TExampleClass = class (TObject)
private
FConnectString: String;
public
[IniValue('Database','ConnectString')]
property ConnectString : String read FConnectString write FConnectString;
end;

The constructor of the INIValue allows you to specify the Section, Name you the field or Property stored in. It also allows you to specify a default value if the name & section did not exist in the INI File.

IniValueAttribute = class(TCustomAttribute)
private
FName: string;
FDefaultValue: string;
FSection: string;
published
constructor Create(const aSection : String;const aName : string;const aDefaultValue : String = '');
property Section : string read FSection write FSection;
property Name : string read FName write FName;
property DefaultValue : string read FDefaultValue write FDefaultValue;
end;

...

constructor IniValueAttribute.Create(const aSection, aName, aDefaultValue: String);
begin
FSection := aSection;
FName := aName;
FDefaultValue := aDefaultValue;
end;


So the magic is really contained in TIniPersist


TIniPersist = class (TObject)
private
class procedure SetValue(aData : String;var aValue : TValue);
class function GetValue(var aValue : TValue) : String;
class function GetIniAttribute(Obj : TRttiObject) : IniValueAttribute;
public
class procedure Load(FileName : String;obj : TObject);
class procedure Save(FileName : String;obj : TObject);
end;

The load and save methods are nearly identical, so lets take a look at load.

class procedure TIniPersist.Load(FileName: String; obj: TObject);
var
ctx : TRttiContext;
objType : TRttiType;
Field : TRttiField;
Prop : TRttiProperty;
Value : TValue;
IniValue : IniValueAttribute;
Ini : TIniFile;
Data : String;
begin
ctx := TRttiContext.Create;
try
Ini := TIniFile.Create(FileName);
try
objType := ctx.GetType(Obj.ClassInfo);
for Prop in objType.GetProperties do
begin
IniValue := GetIniAttribute(Prop);
if Assigned(IniValue) then
begin
Data := Ini.ReadString(IniValue.Section,IniValue.Name,IniValue.DefaultValue);
Value := Prop.GetValue(Obj);
SetValue(Data,Value);
Prop.SetValue(Obj,Value);
end;
end;
for Field in objType.GetFields do
begin
IniValue := GetIniAttribute(Field);
if Assigned(IniValue) then
begin
Data := Ini.ReadString(IniValue.Section,IniValue.Name,IniValue.DefaultValue);
Value := Field.GetValue(Obj);
SetValue(Data,Value);
Field.SetValue(Obj,Value);
end;
end;
finally
Ini.Free;
end;
finally
ctx.Free;
end;
end;

So you can see we basically loop through all the properties and field check for an Attribute, if it exists we get the current value which sets the TypeInfo in the TValue object. Then we assign the string returned from the INI file into the TValue and call SetValue()

Lets look at the two methods called.

class procedure SetValue(aData : String;var aValue : TValue);
class function GetIniAttribute(Obj : TRttiObject) : IniValueAttribute;

Lets look first at SetValue(). You will see that it depends on the TypeInfo being present in the TValue being passed in. We check the TValue and perform the conversions required to convert the String to the Correct Type before storing it
into the TValue.

class procedure TIniPersist.SetValue(aData: String;var aValue: TValue);
var
I : Integer;
begin
case aValue.Kind of
tkWChar,
tkLString,
tkWString,
tkString,
tkChar,
tkUString : aValue := aData;
tkInteger,
tkInt64 : aValue := StrToInt(aData);
tkFloat : aValue := StrToFloat(aData);
tkEnumeration: aValue := TValue.FromOrdinal(aValue.TypeInfo,GetEnumValue(aValue.TypeInfo,aData));
tkSet: begin
i := StringToSet(aValue.TypeInfo,aData);
TValue.Make(@i, aValue.TypeInfo, aValue);
end;
else raise EIniPersist.Create('Type not Supported');
end;
end;

Now lets take a look at GetIniAttribute() the goal of this method is to check to see if a given TRttimember (Field or Property) has the IniValue attribute, and if it does return it, otherwise return NIL.

class function TIniPersist.GetIniAttribute(Obj: TRttiObject): IniValueAttribute;
var
Attr: TCustomAttribute;
begin
for Attr in Obj.GetAttributes do
begin
if Attr is IniValueAttribute then
begin
exit(IniValueAttribute(Attr)); // Exit with a parameter new in Delphi 2010
end;
end;
result := nil;
end;

So all in all, its really not much code, and it make usage simple. Now granted TIniValue is not all that complex, but this situation could be applied to a variety of other applications.

RTTI Article List

RTTI - Practical Examples

Well it took a lot longer than I wanted to get this code out.

There were many more rough edges than I wanted, but after some gentle and firm requests I realized I needed to get this out before the rough edges are finished.

Specifically the XmlSerial.pas has a road map in source code detailing what I still need to get done.

What is released:

  • IniPersist.pas Allows easy mapping of properties and fields to an INI File.

  • XmlSerial.pas Object and Record Serialization and De-serialization to XML

  • ObjDs.pas Read-only mapping of Objects to TClientDataSets.

  • RttiUtils.pas Things to help with common RTTI needs.



How to get the code:
Follow the above links to each unit, or just use SVN.

I am now working on some blog posts to show the how to use this code.

Tuesday, September 22, 2009

TValue in Depth

TValue is one of the key Types in the new RTTI System. We already covered some of the basics in the introduction to TValue. It's now time to pull back the covers and explorer how it was designed so you can exploit the entire power of TValue.


Before we get too far, lets take a look at the field in the interface of TValue.


TValue = record
...
private
FData: TValueData;
end;



Since TValue can store data from any type. I was interested in how this accomplished, it proved useful
in determining how to store data from various unknown types I knew I would be throwing at it in the future.

TValueData is defined as:

TValueData = record
FTypeInfo: PTypeInfo;
// If interface, then a hard-cast of interface to IInterface.
// If heap data (such as string, managed record, array, etc.) then IValueData
// hard-cast to IInterface.
// If this is nil, then the value hasn't been initialized and is empty.
FHeapData: IInterface;
case Integer of
0: (FAsUByte: Byte);
1: (FAsUWord: Word);
2: (FAsULong: LongWord);
3: (FAsObject: TObject);
4: (FAsClass: TClass);
5: (FAsSByte: Shortint);
6: (FAsSWord: Smallint);
7: (FAsSLong: Longint);
8: (FAsSingle: Single);
9: (FAsDouble: Double);
10: (FAsExtended: Extended);
11: (FAsComp: Comp);
12: (FAsCurr: Currency);
13: (FAsUInt64: UInt64);
14: (FAsSInt64: Int64);
15: (FAsMethod: TMethod);
end;


It's just a variant record, that takes 24 bytes of memory.

The key parts are FTypeInfo, and then either FHeapData or one of the variant parts
would be use to store the data.

Knowing this how this is stored helps in understanding the low level routines to set and access
the data stored in a TValue.


TValue = record
...
public
...
// Low-level in
class procedure Make(ABuffer: Pointer; ATypeInfo: PTypeInfo; out Result: TValue); overload; static;
class procedure MakeWithoutCopy(ABuffer: Pointer; ATypeInfo: PTypeInfo; out Result: TValue); overload; static;
class procedure Make(AValue: NativeInt; ATypeInfo: PTypeInfo; out Result: TValue); overload; static;

// Low-level out
property DataSize: Integer read GetDataSize;
procedure ExtractRawData(ABuffer: Pointer);
// If internal data is something with lifetime management, this copies a
// reference out *without* updating the reference count.
procedure ExtractRawDataNoCopy(ABuffer: Pointer);
function GetReferenceToRawData: Pointer;
function GetReferenceToRawArrayElement(Index: Integer): Pointer;
...
end;


Basically, you can use Make() to place any data with type information into a TValue.

Here is an example that placed an Integer and TRect in


program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, Windows, TypInfo,Rtti;

var
IntData : Integer;
IntValue : TValue;

RecData : TRect;
RecValue : TValue;

begin
IntData := 1234;
//Granted it's easier to call IntValue := IntData; but this is an example.
TValue.Make(@IntData,TypeInfo(Integer),IntValue);
Writeln(IntValue.ToString);
RecData.Left := 10;
RecData.Right := 20;
TValue.Make(@RecData,TypeInfo(TRect),RecValue);
Writeln(RecValue.ToString);
readln;
end.

Output:

1234
(record)


When dealing with Deserialization issues I realized I had to recreate record structures when I did not know anything but the TypeInfo. This can be done by calling:


TValue.Make(nil,TypeInfoVar,OutputTValue);


Extracting data can also be done using the low level routines, here is an example of using ExtractRawData.


program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, Windows, TypInfo,Rtti;

var
RecData : TRect;
RecDataOut : TRect;
RecValue : TValue;

begin
RecData.Left := 10;
RecData.Right := 20;
TValue.Make(@RecData,TypeInfo(TRect),RecValue);

RecValue.ExtractRawData(@RecDataOut);
Writeln(RecDataOut.Left);
Writeln(RecDataOut.Right);

readln;
end.

Output:

10
20


You can use things like GetReferenceToRawData() with the SetValue and GetValue on records.


program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, Windows, TypInfo,Rtti;

var
RecData : TRect;
RecValue : TValue;
Ctx : TRttiContext;

begin
Ctx := TRttiContext.Create;
// Create empty record structure
TValue.Make(nil,TypeInfo(TRect),RecValue);
// Set the Left and Right Members, using the pointer to the Record
Ctx.GetType(TypeInfo(TRect)).GetField('Left').SetValue(RecValue.GetReferenceToRawData,10);
Ctx.GetType(TypeInfo(TRect)).GetField('Right').SetValue(RecValue.GetReferenceToRawData,20);
// Extract the record to report the results.
RecValue.ExtractRawData(@RecData);
Writeln(RecData.Left);
Writeln(RecData.Right);
readln;
Ctx.Free;
end.

Output:

10
20


These little examples show how to deal with types that you don't know about at compile time. However, sometimes you do know the type you will be working with at compile time. When this is know it becomes much easier to to work with using a the Generic/Parametrized Type functions that TValue Provides:


class function From<T>(const Value: T): TValue; static;
function AsType<T>: T;
function TryAsType<T>(out AResult: T): Boolean;
function Cast<T>: TValue; overload;


The following example shows From<T>() IsType<T>() and AsType<T>() in use.


program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, Windows, TypInfo,Rtti;

var
RecData : TRect;
RecDataOut : TRect;
RecValue : TValue;
begin
RecData.Left := 10;
RecData.Right := 20;

RecValue := TValue.From<TRect>(RecData);

Writeln(RecValue.IsType<TRect>);

RecDataOut := RecValue.AsType<TRect>;

Writeln(RecDataOut.Left);
Writeln(RecDataOut.Right);
readln;
end.

Output:

TRUE
10
20


There are also other function on TValue that help you with Array Types.


function GetArrayLength: Integer;
function GetArrayElement(Index: Integer): TValue;
procedure SetArrayElement(Index: Integer; const AValue: TValue);


It should be noted that dynamic arrays that are declared like this are currently not
supported:


Var
IntArray : Array of Integer;


But if you can your code to be like this they work fine.


type
TIntArray = Array of Integer;

var
I : TIntArray;
// or
I : TArray<Integer>; {defined in System.pas as: TArray<T> = array of T;}


Another small gotcha is that TValue.FromVariant() is misleading.

I thought it meant that I would be taking a Variant and stuffing it into the TValue, however you will find that it really is not doing that, it's storing the data using originating type, the following code shows how it behaves.


program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, TypInfo,Rtti;

var
vExample : Variant;
Value : TValue;
begin
vExample := 'Hello World';
Value := TValue.FromVariant(vExample);
writeln(GetEnumName(TypeInfo(TTypeKind),Ord(Value.Kind)));
Writeln(value.ToString);

vExample := 1234;
Value := TValue.FromVariant(vExample);
writeln(GetEnumName(TypeInfo(TTypeKind),Ord(Value.Kind)));
Writeln(value.ToString);

readln;
end.

Output:

tkUString
Hello World
tkInteger
1234


If you want to store a Variant into a TValue you can use this method

program Project12;
{$APPTYPE CONSOLE}
uses SysUtils, TypInfo,Rtti;

var
vExample : Variant;
Value : TValue;
begin
vExample := 'Hello World';
Value := TValue.From(vExample);
writeln(GetEnumName(TypeInfo(TTypeKind),Ord(Value.Kind)));
Writeln(value.AsType);

vExample := 1234;
Value := TValue.From(vExample);
writeln(GetEnumName(TypeInfo(TTypeKind),Ord(Value.Kind)));
Writeln(value.AsType);

readln;
end.

Output:

tkVariant
Hello World
tkVariant
1234


There are also other ways to work with TValue that I just don't have the time to cover. I recommend opening up Rtti.pas and exploring the interface to see everything that is available. The items I failed to cover are fairly straight forward.

I hope this give's you a good taste of how TValue works.

RTTI Article List

Thursday, September 17, 2009

Exploring TRttiMember Descendants in depth (Part II) Methods

Today I will be covering TRttiMethod. it is the biggest reason I am so happy with the new RTTI in Delphi 2010. In prior versions of Delphi dynamic method invocation was a black art, that had many limitations. It was painful and was dependent on how your code was compiled. The default behavior of the VCL classes had it disabled. Prior to Delphi 2010 you had to know way too much about the internals of method information structure {$METHODINFO}, to invoke a method dynamically.

Now in Delphi 2010 with default behavior is that RTTI Information for methods is generated for the public and published sections. I don't need to know a thing about how the RTTI information is being stored. There is a very elegant and easy to use API to dynamically query and invoke methods. Which works for all the existing VCL classes.

You must understand TRttiMethod and TValue you have the ability to dynamically invoke most any method.

TRttiMethod descends from TRttiMember, besides the Name & Visibility which was defined in TRttiMember, we may need to know many other aspects about a given method.

A few key properties are defined to allow easy access to this information.


TypInfo.pas
TMethodKind = (mkProcedure, mkFunction, mkConstructor, mkDestructor,
mkClassProcedure, mkClassFunction, mkClassConstructor, mkClassDestructor,
mkOperatorOverload,
{ Obsolete }
mkSafeProcedure, mkSafeFunction);

TCallConv = (ccReg, ccCdecl, ccPascal, ccStdCall, ccSafeCall);

Rtti.pas

TDispatchKind = (dkStatic, dkVtable, dkDynamic, dkMessage, dkInterface);

In TRttiMember
property MethodKind: TMethodKind read GetMethodKind;
property DispatchKind: TDispatchKind read GetDispatchKind;

property CodeAddress: Pointer read GetCodeAddress;
property IsConstructor: Boolean read GetIsConstructor;
property IsDestructor: Boolean read GetIsDestructor;
property IsClassMethod: Boolean read GetIsClassMethod;
// Static: No 'Self' parameter
property IsStatic: Boolean read GetIsStatic;

// Vtable slot for virtual methods.
// Message index for message methods (non-negative).
// Dynamic index for dynamic methods (negative).
property VirtualIndex: Smallint read GetVirtualIndex;
property CallingConvention: TCallConv read GetCallingConvention;
property CodeAddress: Pointer read GetCodeAddress;


Now this information is fairly useless if you don't know the parameters of a given method and a possible result type, there is a property and a function to provide you with this information.


function GetParameters: TArray<TRttiParameter>; virtual; abstract;
property ReturnType: TRttiType read GetReturnType;


Here is an example of looking at these, it looks at the TStringList.AddObject() Method


program Project12;
{$APPTYPE CONSOLE}
uses
Classes, Rtti;
var
ctx : TRttiContext;
t : TRttiType;
Param : TRttiParameter;
AddObjectMethod : TRttiMethod;
begin
ctx := TRttiContext.Create;
t := ctx.GetType(TStringList.ClassInfo);
AddObjectMethod := t.GetMethod('AddObject');
for Param in AddObjectMethod.GetParameters do
begin
Writeln(Param.ToString);
end;
Writeln('Returns:', AddObjectMethod.ReturnType.ToString );
readln;
ctx.Free;
end.

Output:

S: string
AObject: TObject
Returns:Integer


TRttiParameter is contains the information you need to know about a given parameter.


typInfo.pas
...
TParamFlag = (pfVar, pfConst, pfArray, pfAddress, pfReference, pfOut, pfResult);
{$EXTERNALSYM TParamFlag}
TParamFlags = set of TParamFlag;
...

Rtti.pas

TRttiNamedObject
...
property Name: string read GetName;
...

TRttiParameter = class(TRttiNamedObject)
...
function ToString: string; override;
property Flags: TParamFlags read GetFlags;
// ParamType may be nil if it's an untyped var or const parameter.
property ParamType: TRttiType read GetParamType;
...



Now that you have access to the information for a given method you can call it.

In this example you can see two calls, on to constructor and another to the Add() method of TStringList.


program Project12;
{$APPTYPE CONSOLE}
uses
Classes, Rtti, TypInfo;
var
ctx : TRttiContext;
t : TRttiType;
Param : TRttiParameter;
AddMethod : TRttiMethod;
SL : TValue; // Contains TStringList instance

begin
ctx := TRttiContext.Create;
t := ctx.GetType(TStringList.ClassInfo);
// Create an Instance of TStringList
SL := t.GetMethod('Create').Invoke(t.AsInstance.MetaclassType,[]);
// Invoke "Add" and return string representatino of result.
Writeln(t.GetMethod('Add').Invoke(SL,['Hello World']).ToString);
// Write out context.
Writeln((sl.AsObject as TStringList).Text);
readln;
ctx.Free;
end.

Output:

0
Hello World


There are three overloaded versions of Invoke

function Invoke(Instance: TObject; const Args: array of TValue): TValue; overload;
function Invoke(Instance: TClass; const Args: array of TValue): TValue; overload;
function Invoke(Instance: TValue; const Args: array of TValue): TValue; overload;


In the above example you can see I used the TValue and TClass versions, now lets look at a more complex situation using the last overload.

When dealing with method calls that update the parameters, such as those declared with the "var" syntax, the the original array you pass in of parameters is updated with the correct changes.

This program demonstrates how this works:


program Project12;
{$APPTYPE CONSOLE}
uses
Classes, Rtti, TypInfo;
const
AreYouMyMotherISBN = '0-679-89047-5';
type
TBookQuery = class(TObject)
public
function FindBook(ISBN : String;var Title : String) : Boolean;
end;

function TBookQuery.FindBook(ISBN : String;var Title : String) : Boolean;
begin
Writeln('Checking:',ISBN);
// Find one of the books, I get to read every night :-)
if ISBN = AreYouMyMotherISBN then
begin
result := true;
Title := 'Are you my Mother?'
end
else
begin
Title := '';
result := false;
end;
end;

var
ctx : TRttiContext;
BQ : TBookQuery;
Args : Array Of TValue;
Param : TRttiParameter;
FindBook : TRttiMethod;
SL : TValue; // Contains TStringList instance

begin
ctx := TRttiContext.Create;
BQ := TBookQuery.Create;
FindBook := Ctx.GetType(TBookQuery.ClassInfo).GetMethod('FindBook');

SetLength(args,2);
Args[0] := '123'; // an ISBN that won't be found
Args[1] := '';


// Invoke the Method
if FindBook.Invoke(BQ,Args).AsBoolean then
writeln(args[1].ToString)
else
writeln('Not Found');

SetLength(args,2);
Args[0] := AreYouMyMotherISBN; // an ISBN that will be found
Args[1] := '';

// Invoke the Method
if FindBook.Invoke(BQ,Args).AsBoolean then
writeln(args[1].ToString)
else
writeln('Not Found');


readln;
BQ.Free;
ctx.Free;
end.

Output:

Checking:123
Not Found
Checking:0-679-89047-5
Are you my Mother?


Updated:
Well I neglected to be complete when it came to how you can query and access TRttiMethods. I only showed "GetMethod()" on TRttiType but there are four
ways to get information.


// Get's all of the methods on a given class, with the declared ones first.
function GetMethods: TArray<TRttiMethod>; overload; virtual;
// Will return the first method it finds with the given name
function GetMethod(const AName: string): TRttiMethod; virtual;
// Will return all of the methods it finds with a given method, so you can deal with overloads.
function GetMethods(const AName: string): TArray<TRttiMethod>; overload; virtual;
// Will only get methods declared on the given class, and not on parents.
function GetDeclaredMethods: TArray<TRttiMethod>; virtual;


That's really all there is to using TRttiMethod, if you tried to do this in a prior version of Delphi I am sure you will be very happy with the changes. If you never tried don't, just move to Delphi 2010 and use the new functionality. Now you might be why would I use this, well hopefully you will get a taste of that in the practical application articles that are coming soon. However, in my next Article I will cover TValue in Depth.


RTTI Article List