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.
I suspect your asm code won't work either with good old x86!
ReplyDeleteYou are using the EBX register, which shall be preserved.
A call to PUSH EBX + POP EBX at the end to preserve it would work.
Or perhaps
procedure TDelegate.Invoke;
asm
push [EAX].FMethod.Code
MOV EAX,[EAX].FMethod.Data
end; // here RET will go to FMethod.Code
Thanks! In my tests I had no problems with the EBX register but that was just coincidence then I think.
DeleteAbout interface "fake" class generation, you may take a look at our Open Source implementation, as integrated within mORMot.
ReplyDeleteIt works for Delphi 6 up to XE2, and IMHO it is more easy to follow and use that the one integrated in the SOAP good old implementation, or the latest version of the RTTI. It is also more optimized.
See for instance how we wrote the interface wrapper asm content, at http://blog.synopse.info/post/2012/03/07/Interface-based-services-implementation-details
I'll certainly use it to create stub and mocks interface for Delphi, in the next months.
x64 assembly:
ReplyDeletePUSH RBP
SUB RSP,$20
MOV RBP,RSP
MOV RAX,Self.FMethod.Code
MOV RCX,Self.FMethod.Data
CALL RAX
LEA RSP,[RBP+$20]
POP RBP