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

6 comments:

  1. Thank-you for another interesting article.

    What happens when you call GetMethod on an overloaded procedure, for example TStream.WriteInteger. It has two versions, one that takes an Int64 and the other a LongInt. Which one will it return?

    ReplyDelete
  2. So what is the difference between public and published now? Is it simply that published visibility indicates it is to appear in the DFM and Object Inspector?

    ReplyDelete
  3. Alan: Thanks for point out that oversight... I updated the article (near the end) to cover this information.

    Lachlan: Yes is currently that simple.

    ReplyDelete
  4. An unrelated question I have is this:

    Why and when is TArray<TSomeType> used?
    Is it just to save an extra declaration like
    TMyArray = Array of TSomeType
    ?

    BTW thanks for adding the example output.

    ReplyDelete
  5. When I replaced ISBN datatype from "String" to "Variant", I get exception...

    ReplyDelete
  6. VadimShvarts: I took a look at there is a bug in RTTI.pas when calling methods that have a variant as a parameter.

    I have reported this as QC: 77898

    ReplyDelete