Jump to content
Andre1

Dynamic method call with record references

Recommended Posts

Hi,

I have written a wrapper library to make use of Firemonkey from the programming language D. A simple example looks like this:

 

import delta.core;
import System.Classes, System.UITypes, FMX.Forms, FMX.Types, FMX.Memo;

void main()
{
    deltaLibrary.load(`.\views\Win32\Debug\Project1.dll`);
   
    Application.Initialize;
    Application.MainForm = TCustomForm.CreateNew(Application);
    Application.MainForm.Caption = "Sample";
   
    auto memo = TMemo.Create(Application.MainForm);
    memo.Lines.Add("Hello World!");
    memo.Align = TAlignLayout.Client;
    memo.Parent = Application.MainForm;
   
    Application.MainForm.Show();
    Application.Run();
}

The Project1.dll is written in Delphi and provides generic functions for dynamically creating Delphi class instances and calling their methods.
While this is working fine, I have issues calling method which have record arguments.

 

Example TCanvas.DrawLine has two time the record type TPointF:

 

procedure DrawLine(const APt1, APt2: TPointF; const AOpacity: Single); overload;

Within my DLL I have a generic function to call instance methods which have exactly this kind of arguments (record, record, single)

 

procedure executeInstanceMethodReturnNoneArgsStructStructFloat(const Reference: Integer;
  const AName: PAnsiChar; Reference2, Reference3: Integer; s: Single); stdcall;
begin
    // What needs to be done here
    executeInstanceMethod(Reference, string(AName), [???]);
end;

function executeInstanceMethod(const Reference: Integer; const AName: string;
  const Args: array of TValue): TValue;
var
  context: TRttiContext;
  instType: TRttiInstanceType;
  obj: TObject;
begin
  context := TRttiContext.Create;
  try
    try
      obj := TObject(Reference);
      instType := (context.GetType(obj.ClassType) as TRttiInstanceType);
      result := instType.GetMethod(AName).Invoke(obj, Args);
    except
      on E: Exception do
        writeln(E.ClassName + ' error raised, with message : ' + E.Message);
    end;
  finally
    context.Free;
  end;
end;

(Argument Reference has the class reference to e.g. TCanvas, AName has the method name e.g. DrawLine. After that
the actual methods of DrawLine are following).

 

From my application written in D I want to call executeInstanceMethodReturnNoneArgsStructStructFloat and pass references

to records (D structs binary compatible with the Delphi records).

 

Within the Delphi procedure executeInstanceMethodReturnNoneArgsStructStructFloat these record references

should be converted into something which can be used by TRttiMethod.Invoke.

 

Is this technically possible and can you give me a hint or an example how the coding in executeInstanceMethodReturnNoneArgsStructStructFloat should look like? 

 

Kind regards
André

 

Share this post


Link to post

When record argument is declared as const, Delphi always passes it by reference (pointer). So you should try to feed the function with pointers. BTW, if you use Reference as pointer, don't declare it as Integer.

Share this post


Link to post

Thanks I tried your suggestion but it didnt' worked. An invalid type conversion is show. It seems just passing untyped pointer of the record is not possible in the invoke method.
I simplified the problem by creating the example just as Delphi console application:

 

 

program foo;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  System.SysUtils, System.Classes, System.rtti, System.TypInfo;

type
  TDateRec = record
    Year: Integer;
  end;

  TExample = class
  public
    function Calc(const dateRec: TDateRec): Integer;
  end;

function TExample.Calc(const dateRec: TDateRec): Integer;
begin
  result := dateRec.Year;
end;

var
  ex: TExample;
  dateRec: TDateRec;
  intResult: Integer;

function executeInstanceMethod(const Reference: Integer; const AName: string;
  const Args: array of TValue): TValue;
var
  context: TRttiContext;
  instType: TRttiInstanceType;
  obj: TObject;
begin
  context := TRttiContext.Create;
  try
    try
      obj := TObject(Reference);
      instType := (context.GetType(obj.ClassType) as TRttiInstanceType);
      result := instType.GetMethod(AName).Invoke(obj, Args);
    except
      on E: Exception do
        writeln(E.ClassName + ' error raised, with message : ' + E.Message);
    end;
  finally
    context.Free;
  end;
end;

function executeInstanceMethodReturnIntArgsStruct(const Reference: Integer;
  const AName: PAnsiChar; Reference2: Pointer): Integer;
begin
  // Just passing the untyped pointer does not work ?

  result := executeInstanceMethod(Reference, string(AName), [Reference2])
    .AsInteger;
end;

begin

  try
    ex := TExample.Create;
    dateRec.Year := 2022;
    executeInstanceMethodReturnIntArgsStruct(Integer(ex), 'Calc', @dateRec);

  except
    on E: Exception do
      writeln(E.ClassName, ': ', E.Message);
  end;

end.

 

Kind regards
André

Share this post


Link to post

Where the error occurs and what it is?

I repeat: don't cast pointers to Integer! It's incorrect on x64

Share this post


Link to post

Thanks. Yes, for the moment my library targets x86 only. In a later iteration I will make it architecture independent.

 

Below System.Rtti.TRttiMethod.Invoke there is System.Rtti.TValue.Cast which seems unable to

handle the untyped pointer to the Record.

Exception EInvalidCast is thrown.

 

Is the Rtti Invoke method capable to invoke the method with an untyped pointer or have I somehow

convert the untyped pointer to a typed pointer by making use of some other Rtti functions?

 

Kind regards

André

 

 

Share this post


Link to post

By adding 2 more integer fields to record:

 

  TDateRec = record
    Year: Integer;
    Month: Integer;
    Day: Integer;
  end;

 

the error messages changes to

"var- and out arguments must exactly match".

 

Is my understanding correct, that rtti does not support calling the method and passing the record reference as untyped pointer?

 

Kind regards
André

Share this post


Link to post
On 6/23/2022 at 10:37 AM, Fr0sT.Brutal said:

When record argument is declared as const, Delphi always passes it by reference (pointer).

Hmm, seems I remembered wrong.

No declaration => value always

Var/out => reference always

Const => value if size <= pointer size (4 bytes on all platforms since Rio), reference if larger

Share this post


Link to post

Okay I dove a little bit deeper (never touched RTTI before). It seems strictly tied to types so free pointer casting won't help. However I likely managed to get correct results

 

program foo;

{$APPTYPE CONSOLE}


uses
  System.SysUtils,
  System.Classes,
  System.rtti,
  System.TypInfo;

type
  // enlarge record to be always passed by ref
  TDateRec = record
    Year: Integer;
    Dummy: Double;
  end;

  TExample = class
  public
    function Calc(const dateRec: TDateRec): Integer;
  end;

function TExample.Calc(const dateRec: TDateRec): Integer;
begin
  result := dateRec.Year + round(daterec.Dummy); // use 2nd field
end;

var
  ex: TExample;
  dateRec: TDateRec;
  intResult: Integer;

function executeInstanceMethod(Reference: NativeUInt; const AName: string;
  const Args: array of TValue): TValue;
var
  context: TRttiContext;
  instType: TRttiInstanceType;
  params: TArray<TRttiParameter>;
  obj: TObject;
  Arg: TValue;
  p: Pointer;
begin
  context := TRttiContext.Create;
  try
    try
      obj := TObject(Reference);
      instType := (context.GetType(obj.ClassType) as TRttiInstanceType);
      // get the actual pointer - didn't find another way to TValue.GetAsPointer
      Args[0].ExtractRawData(@p);
      // learn method's parameters
      params := instType.GetMethod(AName).GetParameters;
      // make new TValue with proper type
      TValue.Make(p, params[0].ParamType.Handle, Arg);
      result := instType.GetMethod(AName).Invoke(obj, [Arg]);
    except
      on E: Exception do
        writeln(E.ClassName + ' error raised, with message : ' + E.Message);
    end;
  finally
    context.Free;
  end;
end;

function executeInstanceMethodReturnIntArgsStruct(Reference: NativeUInt;
  const AName: PAnsiChar; Reference2: Pointer): Integer;
begin
  result := executeInstanceMethod(Reference, string(AName), [Reference2])
    .AsInteger;
end;

begin

  try
    ex := TExample.Create;
    dateRec.Year := 2022;
    dateRec.Dummy := 1;
    writeln(
      executeInstanceMethodReturnIntArgsStruct(NativeUInt(ex), 'Calc', @dateRec)
    );
  except
    on E: Exception do
      writeln(E.ClassName, ': ', E.Message);
  end;

  Readln;

end.

Works on my XE2, prints "2023"

  • Like 1

Share this post


Link to post

You can also press "Like" on the post to increase my rep 🙂

Addition: as you develop generic wrapper, adding record's size check would save you from mysterious bugs in the future.

  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×