(All
the code listed here has been written and tested
using Borland's D6PE.) (The code coloring is
made thanks to DELPHI CODE CONVERTER by Angus Johnson) Last update: march
2005
© 2005 Olivier Touzot
This article illustrates : ( Links to the source code
and other dowloads are at the bottom of this page) Delphi objects provide a way to access a method
(procedure or function) by it's name (as string), even at runtime. The
only constraint is that the methods targeted must be published
ones. Such a feature's main usage (for us), is to
give our user the possibility to access our procedures and functions "by
hand", at run time, like within a little parser.
To illustrate this feature, we'll first have
a look at the basics, before building a little project using both
procedures and functions. This demo project will consist in a Form showing a stringgrid
and a memo. Basic use of MethodAddress()
:
Let's start with a simple example : Our
code will just try to reach a procedure which will neither take arguments, nor return anything. This method will be stored directly
in the main form's object declaration (TForm1) and will simply show a message box,
with an hardcoded message. The idea is to tell
an object's instance : "Here is a string which holds the name of one
of your published methods. Please, find the actual address of this method and
execute it". We will need an intermediary method, in charge of
finding the one to be executed, and launching it. It will be a procedure,
taking the name of a method as argument.
This procedure will then
have to retrieve a pointer (reference) to a method of an instance
object. Such a pointer will have to point to a "procedure
of object". Once the address retrieved, we will use the
TMethod type provided by Delphi, to "type
cast a method pointer to access the code and data parts of the method
pointer."
TMethod "declares a record that stores the Code and
Data fields." (D6 help).
Here is the whole unit
:
When button1 is clicked, its Click handler will
call our ShowSomeString(); method. This
code doesn't do lots of things, but some additional codding will enhance
it... It is possible to pass
arguments to a method called "by name"
: This implies
just some little changes : In order to pass arguments to a (published) procedure, the
intermediary procedure must receive them too, and pass them to the called
method. Here is it
(new parts are in blue) : // the
"intermediary" methods now receives something (here, a
string) // And so does the
pointer procedure TForm1.ExecProcByName(aMethodName: string, stringArgument:String); Obviously, the Button1Click() must now pass a string to the
intermediary, and the targeted method should use it.
procedure TForm1.ShowSomeString(stringArgument); procedure
TForm1.Button1Click(Sender:
TObject); Better than before, but we still
don't have a return. Let's enhance the things a bit... It is possible to receive
a return too : Some more changes needed, but not that difficult : Our procedure will become a function,and everything concerned must be ready to return something. The only special point is that we need a dedicated
intermediary to deal with functions (our first one doesn't know how to
"return" a result). Here is the new version
(again, new parts are in blue)
: // the
"intermediary" methods now returns something (here, a
string) // the pointer Function ExecFuncByName(aMethodName:
string; stringArgument:String): String; Function
TForm1.ReturnSomeStringFormated(stringArgument):
String; procedure
TForm1
.Button1Click(Sender:
TObject
); To illustrate all this, let's
write a simple application involving the call of procedures and functions
by their name. Before we go on, we will need to think
a bit to the arguments manipulation : However, considering that the datas we will pass to
our procedures may be of any kind, and that we will later access functions,
with returns of anykind, all of this with as few "wrappers" as possible
(one for procedure, one for functions), for the purpose of this little
example, the variant type
seemed to be
ideal. Two of them will call a function using
their caption as string name, and arguments. The two
others will illustrate the basics of storing pointers into variants
(You can see an implementation excerpt
bellow).
We will send these
lines to our little parser, charge for him to extract the code and the
arguments, and then call the wanted function "by string".
The two buttons
illustrating the storage of pointers in variants : Delphi's help says
"By default, Variants can hold values of any type
except records, sets, static arrays, files, classes, class references, and
pointers. In other words, variants can hold anything but structured types
and pointers." Here is an excerpt of
the demo code, showing it : type It will have 6 possible
entries : To keep things
simple, all our menu entries will only call procedures, and pass them a
pointer to the stringGrid. Each called procedure will be in charge of
dealing with the grid, and finding into it the informations it needs. Passing (a
pointer to) the
whole grid each time allows us to write a unique click handler for any
menu entry. It will be of the form (pseudo code) : ExecProcByName
( MenuItem.Caption, ^
StringGrid1);
Provided the code to
be executed is written in the second unit, nothing else will be required
to add items to our menu.
The memo "parser"
:
The other part of the main Form will be a TMemo, in
which a user will enter lines of "code". We will first separate the
code and the arguments, and then call the wanted function "by string".
Being
simplistic, our parser will only allow basic operations, line by line.
Remember that this is just an illustration.
This time, we'll allways use the
functions "intermediary" method, be it to call a function or a
procedure. To avoid problems with the return if a method called is in fact
a procedure, the "function of object" intermediary will allways
initialize it's return to an empty string. On the other side, the
caller will display something only if
the returned value is not an empty string... Here is the whole code needed,
on the "Memo" (main unit) side : procedure TForm1.Button1Click(Sender:
TObject);
We saw how to call
published procedures and functions of an object once we know their string
name. I hope you found
this helpfull, Olivier.
You can download the full project source
code here ;
A compiled version
of the demo is available too ;
And you
can preview the main unit and the parser unit source code. Back to Delphi components and articles
Accessing a published method of a class by
its name used as string.
- Accessing a
method (procedure or function) at runtime, using it's name as string,
including reading the result (when functions are concerned) ;
- Incidentally : Some possible
variants
usage, including storing (and retrieving)
pointers in them.
It
could be used too to ease the
use of dynamic menus which, once build, can be managed (and later
maintained) in just some lines of code.
The stringgrid cells will hold
strings, dateTime and integers, and a dynamic popup menu will allow the
user to do different things, depending on the contents of the cell he has
just clicked upon.
The memo will support a really basic parser,
allowing you to call some methods "by hand".
--------------------------------unit BasicU;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
public
// here goes our "intermediary"
procedure ExecProcByName(aMethodName: string);
published
// Here goes the targeted method
procedure ShowSomeString;
end;
// Our intermediary method requires a pointer to a "procedure of object"
PProcMethod = ^TProcMethod; // will allow us to retrieve a pointer
TProcMethod = procedure of object; // to a method of an instance object
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.ExecProcByName(aMethodName: String);
var PAddr: PProcMethod // PAddr will store the pointer to the method
aMethode: TMethod // See bellow...
begin
PAddr := MethodAddress(aMethodName); // Get the address of the method
If PAddr <> Nil then // Have we received the name of a known method ?
Begin
aMethode.Code := PAddr; // D6 help : "type cast of a method pointer
aMethode.Data := Self; // to access the code and data parts of the method ptr"
TProcMethod(aMethode); // Does the casting -> Calls the method
End;
end;
// The method we will call using it's "string" name :
procedure TForm1.ShowSomeString;
begin
ShowMessage('Here is an hardcoded message');
end;
// Here is the call to the procedure TForm1.ShowSomeString(); :
procedure TForm1.Button1Click(Sender: TObject);
begin
Self.ExecProcByName('ShowSomeString');
end;
end.
------------------------------------------------------------
procedure TForm1.ExecProcByName(aMethodName: string, stringArgument:String);
PProcMethod = ^TProcMethod;
TProcMethod = procedure(stringArgument:String) of object;
{...}
begin
TProcMethod(aMethode)(stringArgument);
end;
begin
ShowMessage(stringArgument);
end;
begin
Self.ExecProcByName('ShowSomeString', 'a string to show');
end;
------------------------------------
Function ExecFuncByName(aMethodName:
string; stringArgument:String): String;
PFuncMethod =
^TFuncMethod;
TFuncMethod =
function (stringArgument:
String): string of object;
{...}
begin
end;
Button1Click() should now wait for the value returned
by the intermediary
.
begin
result := 'Here
is the result ' + (stringArgument);
end;
begin
ShowMessage(ExecProcByName('ReturnSomeStringFormated', 'a string to format'));
end;
Example :
---------
This application will rely upon two units (apart from the
usual "uses" ones).
The
first unit will deal with the GUI (main form), and the second one will
implement a little parser class, with some published procedures and
functions.
We saw that we need a kind of wrapper
to acces our published methods. If we were about to acces
only one procedure, then the arguments used could have been anything,
charge to the procedure to take care of them.
Obviously, it comes with some limits,
mainly the time cost of variant manipulation, together with the memory consumption (16 bytes memory per variant). The other
'most known limit' of variants : the theorical impossibility of dealing
with objects (storing pointers in variants) will be easilly circumvent
with a little imagination, so we'll just ignore it.
(In an intermediary step, the
caption will be split between arguments and method name.)
It will list some
datas (hardcoded for the purpose of the demo) about "activities"
characterized by a name, two timeStamps (start and end of
an observation period) and some integer values ("number of accesses").
We will assign a PopUp menu to the grid, and will show or hide its entries at runTime, depending
on where the user RightClicks on the grid.
The four
informations displayed in each gridline will be stored in objects. This
will allow us to pass the object to our functions when needed.
I they say it, it's true, but a little
imagination may help us bypass this rule :
Pointers and integers
are both stored on 4 bytes. And variants can hold integer, so the solution
will simply be to cast our pointer to an integer
, and store this integer in a variant, then cast it "back" on the other
side, to be able to use it.
// -1- We declare a record
type.
TPointedRec = Record
anInteger: Integer;
aSender:
TObject;
End;
// and a pointer to instances of
it:
PPointedRec = ^TPointedRec;
{...}
//
-----------------------------------------------------------------------------
// --- On the caller side (main
unit) :
//
-----------------------------------------------------------------------------
procedure TForm1.Button3Click(Sender:
TObject);
// Declare a variable of the pointer type we're about to use
:
//
aPointedRec: PPointedRec;
var
aPointedRec: PPointedRec;
v:
Variant;
aPArserInstance: TLittleParser;
begin
inc(appelCount);
// Let's declare an instance of a
TPointedRec record
new(aPointedRec);
aPointedRec.anInteger
:=
appelCount;
aPointedRec.aSender :=
Sender;
// To store our pointer in a
variant, just cast it to a legal type :
// (integers and pointers are both
stored on 4 bytes.)
v :=
Integer(aPointedRec);
// call the wanted function, and use
it's return
aParserInstance :=
TLittleParser.Create;
ShowMessage(
aPArserInstance.ExecFuncByName('HandlePointerValues', v) );
// be kind, free
memory
Dispose(aPointedRec);
end;
//
-----------------------------------------------------------------------------
// --- On the called side (parser
unit) :
//
-----------------------------------------------------------------------------
function
TLittleParser.HandlePointerValues(Const
value: Variant):
Variant;
var
v:
Variant;
i:
Integer;
s: String;
Begin
// The variant received ("value")
contains a pointer to a record of kind TPointedRec;
// To read this record we'll just
need to cast our variant back to an integer,
// then to a ^TPointedRec
:
i := StrToInt(
VarToStr( value ));
// "i" now stores our pointer, but
doesn't know it yet.
// Casting it to some type will do
the trick. Eg: PPointedRec(i) casts it
// to a pointer to a record of kind
TPointedRec.
If
PPointedRec(i).aSender is
TButton
Then s :=
TButton( PPointedRec(i).aSender
).Caption
Else s := '(Sender is not a
button)';
v :=
'This function has been called
'
+
VarToStr( PPointedRec(i).anInteger ) + '
times.'
+ #13#10
+ ' The caller is this time :
'
+ s;
result := v;
End;
The stringGrid menu
:
- View
the duration of this particular operation (working with this line only)
;
- View the total durations of all operations (working on the
whole columns) ;
- View The average duration of all operations
(working on the whole columns) ;
- view the total number of access (sum of this colum values)
;
- view the average number of
access (sum of this colum values divided by
number of values) ;
// 'Execute Memo lines' button
// Will first split each uncommented
line between a method name and an array of
// arguments, then call the function in
charge of launching the wanted method.
// If the method is a function, the
result will be appended at the end of the
// line.
var aParsedLine:
TParsedLine;
// declared in the parser
unit
v:
Variant;
i:
Integer;
aParserInstance: TLittleParser;
begin
aParserInstance
:=
TLittleParser.Create;
For i:= 0 To Memo1.Lines.Count
-1 Do
Begin
aPArsedLine := aParserInstance.GetParsedLine(Memo1.Lines[i]);
If aParsedLine.lnIsValide then
Begin
v :=
aParserInstance.ExecFuncByName(aParsedLine.lnMethodName,
aParsedLine.lnArguments);
If varIsStr(v) Then If v = '' Then exit;
If Not(
VarIsNull(v)
)
Then Memo1.Lines[i] :=
Memo1.Lines[i] + ' :
' + varToStr(v)
Else Memo1.Lines[i] :=
Memo1.Lines[i] + ' : (Unknown
method
:'
+
aParsedLine.lnMethodName + ')';
End;
End;
aParserInstance.Free;
end;
Finally
This may simplify the "GUI" developpement work, and
maintenance.
Obviously, things could be widely enhanced, to make
them fit closer to your needs, like avoiding the use of variants when not needed,
aso.
Siblingly, it would be possible to show more correct captions in
our menu :
For example, an entry could be "Line Summary" instead of
"LineSummary". We would simply have to reformat the menuItem.Caption
(remove spaces) before using it to call the underlying procedure.