(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


 
Accessing a published method of a class by its name used as string.

 

This article illustrates :
- 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.

( 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.
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.

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.
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".

 

screenshot

 

sep

 

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 :

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.

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... 

 

sep

 

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)
procedure TForm1.ExecProcByName(aMethodName: string, stringArgument:String);

// And so does the pointer
PProcMethod = ^TProcMethod;
TProcMethod = procedure(stringArgument:String) of object;

procedure TForm1.ExecProcByName(aMethodName: string, stringArgument:String);
  {...}
begin

    TProcMethod(aMethode)(stringArgument); 
end;

Obviously, the Button1Click() must now pass a string to the intermediary, and the targeted method should use it.

procedure TForm1.ShowSomeString(stringArgument);
begin
  ShowMessage(stringArgument);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Self.ExecProcByName('ShowSomeString', 'a string to show');
end;

Better than before, but we still don't have a return. Let's enhance the things a bit...

 

sep

 

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)
Function  ExecFuncByName(aMethodName: string; stringArgument:String): String;

// the pointer
PFuncMethod = ^TFuncMethod;
TFuncMethod = function (stringArgument: String): string of object;

Function  ExecFuncByName(aMethodName: string; stringArgument:String): String;
  {...}
begin

    result := TFuncMethod(aMethode)(stringArgument); 
end;

Button1Click() should now wait for the value returned by the intermediary .

Function  TForm1.ReturnSomeStringFormated(stringArgument): String;
begin
  result := 'Here is the result ' + (stringArgument);
end;

procedure TForm1 .Button1Click(Sender: TObject );
begin
  ShowMessage(
ExecProcByName('ReturnSomeStringFormated', 'a string to format'));
end;

 

sep


Example :
---------

To illustrate all this, let's write a simple application involving the call of procedures and functions by their name.
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.

Before we go on, we will need to think a bit to the arguments manipulation :
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.

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.
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.

  • The form will contain four TButton, used to illustrate the basics :

Two of them will call a function using their caption as string name, and arguments.
(In an intermediary step, the caption will be split between arguments and method name.)

The two others will illustrate the basics of storing pointers into variants (You can see an implementation excerpt bellow).

  • The form will contain a TStringGrid too.
    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.
  • The last part of the main Form will be a TMemo , in which a user will enter lines of "code".

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."
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.

Here is an excerpt of the demo code, showing it :

type
  // -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 :

It will have 6 possible entries :

  • When the user clicks on a cell of the first column ("Operation name"), the menu will only allow to display a summary of the contents of this particular line (a concatenation of everything, enclosed within a "phrase").
     
  • When the user clicks on one of the "timeStamp" cells he will be shown a menu allowing to :
    - 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) ;
  • When the user clicks on a cell of the last column ("number of access)", he will be shown a menu allowing to :
    - 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) ;

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);
// '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;

sep


Finally

We saw how to call published procedures and functions of an object once we know their string name.
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.

I hope you found this helpfull,

Olivier.


 

--- Downloads ---

 

You can download the full project source code here ;

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