Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How and when are variables referenced in Delphi's anonymous methods captured?

Tags:

This was prompted by How to compare TFunc/TProc containing function/procedure of object?, specifically by David's comment to Barry's question. Since I don't have a Blog to post this to I'm going to ask this question here, and answer it.

Question: When and how are variables referenced in Delphi's anonymous methods captured?

Example:

procedure ProcedureThatUsesAnonymousMethods; var V: string;     F1: TFunc<string>;     F2: TFunc<string>; begin   F1 := function: string         begin           Result := V; // references local variable         end   V := '1';   F2 := function: string         begin           Result := V;         end  V := '2';  ShowMessage(F1);  ShowMessage(F2); end; 

Both ShowMessage are going to show 2. Why? How does V get captured and when?

like image 824
Cosmin Prund Avatar asked Mar 01 '11 13:03

Cosmin Prund


People also ask

What is the advantage of using anonymous methods?

The advantage of an anonymous function is that it does not have to be stored in a separate file. This can greatly simplify programs, as often calculations are very simple and the use of anonymous functions reduces the number of code files necessary for a program.

What are variables in Delphi?

A variable is an identifier whose value can change at run time. Put differently, a variable is a name for a location in memory; you can use the name to read or write to the memory location.

How do you declare a constant variable in Delphi?

To declare a record constant, specify the value of each field - as fieldName: value , with the field assignments separated by semicolons - in parentheses at the end of the declaration. The values must be represented by constant expressions.


1 Answers

When you have a function like the one in the question, where you have an anonymous method accessing a local variable, Delphi appears to create one TInterfacedObject descendant that captures all the stack based variables as it's own public variables. Using Barry's trick to get to the implementing TObject and a bit of RTTI we can see this whole thing in action.

The magic code behind the implementation probably looks like this:

// Magic object that holds what would normally be Stack variables and implements // anonymous methods. type ProcedureThatUsesAnonymousMethods$ActRec = class(TInterfacedObject) public   V: string;   function AnonMethodImp: string; end;  // The procedure with all the magic brought to light procedure ProcedureThatUsesAnonymousMethods; var MagicInterface: IUnknown;     F1: TFunc<string>;     F2: TFunc<string>; begin   MagicInterface := ProcedureThatUsesAnonymousMethods$ActRec.Create;   try     F1 := MagicInterface.AnonMethod;     MagicInterface.V := '1';     F2 := MagicInterface.SomeOtherAnonMethod;     MagicInterface.V := '2';     ShowMessage(F1);     ShowMessage(F2);   finally MagicInterface := nil;   end; end; 

Of course this code doesn't compile. I'm magic-less :-) But the idea here is that an "Magic" object is created behind the scenes and local variables that are referenced from the anonymous method are transformed in public fields of the magic object. That object is uses as an interface (IUnkown) so it gets reference-counted. Apparently the same object captures all used variables AND defines all the anonymous methods.

This should answer both "When" and "How".

Here's the code I used to investigate. Put a TButton on a blank form, this should be the whole unit. When you press the button you'll see the following on screen, in sequence:

  • 000000 (bogus number)
  • 000000 (the same number): This proofs both anonymous methods are actually implemented as methods of the same object!
  • TForm25.Button1Click$ActRec: TInterfacedObject : This shows the object behind the implementation, it's derived from TInterfacedObject
  • OnStack:string: RTTI discovers this field on that object.
  • Self: TForm25: RTTI discovers this field on that object. It's used to get the value of ClasVar
  • FRefCount:Integer - this comes from TInterfacedObject
  • Class Var - result of ShowMessage.
  • On Stack - result of ShowMessage.

Here's the code:

unit Unit25;  interface  uses   Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,   Dialogs, StdCtrls, Rtti;  type   TForm25 = class(TForm)     Button1: TButton;     procedure Button1Click(Sender: TObject);   private     ClassVar: string;   public   end;  var   Form25: TForm25;  implementation  {$R *.dfm}  procedure TForm25.Button1Click(Sender: TObject); var F1: TFunc<string>;     F2: TFunc<string>;      OnStack: string;      i: IInterface;     o: TObject;      RC: TRttiContext;     R: TRttiType;     RF: TRttiField;  begin   // This anonymous method references a member field of the TForm class   F1 := function :string         begin           Result := ClassVar;         end;    i := PUnknown(@F1)^;   o := i as TObject;   ShowMessage(IntToStr(Integer(o))); // I'm looking at the pointer to see if it's the same instance as the one for the other Anonymous method    // This anonymous method references a stack variable   F2 := function :string         begin           Result := OnStack;         end;    i := PUnknown(@F2)^;   o := i as TObject;   ShowMessage(IntToStr(Integer(o)));    ShowMessage(o.ClassName + ': ' + o.ClassType.ClassParent.ClassName);    RC.Create;   try     R := RC.GetType(o.ClassType);     for RF in R.GetFields do       ShowMessage(RF.Name + ':' + RF.FieldType.Name);   finally RC.Free;   end;    ClassVar := 'Class Var';   OnStack := 'On Stack';    ShowMessage(F1);   ShowMessage(F2); end;  end. 
like image 184
Cosmin Prund Avatar answered Sep 18 '22 17:09

Cosmin Prund