Wednesday, October 14, 2009
RTTI & XmlSerial.pas Refactoring - SVN Changes
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
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 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
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
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
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
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
Wednesday, September 16, 2009
TRttiContext.Create() & TRttiContext.Free()
TRttiContext.Create() and TRttiContext.Free() when in fact you don't need too.
Yes you don't need to do it, but yet I do... Why?
First off lets look at the implementation of both.
class function TRttiContext.Create: TRttiContext;
begin
Result.FContextToken := nil;
end;
procedure TRttiContext.Free;
begin
FContextToken := nil;
end;
At first glance there is nothing special, FContextToken is set to NIL in both cases.
So what does setting FContextToken to NIL really do?
We all know that Delphi currently does not have any Garbage collection mechanism. As such having a rich RTTI library that is based on Objects could be problematic.
In my code an I able to such cool things as...
c.GetType().GetField().FieldType.ToString
Without having to set temporary values for each returned object, to free them.
How is this possible without a garbage collector?
Well under the hood you will find TRttiPool that contains all of the RTTI object that are created.
When this pool is freed all of the objects created during RTTI calls are then freed.
The construction and destruction of this pool is controlled by TPoolToken which is the Interface that is stored in FContextToken.
When the TPoolToken is created and freed, the PoolRefCount is maintained, when it reaches zero the TRttiPool is freed.
So why call .Create() and .Free()
So lets start with .Create()
So it's not like calling this code is a huge overhead, yes sure a record is managed and as such it will be initialized, making the call to set FContextToken redundant. well sort of... Although I don't intend every create a pointer to TRttiContext, its possible have the memory space not be initialized as expected, unless you are calling .Create.
I also can't be sure that the implementation will remain as simple. It is very possible that additional code could be called at this point. Keeping my code safe in the future is always critical to me in my designs.
Moving on to .Free()
Well, I like to keep my memory foot print clean. Sure when the TRttiContext variable goes out of scope the exact same behavior will occur.
Very early on when playing with new RTTI I inadvertently caused an access violation in a TCustomAttribute Descendant destructor. With the call to .free() it was easier to see what had caused the problem. Instead of having it occur after the destruction of the Object that had the TRttiContext declared as a field.
And just to stop the obvious comment.... Yes, I know the documentation say's you don't have to call .Free() And implies that the technique is important because it insures that all RTTI Objects are cached and reused, well this would only be true of TRttiContext was declared as a global variable, which I think is a very BAD idea.
Instead I think you should keep your TRttiContext around as long as you need it, but free it when the time is correct.
In short... I find it a matter of personal preference.
Tuesday, September 15, 2009
Exploring TRttiMember Descendants in depth (Part I) Properties and Fields
RTTI information starts with the TRttiMember which provides 4 bits of information about each Member.
- The name
- Visibility (private,protected,public,published)
- The type that the member is associated with
- Attributes associated with that member
This example code shows how to access each of these.
program Project4;
{$APPTYPE CONSOLE}
uses
SysUtils, Rtti, TypInfo;
type
TTest = class(TCustomAttribute)
end;
TBook = class(TObject)
private
FTitle: String;
public
[TTest]
property Title : String read FTitle write FTitle;
end;
var
c : TRttiContext;
m : TRttiMember;
{ TExample }
begin
c := TRttiContext.Create;
m := c.GetType(TBook).GetProperty('Title');
Writeln('Name:',m.Name);
Writeln('Visibility:',GetEnumName(TypeInfo(TMemberVisibility),ord(m.Visibility)));
WriteLn('Parent:',m.Parent.ToString);
Writeln('First Attribute:',m.GetAttributes[0].ToString);
writeln;
m := c.GetType(TBook).GetField('FTitle');
WriteLn('Name:',m.Name);
WriteLn('Visibility:',GetEnumName(TypeInfo(TMemberVisibility),ord(m.Visibility)));
WriteLn('Parent:',m.Parent.ToString);
readln;
c.Free;
end.
Output:
Name:Title
Visibility:mvPublic
Parent:TBook
First Attribute:TTest
Name:FTitle
Visibility:mvPrivate
Parent:TBook
The two basic TRttiMember descendants I want to cover in this blog post is TRttiField and TRttiPropery. Both descendants allow you to get and set the values on an Instance of the given type. They both provide SetValue() and GetValue() methods.
The following code demonstrates how to use both. If you missed my Introduction to TValue
I suggest you take some time to read it as it explains some of the "Magic" behind what is going on here.
program Project10;
{$APPTYPE CONSOLE}
uses
SysUtils, Rtti, TypInfo;
type
TBook = class(TObject)
private
FTitle: String;
public
property Title : String read FTitle write FTitle;
end;
var
c : TRttiContext;
p : TRttiProperty;
f : TRttiField;
book : TBook;
v : TValue;
begin
book := TBook.Create;
try
c := TRttiContext.Create;
p := c.GetType(TBook).GetProperty('Title');
p.SetValue(Book,'Go, Dog, Go!');
v := p.GetValue(Book);
Writeln('Title:',v.ToString);
f := c.GetType(TBook).GetField('FTitle');
f.SetValue(Book,'Green Eggs and Ham');
v := f.GetValue(Book);
Writeln('FTitle:',v.ToString);
readln;
c.Free;
finally
Book.Free;
end;
end.
Output:
Title:Go, Dog, Go!
FTitle:Green Eggs and Ham
With Properties you have to worry about if they are readable or writable, as attempting to call SetValue on a property that is not writable will produce an EPropReadOnly Exception. TRttiProperty has two properties "isReadable" and 'isWritable" that allow you to discover how the property was declared. The following code demonstrates how it works.
program Project10;
{$APPTYPE CONSOLE}
uses
SysUtils, Rtti, TypInfo;
type
TBook = class(TObject)
private
FTitle: String;
FUgly: String;
FAuthor: String;
public
property Title : String read FTitle write FTitle;
property Author : String read FAuthor;
property Ugly : String write FUgly;
end;
var
c : TRttiContext;
p : TRttiProperty;
begin
c := TRttiContext.Create;
try
p := c.GetType(TBook).GetProperty('Title');
WriteLn('Name:',p.Name);
Writeln('IsReadable:',p.IsReadable);
Writeln('IsWritable:',p.IsWritable);
writeln;
p := c.GetType(TBook).GetProperty('Author');
WriteLn('Name:',p.Name);
Writeln('IsReadable:',p.IsReadable);
Writeln('IsWritable:',p.IsWritable);
writeln;
p := c.GetType(TBook).GetProperty('Ugly');
WriteLn('Name:',p.Name);
Writeln('IsReadable:',p.IsReadable);
Writeln('IsWritable:',p.IsWritable);
readln;
finally
c.Free;
end;
end.
Output:
Name:Title
IsReadable:TRUE
IsWritable:TRUE
Name:Author
IsReadable:TRUE
IsWritable:FALSE
Name:Ugly
IsReadable:FALSE
IsWritable:TRUE
That pretty much wraps it up for TRttiField and TRttiProperty, in the next
post I will cover TRttiMethod.
RTTI Article List
Monday, September 14, 2009
Introduction to TValue
For example, in the following program, you can see that the TypeInfo property on
a TValue is the same as the what the TypeInfo() function would return for that type.
program Project10;
{$APPTYPE CONSOLE}
uses
SysUtils, Rtti;
var
v : TValue;
i : Integer;
begin
i := 10;
v := I;
Writeln('Address of TypeInfo on TValue :', IntToStr(Integer(v.TypeInfo)));
Writeln('Address of TypeInfo on Integer:', IntToStr(Integer(TypeInfo(Integer))));
Writeln('Value of I:',I);
Writeln('Value of V:',v.AsInteger);
readln;
end.
Output:
Address of TypeInfo on TValue :4198560
Address of TypeInfo on Integer:4198560
Value of I:10
Value of V:10
There are several implicit operators defined that allow assignment of these types easy. If you have a type that is not on this list, don't worry there is a way to handle these, it just want to dedicate a single blog post to how to do this.
class operator Implicit(const Value: string): TValue;
class operator Implicit(Value: Integer): TValue;
class operator Implicit(Value: Extended): TValue;
class operator Implicit(Value: Int64): TValue;
class operator Implicit(Value: TObject): TValue;
class operator Implicit(Value: TClass): TValue;
class operator Implicit(Value: Boolean): TValue;
There are as set of matching functions to get the data out.
function AsString: string;
function AsInteger: Integer;
function AsExtended: Extended;
function AsInt64: Int64;
function AsObject: TObject;
function AsClass: TClass;
function AsBoolean: Boolean;
Although, this may make you think about the Variant type, conversions from one type to another do not automatically occur. The following code generates an
EInvalidCast Exception, as the stored type is an Integer and not a string.
program Project10;
{$APPTYPE CONSOLE}
uses
Rtti;
var
v : TValue;
i : Integer;
begin
i := 10;
v := I;
// Generates and Invalid Type Cast
Writeln('Value of V:',v.asString);
readln;
end.
To help you in determining what type is in a given TValue you have several properties and functions.
// in TypInfo.pas
TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat,
tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString,
tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray, tkUString,
tkClassRef, tkPointer, tkProcedure);
// part of TValue in rtti.pas
property Kind: TTypeKind
function IsObject: Boolean;
function IsType<T>: Boolean; overload;
The following shows each of these:
program Project10;
{$APPTYPE CONSOLE}
uses
Rtti,TypInfo;
var
v : TValue;
i : Integer;
begin
i := 10;
v := I;
Writeln('V.Kind =',GetEnumName(TypeInfo(TTypeKind),ord(v.Kind)));
Writeln('V.IsType<Integer> = ',v.IsType<Integer>);
Writeln('V.IsType<TObject> = ',v.IsType<TObject>);
Writeln('V.IsObject = ',v.IsObject);
readln;
end.
Output:
V.Kind =tkInteger
V.IsType<Integer> = TRUE
V.IsType<TObject> = FALSE
V.IsObject = FALSE
TValue has a way more functionality to explore, that I will cover in an in depth article later, but the basics needed to be covered before
moving on to TRttiMember for Properties and Fields which is the next article.
RTTI Article List
Saturday, September 12, 2009
Exploring TRttiType and descendants in Depth
I introduced TRttiType in the previous articles.
Like I stated in a prior article, if your type supports, Fields, Properties, and/or Methods. There are a few easy access method to get access to these.
These methods provide access to all of the Fields, Properties, and Methods that have RTTI information, in up coming articles I will go in depth on TRttiField, TRttiProperty, and TRttiMethod.
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;
However, sometimes you one want to access what was declared on that specific type and not in the parent types, this can be done with the following functionality.
function GetDeclaredMethods: TArray<TRttiMethod>; virtual;
function GetDeclaredProperties: TArray<TRttiProperty>; virtual;
function GetDeclaredFields: TArray<TRttiField>; virtual;
If your type supports ancestors, you can get the BaseType through the base type property.
The following code shows how you can walk back up the type tree to find the parent types.
program Project10;
{$APPTYPE CONSOLE}
uses
SysUtils, RTTI;
type
TOneObject = Class(TObject)
end;
TTwoObject = Class(TOneObject)
end;
TThreeObject = Class(TTwoObject)
end;
var
c : TRttiContext;
t : TRttiType;
begin
c := TRttiContext.Create;
try
t := c.GetType(TThreeObject);
writeln(t.Name);
while Assigned(t.BaseType) do
begin
t := t.BaseType;
writeln(t.Name);
end;
finally
c.Free
end;
readln;
end.
Output:
TThreeObject
TTwoObject
TOneObject
TObject
There are 3 sets of properties that give you more information about the given type.
property AsInstance: TRttiInstanceType read GetAsInstance;
property IsInstance: Boolean read GetIsInstance;
property AsOrdinal: TRttiOrdinalType read GetAsOrdinal;
property IsOrdinal: Boolean read GetIsOrdinal;
property IsSet: Boolean read GetIsSet;
property AsSet: TRttiSetType read GetAsSet;
Instance Types are Classes, and the TRttiInstanceType provides the property "MetaclassType" which returns the TClass for the given type. The following example shows how to use this type to construct an instance of an object.
program Project10;
{$APPTYPE CONSOLE}
uses
SysUtils, RTTI;
type
TOneObject = Class(TObject)
end;
TTwoObject = Class(TOneObject)
end;
TThreeObject = Class(TTwoObject)
end;
var
c : TRttiContext;
t : TRttiType;
o : TObject;
begin
c := TRttiContext.Create;
try
t := c.GetType(TThreeObject);
o := t.AsInstance.MetaclassType.Create;
Writeln(o.ClassName);
o.Free;
finally
c.Free
end;
readln;
end.
Output:
TThreeObject
TRttiOrdinalType handles Ordinal Types, such as Integer, Enumerated Type, etc...
It exposes 3 new properties to help you when working with Ordinal types.
TOrdType = (otSByte, otUByte, otSWord, otUWord, otSLong, otULong);
...
property OrdType: TOrdType read GetOrdType;
property MinValue: Longint read GetMinValue;
property MaxValue: Longint read GetMaxValue;
The following example code shows how they behave.
program Project10;
{$APPTYPE CONSOLE}
uses
SysUtils, RTTI,TypInfo;
type
TMyEnum = (enOne,enTwo,enThree);
var
c : TRttiContext;
t : TRttiType;
begin
c := TRttiContext.Create;
try
t := c.GetType(TypeInfo(TMyEnum));
writeln(GetEnumName(TypeInfo(TOrdType),ord(t.AsOrdinal.OrdType)));
writeln(t.AsOrdinal.MinValue);
writeln(t.AsOrdinal.MaxValue);
finally
c.Free
end;
readln;
end.
Output:
otUByte
0
2
Notice, I dipped back into TypInfo.pas to call GetEnumName(), it and it's partner function GetEnumValue(), allow you to work with the names of an enumerated type instead of the ordinal values.
The previous RTTI information that was available in prior versions of Delphi used a pointer to the type information i.e "pTypeInfo"
This is now stored in the .Handle property of the TRttiType, I mention this as the routines in TypInfo.pas still work if you need drop to some lower level functionality.
The TRttiSetType provides one new property "ElementType" which allows you to get the type of the elements of the set. The following code shows an example of this in action.
program Project10;
{$APPTYPE CONSOLE}
uses
SysUtils, RTTI,TypInfo;
type
TMyEnum = (enOne,enTwo,enThree);
TMySet = set of TMyEnum;
var
c : TRttiContext;
t : TRttiType;
begin
c := TRttiContext.Create;
try
t := c.GetType(TypeInfo(TMySet));
Writeln('Element Type:');
writeln(t.AsSet.ElementType.ToString);
finally
c.Free
end;
readln;
end.
Output:
Element Type:
TMyEnum
To be complete the following are provided on TRttiType, but TRttiRecordType only provides one new property, ManagedFields, however I have not found a reason I to need this property.
property AsRecord: TRttiRecordType read GetAsRecord;
property IsRecord: Boolean read GetIsRecord;
But, don't overlook IsRecord as it's very useful.
There are also several other descendant that you can use the standard "is" and "as" functions on get access to additional information associated with the given type.
TRttiInterfaceType
TRttiInt64Type
TRttiMethodType
TRttiClassRefType
TRttiEnumerationType
TRttiStringType
TRttiAnsiStringType
TRttiFloatType
TRttiArrayType
TRttiDynamicArrayType
TRttiPointerType
TRttiProcedureType
The following code shows how you type cast a TRttiType to a TRttiStringType,
to determine what type of string it's associated with.
program Project10;
{$APPTYPE CONSOLE}
uses
SysUtils, RTTI,TypInfo;
type
TmyRecord = record
UniStr : String;
AnsiStr : AnsiString;
WideStr : WideString;
end;
var
c : TRttiContext;
t : TRttiType;
field : TRttiField;
begin
c := TRttiContext.Create;
try
for field in c.GetType(TypeInfo(TMyRecord)).GetFields do
begin
t := field.FieldType;
writeln('Field:',field.Name);
writeln('RttiType:',t.ClassName);
if (t is TRttiStringType) then
Writeln('String Kind:',GetEnumName(TypeInfo(TRttiStringKind),ord((t as TRttiStringType).StringKind)));
Writeln;
end;
finally
c.Free
end;
readln;
end.
Output:
Field:UniStr
RttiType:TRttiStringType
String Kind:skUnicodeString
Field:AnsiStr
RttiType:TRttiAnsiStringType
String Kind:skAnsiString
Field:WideStr
RttiType:TRttiStringType
String Kind:skWideString
In conclusion, there are several other small features of TRttiType but I am not trying to reinvent the documentation, so I will leave that for you to discover.
RTTI Article List
Friday, September 11, 2009
Using Attributes and TCustomAttribute descendants
Attributes are a way of associating additional metadata information with a given type or member of a type.
They can be applied in many places, the following code shows several of the places you can place attributes.
// Declaring an attribute.
TAttrTest = class(TCustomAttribute)
end;
// Places you can use an attribute.
[TAttrTest]
TRec = Record
[TAttrTest]
value : String;
[TAttrTest]
procedure DoThis([TAttrTest]arg1: String);
End;
[TAttrTest]
TMyEnum = (enOne,enTwo,enThree);
[TAttrTest]
TMySet = set of TMyEnum;
[TAttrTest]
TObj = class(TObject)
private
[TAttrTest]
FID: Integer;
public
[TAttrTest]
FName : String;
[TAttrTest]
property Id : Integer read FID write FID;
[TAttrTest]
constructor Create;
[TAttrTest]
destructor Destroy; override;
[TAttrTest]
procedure DoThis;
end;
var
[TAttrTest]
I : Integer;
So how do attributes work in Delphi 2010?
Attributes must descend from TCustomAttribute, if you look at the declaration of TCustomAttribute you will find that there is nothing special.
{ The base class for all custom attributes. Attribute
instances created by the RTTI unit are owned by those
members to which they apply. }
TCustomAttribute = class(TObject)
end;
Passing just the name of the new attribute is only practical in a few cases, usually
you need additional data associated. This is done through the constructor. The following example shows how to setup the call to the constructor in the attribute.
Type
TAttrTest2 = class(TObject)
private
FId : Integer;
public
constructor Create(aID : Integer);
property ID : Integer read FID write FID;
end;
[TAttrTest2(123)]
TMyObject = Class(TObject)
end;
So its simple to declare an Attribute and decorate your types with them. Accessing the attributes stored in a given type involves using rtti.pas, I covered some of the basics of how this works in the previous post
Anything that can have attributes has an associated .GetAttributes() method that returns
the array of the attributes associated with that code.
The following code shows how to access the attributes.
program Project10;
{$APPTYPE CONSOLE}
uses
SysUtils, RTTI;
type
TAttrTest2 = class(TCustomAttribute)
private
FId : Integer;
public
constructor Create(aID : Integer);
property ID : Integer read FID write FID;
end;
[TAttrTest2(1)]
[TAttrTest2(2)]
[TAttrTest2(3)]
TMyObject = Class(TObject)
end;
{ TAttrTest2 }
constructor TAttrTest2.Create(aID: Integer);
begin
FID := aId;
end;
var
c : TRttiContext;
t : TRttiType;
a : TCustomAttribute;
begin
c := TRttiContext.Create;
try
t := c.GetType(TMyObject);
for a in t.GetAttributes do
begin
Writeln((a as TAttrTest2).ID);
end;
finally
c.Free
end;
readln;
end.
Output:
1
2
3
Attributes also have a few other special items that the compiler implements.
If you have an attribute named like this...
type
TestAttribute = class(TCustomAttribute)
end;
It can be refered to in two different ways
[TestAttribute]
TExample = class(Tobject)
end;
[Test]
TExample2 = class(TObject)
end;
The compiler will look for the type, if it is not found it will automatically append "Attribute" to the name and search again. This is done to mimic the .NET behavior.
The compiler also has some special support for types allow you to get TRttiType
easily from the pTypeInfo pointer. The following code segment shows how that pTypeInfo can be interchanged for TRttitype in attributes.
uses
SysUtils, Rtti;
type
TestAttribute = class(TCustomAttribute)
public
constructor Create(aType : TRttiType);
end;
[Test(typeinfo(Integer))]
TEmployee = class(TObject)
end;
There are many practical applications for Attributes, I will explore many of these in later articles.
RTTI Article List
Thursday, September 10, 2009
Delphi 2010 RTTI - The basics
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
Wednesday, September 9, 2009
CodeRage IV - BDE to DBX
My slides and code are available in SVN at my Google Code Site
The BDEtoDBXDataPump was a quick and dirty application but it should work, let
me know if you have a problem and I will update the code in SVN, I had to update it once since DelphiLiver version of this session.
The ComponentConverter will convert TQuery and TTable to TdbxQuery components.
It will also convert TDatabase to TSqlConnection, and few other things to help in the conversion, such as removing the BDE Units and adding in the DBX units.
The ComponentCoverter contains a few little gems such as a DFM Parser.
Tuesday, September 1, 2009
CodeRage Sessions
Although, I just posted a fairly big teaser of my RTTI session, I neglected the other sessions I will be covering at CodeRage.
- Building and Consuming Web Services in Delphi and Delphi Prism
- Building Unit Tests with DUnit
- Converting from BDE to DBX
- Practical Application of RTTI and Attributes
This is a introduction to the topic, if you have done web services you won't learn anything new :-)
This is a introduction to the topic, I will be covering the XML test runner and other features that not visible on the surface. So it may be useful even if you use basic DUnit Tests.
This is a refined version of my DelphiLive material. This has been my most popular session I have ever given, I have recieved more feedback on this than any other. It does not hurt that Andreano Lanusse repeated parts of the session on his various stops in Brazil I will be showing the DBX framework and some code/dfm parsers that do much of the work to convert a BDE application to DBX. I will also show a BDE to DBX datapump.
I have never been more exicited about a feature in Delphi. After seeing this session and the associated blog posts, I hope you realize what I am talking about :-)
I hope to be able to chat with many of you next week at CodeRage.
Delphi 2010 - RTTI & Attributes
Delphi has always had RTTI, but Delphi 2010 has taken RTTI to the next level.
CodeRage is next week, there are two session that will be covering the RTTI system in Delphi 2010.
The first is Barry Kelly's presentation on "Delphi Compiler RTTI Enhancements" if you have time to only see one, then see this one. Barry is the engineer behind the Compiler RTTI Enhancements. His presentation is currently scheduled for Tuesday.
The second, is mine on "Practical Application of RTTI and Attributes" my presentation is currently scheduled for Thursday.
Both are only 40 minutes long, it is enough to get your tips of your toes wet, and I want to jump right in :-)
As such I have prepared a series of blog posts, I will start posting these after my CodeRage Session. Most likely one each day. Although, I tend to hate teasers, I decided to post one :-P
Here is what to expect:
- Delphi 2010 RTTI - The basics
- Using Attributes and TCustomAttribute descendants
- Exploring TRTTIType in depth
- Introduction to TValue
- Exploring TRttiMember Descendants in depth (Part I) Properties and Fields
- Why I call TRttiContext.Create() and TRttiContext.Free()
- Exploring TRttiMember Descendants in depth (Part II) Methods
- TValue in Depth
- INI persistence the RTTI way
- Xml Serialization - Basic Usage
- Xml Serialization - Control via Attributes
- Attributes: Practical Example- Object to Client Dataset
- Types by Package... Dynamic plug-in systems.
The above list may change a bit as I am still editing the material.
I will update this post with links to the blog posts, as the become available if you want to bookmark this page.
I hope to see you at CodeRage, to unleash the Chaos :-)
Friday, August 14, 2009
Wildfires - Santa Cruz Mountains
Tuesday, July 14, 2009
CodeRage sessions, ask your questions now.
Do you have any questions on the topics below?
If so let me know via either a comment to this blog post or an email rlove at peakbiz.com
- Converting from BDE to DBX
Learn how to convert your existing BDE applications to the DBX Architecture. We will cover the architectural and coding differences between BDE and DBX. We will then cover tools that freely available to assist you in this conversion.
Note: This session is similar to the material I presented at DelphiLive and by Andreano Lanusse in Brazil, so if you attended one of these sessions and have feedback, I would be glad to hear it. - Building and Consuming Web Services in Delphi and Delphi Prism
Walk step by step through the process to build and consume web services, with both Delphi Win32 and Prism. - Building Unit Tests with DUnit
Get up to speed with unit testing in DUnit. No prior knowledge of Unit testing or DUnit is required.
Note: If you have tried DUnit and did not understand it, I want to hear from you!
I need the questions by August 13th, as I need to record these sessions before I leave for a back-packing trip to the top of Mt. Whitney
Monday, June 1, 2009
BDE to DBX Data Pump - Small updates
The updates can be found in my googlecode SVN
The bug is most likely in an incorrect mapping here of currency fields. All of the mappings are done through a constant. See the code below, by my comments you can
tell how good I felt about it :-)
//Mapping Best Guess in 10 minutes of work, so I suspect there
//are problems but you could also change this to find tune for your
//specific mapping needs.
FieldTypeMap : Array[TFieldType] of Integer =
( {ftUnknown} TDBXDataTypes.UnknownType, {ftString} TDBXDataTypes.AnsiStringType, {ftSmallint} TDBXDataTypes.Int16Type, {ftInteger} TDBXDataTypes.Int32Type, {ftWord} TDBXDataTypes.UInt16Type, // 0..4
{ftBoolean} TDBXDataTypes.BooleanType, {ftFloat} TDBXDataTypes.DoubleType, {ftCurrency} TDBXDataTypes.BcdType, {ftBCD} TDBXDataTypes.BcdType, {ftDate}TDBXDataTypes.DateType , {ftTime} TDBXDataTypes.TimeType, {ftDateTime} TDBXDataTypes.DateTimeType, // 5..11
{ftBytes}TDBXDataTypes.BytesType , {ftVarBytes} TDBXDataTypes.VarBytesType, {ftAutoInc} TDBXDataTypes.AutoIncSubType, {ftBlob} TDBXDataTypes.BlobType, {ftMemo} TDBXDataTypes.MemoSubType, {ftGraphic} TDBXDataTypes.BlobType, {ftFmtMemo} TDBXDataTypes.MemoSubType, // 12..18
{ftParadoxOle}TDBXDataTypes.UnknownType , {ftDBaseOle}TDBXDataTypes.UnknownType, {ftTypedBinary}TDBXDataTypes.BlobType, {ftCursor}TDBXDataTypes.UnknownType, {ftFixedChar}TDBXDataTypes.CharArrayType, {ftWideString} TDBXDataTypes.WideStringType, // 19..24
{ftLargeint}TDBXDataTypes.Int64Type, {ftADT}TDBXDataTypes.AdtType , {ftArray}TDBXDataTypes.ArrayType , {ftReference}TDBXDataTypes.UnknownType, {ftDataSet}TDBXDataTypes.UnknownType, {ftOraBlob}TDBXDataTypes.BlobType, {ftOraClob} TDBXDataTypes.BlobType, // 25..31
{ftVariant} TDBXDataTypes.UnknownType, {ftInterface}TDBXDataTypes.UnknownType, {ftIDispatch}TDBXDataTypes.UnknownType, {ftGuid}TDBXDataTypes.CharArrayType, {ftTimeStamp} TDBXDataTypes.DateTimeType, {ftFMTBcd} TDBXDataTypes.BcdType, // 32..37
{ftFixedWideChar} TDBXDataTypes.WideStringType, {ftWideMemo} TDBXDataTypes.UnknownType, {ftOraTimeStamp} TDBXDataTypes.OracleTimeStampSubType, {ftOraInterval}TDBXDataTypes.OracleIntervalSubType, // 38..41
{ftLongWord} TDBXDataTypes.Uint32Type, {ftShortint} TDBXDataTypes.Int16Type, {ftByte} TDBXDataTypes.Int8Type, {ftExtended} TDBXDataTypes.DoubleType, {ftConnection} TDBXDataTypes.UnknownType, {ftParams}TDBXDataTypes.UnknownType, {ftStream}TDBXDataTypes.UnknownType); //42..48
Hopefully my stab in the dark on the remaining issue of change ftCurrency to map to TDBXDatatypes.BCDType instead of CurrencyType is correct. I based it on the comments in dbxCommon.pas
///Delphi Currency data type in System unit.
/// Internally managed as aTDBXDataTypes.BCDType
///
CurrencyType = 25;
Anyway if anyone has any problems with BDEtoDBXDatapump please let me know by
posting an issue on Google Code that way I fix them. Please include the source and destination table/database types as it may be critical to figuring out the mapping problem.
Friday, May 29, 2009
Computer Language/Compiler Development
I have to write a research paper on the basics of computer language development.
I am required to do a survey as part of this class.
If any of you would be so kind, would you please take my survey.
Update: Thank you for those that helped.
Friday, May 15, 2009
Delphi Live RTTI Session
The source code and the 3 slides from my RTTI session are on my SVN Repository.
DelphiLive Pictures
Slide Show below...
Thursday, May 14, 2009
BDE to DBX
My slides and code are available in SVN at my Google Code Site
The BDEtoDBXDataPump was a quick and dirty application but it should work, let
me know if you have a problem and I will update the code in SVN.
The ComponentConverter will convert TQuery and TTable to TdbxQuery components.
It will also convert TDatabase to TSqlConnection, and few other things to help in the conversion, such as removing the BDE Units and adding in the DBX units.
DelphiLive - Where is Delphi Going?
Here are some of my raw notes from the first two sessions:
Four Delphi Projects going on at the same time.
- Project Weaver
- Main Themes
- User Experience
- Enhance Connectivity
- Documentation
- IDE usability
- Team Productivity
- Touch
- IDE – Insight (easy Keyboard access to almost everything)
- Improvements to DataSnap
- Firebird Support
- .NET AOP
- SCM Support
- Enhanced RTTI Support
- Attribute Support
- Seamless .NET <> Native communication
- Windows 7 APIs and Direct 2D
- Full Support of SOAP 1.2 Clients
- Project X
- Cross-platform Windows, Mac OS, and Linux
- Cross-platform component library
- DataSnap on all platforms
- Project Chromium, Quality, Quality
- Quality, Quality, Quality
- Pascal Code Formatter
- Documentation of the OTA
- New Data binding model allowing binding to almost any property on a control
- More integration with the database tools.
- Project Commodore
- 64 Bit native
- Full compiler, RTL and VCL support for 64 native
- Multi-Core. Multi-threaded applications.
Sure it's not much more than a bullet point list... But several of the items are very intresting. The order is not defined... Other than Weaver is expected next.
I am sure more details will be coming from the Labs sessions.
We did see a really cool Touch Demo!
Wednesday, May 13, 2009
DelphiLive - Just getting started...
At the break I will be switching and sitting in on the Delphi DataSnap 2009 Deep Dive. I have considered using the new Datasnap more than once. However, I have always ended up switching to something else. I have explored the source, written several demo apps, and feel like I understand it, I am really looking to see another perspective.
On the plane flight out, I was thinking about all of the questions I might be asked during my BDE to DBX session.
One of the questions I realized I could be asked was... How do I convert paradox and/or another BDE database to a DBX supported Database.
There are several options, but I don't feel like there is a really, really easy way to do this.
So hopefully I can finish before my session tomorrow a BDE to DBX DataPump application.... But no commitments :-)
Friday, May 8, 2009
Finalizing DelphiLive Material
I will be presenting:
- Power of the RTTI In Delphi
- Moving from BDE to DBX
I have this love hate relationship with PowerPoint, sometimes I find its the best way to communication information. However as soon as things get technical I really dislike seeing a bunch of code on slides.
In My Power of the RTTI in Delphi Session I currently I have grand total of 3 Slides for this presentation the rest is code.
I will be covering:
- Interacting with Published Information
- Dynamic Method Invocation
- Dynamic Class Creation
- Ideas on how you might use it your applications.
In My BDE to DBX Session, I have several slides, but don't worry I have plenty of code and examples to show.
One of the things I like the best about this presentation won't come till near the end, but at the State of Utah we wrote a simple DFM parser and combined it with Martin Waldenburg Delphi Parser (TmwPasLex) and wrote a simple but effective conversion utility. It saved us tons of time when converting a BDE Application to DBX. The application we converted was not a simple demo program, It has 650+ DFMs. And several more units 500+ with no associated dfm. That does not count any third party code, or our own internally developed components.
I have updated the conversion utility to be slightly more generic as the original was specific to our needs. I will share that code with the attendees.
I hope to see you there!
Wednesday, April 29, 2009
How do you improve Quality?
- The focus is getting the code out the door and then you pick up the pieces.
- The focus is on getting quality code out the door until deadline nears then you revert to number 1.
- The focus is on getting quality code out the door and as deadline nears features may be dropped and the deadline may slip.
At my current employer we have an internal application that is used by 300+ end users.
We used to have:
- ZERO people dedicated to QA. (4-5 Delphi Developers, 1 Mainframe Programmer, no analysts)
- Testing was done by end users testing as they had time.
- We had no automated or unit tests.
- The build process was compile from your own machine and copy it the network, with any developer free to release any code they wanted. Which would sometimes step over the other developers uncommitted code.
- We had two environment's
- Development used by both developers and testing.
- Production
- Exceptions had no details or context to help track down problems.
- We had a very small and painful bug tracking system.
- We would commonly be related to #1 type model above.
We now have:
- We now have three people dedicated to QA. ( 7 Delphi Developers, 1 Database PL/SQL developer, and 1 Analyst)
- We have end users still testing with the time that they have. But we now have better management support to get additional time needed.
- We have a few automated tests, and a few more unit tests, but we really could use many more.
- Our build process runs every hour through Final Builder, alerting us with email when someone checks in bad code.
- We now have 4 environment's
- Development (Dedicated to Developers only)
- TEST where users get first look
- QA a final look before production usually with data refreshed from production
- Production.
- We now use the JCL Debug to help use find those Hard to Reproduce Errors.
- We have a better bug tracking system, but still not nearly as nice as some of the home grown solutions I have used at past employers.
- We are now some where between the #2 and #3 model's above.
Every release we ask what can we do better? We try to learn from our mistakes and implement solutions to prevent problems. We release a new version of our software nearly every month, so we are continually improving, and quality has improved in many ways in the past few years.
However, today I feel awful!
We put out a release on Friday morning last week. I don't think I have ever had a release go bad as this one has. Today we are still working on fixing major problems that were caused by the release.
- Some of which would have been caught by better testing.
- Some of which would have been caught by better management of the information coming in about problems. (i.e. it was reported but not acted on!) Since, I usually manage this information, it's the reason I feel awful.
- Of course like all systems some of them would have taken an unreasonable amount of testing to find.
So I have been thinking, it's time to go back to and look at quality in new and different ways.
I have been making a list for a while that specific to our product on ways to improve quality. Several items on the list are general enough to share with others.
- Build a health check system detects common/possible problems that can be run daily in each environment's, which is very database specific. We have scripts for all of these, but not an automated way to run them.
- Tables without primary keys
- Missing Synonyms
- Invalid Synonyms
- Tables without indexes
- Missing Grants
- Disabled Constraints in Oracle
- Tables that have not been analyzed recently.
- FK Constraints without index on child table (table level lock will be placed on the parent table if these are found)
- Invalid Database Objects
- Plus several that are specific to our schema, i.e. constraints that can't be described in meta data.
- Error Messages (Exceptions)
- Ability to Send error messages to a web service
- Ability to capture a screen shot with the error message.
- Ability to ask users what they are doing when the exception occurred.
- Ability to attach reported errors to items in bug tracking system.
- Ability to Send error messages to a web service
- Code Review to help identify and build additional unit tests around the code.
- Automated Performance Testing Benchmark system. (I think DUnit has something that will work for us)
- Get current Delphi Unit tests into an daily build and smoke test instead of just being run by hand by developers.
I have shared some of my ideas on how to improve quality, as well as some of the things we have done.
How have you improved Quality in the software you write?
I am open to any suggestions, this is just a small attempt to think out side of the box.
Since I just started this blog, I really hope that someone is reading this and has some good ideas they are willing to share.