Sunday, April 22, 2012

Creating a delegate at runtime

We know how anonymous methods work in Delphi for a while now. Barry explained how you can use that knowledge to put a delegate into a method pointer and Mason used the enhanced RTTI to get some details about them.

So we know that a delegate type (like TProc<T>) is basically an interface with an Invoke method that has the same signature as the delegate type definition (like procedure(Arg: T)). We also know that there is an TInterfacedObject behind it that is normally compiler created (these classes with $ActRec in their name).

So we basically have all we need to create our own delegate. Before you say: Why are you doing this? You can assign any regular procedure or method to a delegate when the signature is compatible - I will explain that later.

type
  PDelegate = ^IDelegate;
  IDelegate = interface
    procedure Invoke;
  end;

  TDelegate = class(TInterfacedObject, IDelegate)
  private
    FMethod: TMethod;
    procedure Invoke;
  public
    constructor Create(const AMethod: TMethod);
  end;

So we did something similar to what the compiler does when creating this (without the variable capturing of course which we do not need). Now we can put our method into a delegate variable without the built-in type compatibility and using a much more complicated and non type safe way. ;)

var
  proc: TProc<TObject>;
  method: TMethod;
begin
  method.Code := @TForm1.Button1Click;
  method.Data := Self;
  PDelegate(@proc)^ := TDelegate.Create(method);
  proc(Button1);
end;

One detail is missing. The implementation of the Invoke method. This is basically just adjusting the Self pointer and rerouting the call to the object that belongs to the method pointer (I am using my horribly poor x86 asm knowledge here).

procedure TDelegate.Invoke;
asm
  MOV EBX,[EAX].FMethod.Code
  MOV EAX,[EAX].FMethod.Data
  CALL EBX
end;

I guess many of you have more knowledge about that than I have so you might tell me if I made some mistake here. Also someone knows how this should look for x64?

So what is this all about? Why do we need to put a method into a delegate without the built-in assignment?

Remember DSharp multicast events? They work for any method type. But not for anonymous methods. Internally these events are using the ObjAuto unit to create a method pointer. So I can define Event<TNotifyEvent> and add any method that matches the TNotifyEvent signature. Just not anonymous methods that have this signature. So what if I just want to add a simple delegate? Not possible. I would need to define Event<TProc<TObject>> to do so. Internally the TEvent class works with a list of TMethod so I had to convert T which in that case is a anonymous method type (which is an interface you remember) to TMethod and the other way around. To actually call a multicast event there is the Invoke property which is of type T. Since the internally created method pointer is TMethod I needed to stuff that into an anonymous method type. In that case I already had the object which could hold the "magic" interface, the TEvent class itself.

Now there was just a little thing missing. The CreateMethodPointer function from ObjAuto needs a PTypeData for the signature of the method to create. Easy enough for a defined type in case of methods but not so for anonymous methods. So I needed the signature from the Invoke method of the interface and create some TTypeData from it. Important to mention that anonymous methods still do not have the $M+ by default so that does not work for the types defined in SysUtils.pas. You have to define your own types and adding that (so enhanced RTTI get generated for the interface methods). Fortunately Hallvard has explained how these things look and work so I "just" had to use the information from TRttiMethod to create a TTypeData record and fill that with the information needed by CreateMethodPointer.

I have to run some more tests with various signatures to make sure everything works (also add the 64-bit support). Then you will see these changes commited to the svn repository.