Search code examples
multithreadingdelphirttimethod-invocation

Store arbitrary data into object instance


Consider the following example:

type

  TTestClass = class
    public
      procedure method1; virtual;
  end;

  TForm2 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
  public
    vmi: TVirtualMethodInterceptor;
    ttc: TTestClass;
  end;

{ Initially SomeFlag is PostponeExecution }
procedure TForm2.FormCreate(Sender: TObject);
begin

  vmi := TVirtualMethodInterceptor.Create(TTestClass);
  ttc := TTestClass.Create;

  vmi.OnBefore :=
    procedure(Instance: TObject; Method: TRttiMethod;
      const Args: TArray<TValue>; out DoInvoke: Boolean;
        out Result: TValue)
    begin
      if { SomeFlag = DirectExecution } then
        DoInvoke := true
      else
      begin
        { SomeFlag := DirectExecution }
        TThread.CreateAnonymousThread(
          procedure
          begin                
            // Invoke() will trigger vmi.OnBefore 
            // because Instance is the proxified object
            // I want to keep "Self" to be the proxified object
            Method.Invoke(Instance, Args);
          end
        ).Start;
      end
    end;

  vmi.Proxify(ttc);

  ttc.method1;

end;

{ TTestClass }

procedure TTestClass.method1;
begin
  //  Do something async
end;

procedure TForm2.FormDestroy(Sender: TObject);
begin
  vmi.Unproxify(ttc);
  vmi.Free;
  ttc.Free;
end;

I want hooked virtual method to execute itself in a thread i.e. delay/defer its execution.

For this purpose I use TVirtualMethodInterceptor to intercept virtual methods of a given class. When a virtual method is invoked vmi.OnBefore is fired. This is simplified representation of my idea:

Call_VirtualMethod(method1) -> OnBefore_fires_1 -> CreateThread_and_InvokeAgain -> OnBefore_fires_2 -> DoInvoke := true (i.e. directly execute the method)

Explanation:

  1. Initially SomeFlag has a value of PostponeExecution.

  2. The first call to ttc.method1 will trigger OnBefore event (OnBefore_fires_1). The method will not execute, because SomeFlag is PostponeExecution. Therefore a thread will be created which will set SomeFlag to DirectExecute and will invoke the same method again, but within the thread's context.

  3. Then OnBefore fires again (because Instance is the proxified object i.e. the method is the hooked method). This time SomeFlag is DirectExecute and the method will be invoked.

I use proxified object (Instance var) when invoking the method, because I want "Self" to point to it. This way if method1 calls other virtual method of the same class the later will also be automatically executed in a thread.

For this to happen I need to store the flag somewhere i.e. indicate OnBefore's second call what to do. My question is how/where to store "SomeFlag" so it's accessible during the two calls of OnBefore? The solution should be cross-platform. Suggestions/other solutions are also welcome.

I imagine it can be done with VMT patching (link1, link2, link3), but VirtualProtect is a Windows only function so cross-platform requirement would be violated.

Any idea is highly appreciated.

What's this all about:

Imagine you can have this kind of class in Delphi:

TBusinessLogic = class
  public
    // Invokes asynchronously
    [InvokeType(Async)]
    procedure QueryDataBase;

    // Invokes asynchronously and automatically return asocciated ITask (via OnBefore event)
    [InvokeType(Await)]
    function DownloadFile(AUrl: string): ITask;

    // This method touches GUI i.e. synchonized
    [InvokeType(VclSend)]
    procedure UpdateProgressBar(AValue: integer);

    // Update GUI via TThread.Queue
    [InvokeType(VclPost)]
    procedure AddTreeviewItem(AText: string);

end;

...

procedure TBusinessLogic.QueryDataBase;
begin
  // QueryDataBase is executed ASYNC (QueryDataBase is tagged as Async)
  // Do heavy DB Query here

  // Updating GUI is easy, because AddTreeviewItem is tagged as VclPost
  for SQLRec in SQLRecords do
    AddTreeviewItem(SQLRec.FieldByName["CustomerName"].asString);
end;

This approach really simplifies threading and synchronization. No more ducktyping TThread.Synchronize(), TThread.Queue() etc. You just focus on the business logic and call appropriate methods - OnBefore event does the "dirty" job for you. Very close to Await methods in C#.

This is the main idea!

UPDATE: I re-edited the entire question to make it more clear.


Solution

  • Your approach is wrong. What you try to do is basically call the virtual method but without going through the interceptor again. Since the interceptor itself has registered stubs inside the VMT calling the method through invoke will hit the interceptor stub again causing a recursion.

    I have done this in the past in the Spring4D interception by doing the invoking on a lower level using the Rtti.Invoke routine.

    This is how you do it:

    procedure DirectlyInvokeMethod(Instance: TObject; Method: TRttiMethod;
      const Args: TArray<TValue>);
    var
      params: TArray<TRttiParameter>;
      values: TArray<TValue>;
      i: Integer;
    begin
      params := Method.GetParameters;
      SetLength(values, Length(Args) + 1);
      values[0] := Instance;
    
      // convert arguments for Invoke call (like done in the DispatchInvoke methods
      for i := Low(Args) to High(Args) do
        PassArg(params[i], args[i], values[i + 1], Method.CallingConvention); // look at Rtti.pas for PassArg
    
      Rtti.Invoke(Method.CodeAddress, values, Method.CallingConvention, nil);
    end;
    

    Since you are calling this asynchronously I left handling of functions out - otherwise you have to check the ReturnType of Method to pass the correct handle, here we are just passing nil.

    For the PassArg routine look into the System.Rtt.pas.

    Then you just call it like this:

    vmi.OnBefore :=
      procedure(Instance: TObject; Method: TRttiMethod;
        const Args: TArray<TValue>; out DoInvoke: Boolean;
          out Result: TValue)
      begin
        DoInvoke := Method.Parent.Handle = TObject.ClassInfo; // this makes sure you are not intercepting any TObject virtual methods
        if not DoInvoke then // otherwise call asynchronously
          TThread.CreateAnonymousThread(
            procedure
            begin
              DirectlyInvokeMethod(Instance, Method, Args);
            end).Start;
      end;
    

    Keep in mind that any var or out parameters are a no go for this approach for obvious reasons.