d-mozulyov / Tiny.Library

General purpose low level library for Delphi 7-XE10, FreePascal and C++Builder
MIT License
79 stars 17 forks source link

How can I intercept an event type? #6

Closed sglienke closed 4 years ago

sglienke commented 4 years ago

Hi,

I was looking into using Tiny.Invoke for creating the necessary code for my multicast events in Spring4D for non windows platforms which currently use the dead slow System.Rtti.

However I cannot find a way how to intercept a tkMethod type. I need an intercept callback for a TMethodSignature similar to how I can see in the VirtualInterface Demo.

Regards

d-mozulyov commented 4 years ago

Hi, Stefan!

Events (methods) can be intercepted. On the one hand, this is easier to do than interfaces. On the other hand, it's a little more complicated. The main difficulty in intercepting events is to store the overhead. Below I sketched a simple example of how to make a virtual event inheriting from a virtual interface. I hope this will help you understand the features of the library.

program Project1;

{$I TINY.DEFINES.inc}
{$APPTYPE CONSOLE}

uses
  SysUtils,
  Tiny.Types,
  Tiny.Rtti,
  Tiny.Invoke;

type
  TVirtualMethod<TMethod> = class(TRttiVirtualInterface)
  protected
    {$ifdef WEAKINSTREF}[Unsafe]{$endif} FValue: TMethod;
  public
    constructor Create(const ACallback: TRttiVirtualMethodInvokeEvent);
    property Value: TMethod read FValue;
  end;

constructor TVirtualMethod<TMethod>.Create(const ACallback: TRttiVirtualMethodInvokeEvent);
var
  LTypeData: PTypeData;
  LSignature: TRttiSignature;
  LMethods: TRttiVirtualMethodDataDynArray;
  LSystemMethod: ^System.TMethod;
begin
  LTypeData := GetTypeData(TypeInfo(TMethod));
  LSignature.Init(LTypeData.MethodSignature, @DefaultContext);

  SetLength(LMethods, 1);
  LMethods[0].InterceptFunc := LSignature.OptimalInterceptFunc;
  LMethods[0].Method.Name := Pointer(@PTypeInfo(TypeInfo(TMethod)).Name);
  LMethods[0].Method.Index := 0;
  LMethods[0].Method.Signature := InternalCopySignature(LSignature);
  LMethods[0].Method.Context := nil;
  FDefaultInvokeEvent := ACallback;
  LMethods[0].Callback := GetCallback(FDefaultInvokeEvent);

  CreateDirect(nil, LMethods);

  LSystemMethod := Pointer(@FValue);
  LSystemMethod.Data := @FTable.Vmt;
  LSystemMethod.Code := FTable.Vmt[3];
end;

type
  TMyMethod = function(const X, Y, Z: Integer): Integer of object;

var
  MyMethod: TMyMethod;
  MyMethodStorage: IInterface;
  R, X, Y, Z: Integer;

begin
  MyMethodStorage := TVirtualMethod<TMyMethod>.Create(
     function(const AMethod: Tiny.Invoke.TRttiVirtualMethod;
      const AArgs: TArray<Tiny.Rtti.TValue>; const AReturnAddress: Pointer): TValue
    begin
      Result := AArgs[1].AsInteger + AArgs[2].AsInteger + AArgs[3].AsInteger;
    end) as IInterface;
  MyMethod := (MyMethodStorage as TVirtualMethod<TMyMethod>).Value;

  X := 1;
  Y := 2;
  Z := 3;
  Write('Summ(', X, ', ', Y, ', ', Z, ') = ');
  R := MyMethod(X, Y, Z);
  Writeln(R);

  Write('Press Enter to quit');
  Readln;
end.
sglienke commented 4 years ago

Thank you - I tried without putting it into the TRttiVirtualInterface instance because that seemed like unnecessary overhead - can I assume that the intercept stubs that you generate are generated specifically for the layout of that class so that is why I ended up getting AVs when trying this?

Edit: Ah, got it - they are for the layout of TRttiVirtualInterfaceTable - when I use that it works.

d-mozulyov commented 4 years ago

Quite right. The interception was done specifically for virtual interfaces. Because virtual interfaces are more complex and more commonly used.

There are 3 stages of interception:

  1. Depending on the number of the interface function, the corresponding TRttiVirtualMethodData is looked for in the dynamic array. For the case of a virtual event, the function number is 0, so we can not use a dynamic array, but immediately put a pointer to the desired structure.
    TInterception = packed record
    MethodData: PRttiVirtualMethodData;
    Data: packed record end;
    // System.TMethod(MyMethod).Data := @Interception.Data;
    // System.TMethod(MyMethod).Code := Signature.InterceptJumps[0];
    end;
asm
  ...
  mov edx, [eax - 4]
  add edx, ITEM_OFFSET
  jmp [edx] // jmp MethodData.InterceptFunc
end
  1. TRttiVirtualMethodData contains all necessary information about the method. The InterceptFunc function is important, it reads and writes registers relative to the dump. It can be determined either through Signature.OptimalInterceptFunc or Signature.UniversalInterceptFunc. After the used registers are written, the TRttiVirtualMethodData.Callback is called, which is user-defined.
  2. High-level interception can be used if necessary (TValue). Conversion of dump values to TValue array occurs in the TRttiVirtualInterface.InternalEventCallback procedure.
sglienke commented 4 years ago

Thanks again

I was able to make it work to power the multicast events - performance is nice on Windows (still a little slower than the handwritten asm implementation though) and while a couple times slower on Linux it runs circles around what I had using System.Rtti.

d-mozulyov commented 4 years ago

Tell me what multicast events are Maybe I will be able to offer an effective solution

A big request, if there is any code that runs slower in Linux than the standard solution - please open a corresponding ticket. Thank.

sglienke commented 4 years ago

https://bitbucket.org/sglienke/spring4d/src/534d63bccd0891d165d6cee1cbfff18df790661e/Source/Base/Spring.pas#lines-1066

Internal implementation is in https://bitbucket.org/sglienke/spring4d/src/develop/Source/Base/Spring.Events.pas and this is where I added the support for Tiny.Invoke (still in a private branch as there is more going on unrelated to this)

Basically the observer pattern as a ready to use data type and fully compatible with any Delphi event or method reference type.

And no I am just saying that the same code runs a couple times slower on Linux than it does on Windows - but I blame the poor optimization of the Delphi Linux compiler for that. Using Tiny.Invoke is several magnitudes faster than using System.Rtti which was why I tried it in the first place.

d-mozulyov commented 4 years ago

Anonymous method and event with the same signature practically do not require intermediate interception and invocation.

It is very easy to create an event, that invoke an anonymous method:

program Project1;

{$APPTYPE CONSOLE}

type
  TMyMethod = function(const X, Y, Z: Integer): Integer of object;
  TMyAnonimous = reference to function(const X, Y, Z: Integer): Integer;

var
  MyMethod: TMyMethod;
  MyAnonimous: TMyAnonimous;
  R, X, Y, Z: Integer;

begin
  MyAnonimous := function(const X, Y, Z: Integer): Integer
  begin
    Result := X + Y + Z;
  end;

  // MyMethod := MyAnonimous;
  TMethod(MyMethod).Data := PPointer(@MyAnonimous)^;
  TMethod(MyMethod).Code := PPointer(PNativeUInt(TMethod(MyMethod).Data)^ + 3 * SizeOf(Pointer))^;

  X := 1;
  Y := 2;
  Z := 3;
  Write('Summ(', X, ', ', Y, ', ', Z, ') = ');
  R := MyMethod(X, Y, Z);
  Writeln(R);

  Write('Press Enter to quit');
  Readln;
end.

In newer versions of Delphi, the inverse transformation is even easier:

MyAnonimous := MyMethod;

But since what version of Delphi this construction works - I don't know. You may have to do the transformation magic manually.

sglienke commented 4 years ago

You are missing the point but that's ok.