Search code examples
delphirttidelphi-10.2-tokyo

Rtti: getting "Invalid class typecast" when calling a method in a class


I made a small working example of what I'm trying to do, everything works fine but not the part where I want to call a method with Rtti, check the comments in the method DoSomeTask. I guess looking straight to the line that fails is better than an explanation, but I will try. Briefly, I have some nested classes, the main static class TSCSettings with a public property (Users) which is an instance of the class TUsers. TUsers has a field fAccounting which is an instance of the class TSettingsAccounting. The idea is to load users settings in this way:

TSCSettings.Users.Accounting.LoadSettings(UserID);

and it's working fine like that, but in the method DoSomeTask it fails to call the method LoadSettings of the instance fAccounting with Rtti in the line

meth.Invoke(vField, [1]); --> exception class EInvalidCast with message 'Invalid class typecast'.

Thanks a lot for your help.

program Rtti_CallMethods;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  RTTI;

type

  TSettingsAccounting = Class(TObject)
      public
         function LoadSettings(UserID: Integer): Boolean;
  end;

  TUsers = Class(TObject)
      strict private
         fAccounting: TSettingsAccounting;
      public
         constructor Create; virtual;
         property Accounting: TSettingsAccounting read fAccounting write fAccounting;

         function DoSomeTask: Boolean;
  end;

  //Static class
  TSCSettings = Class(TObject)
      strict private
         class var fUsers: TUsers;
      public
         class property Users: TUsers read fUsers write fUsers;
         class constructor Create;

  end;


{ TSettingsAccounting }
function TSettingsAccounting.LoadSettings(UserID: Integer): Boolean;
begin
   Writeln('...Task load settings ' + UserID.ToString);
end;


{ TCLUsers }
constructor TUsers.Create;
begin
   fAccounting:= TSettingsAccounting.Create;
end;

function TUsers.DoSomeTask: Boolean;
var
   vCtx: TRttiContext;
   vType: TRttiType;
   vField: TRttiField;
   meth : TRttiMethod;
   sInfo: string;
begin

   //---- fAccounting is already instanciated and calling the method in 
   //this way works fine, but fails when I try to do the same below with Rtti
   fAccounting.LoadSettings(4); 
   //----

   vType := vCtx.GetType(self.ClassType); //--> TCLUsers
   for vField in vType.GetFields do
   begin
      sInfo:= vField.Name; // --> fAccounting

      //------ This way works fine
      meth := vField.FieldType.GetMethod('LoadSettings');
      meth.Invoke(fAccounting, [2]);
      //------

      //------ This way fails: ... exception class EInvalidCast with message 'Invalid class typecast'.
      meth.Invoke(vField, [1]);
      //------
   end;
end;

class constructor TSCSettings.Create;
begin
   fUsers:=  TUsers.Create;
end;


// -------------------------------
begin
   TSCSettings.Users.Accounting.LoadSettings(3); //This works fine
   TSCSettings.Users.DoSomeTask;
   readln;
end.

Solution

  • The 1st parameter of TRttiMethod.Invoke() is a pointer to the object to call the method on. That pointer can be passed in as either a direct TObject pointer, or as a TValue containing the TObject pointer.

    In this case, a TSettingsAccounting object is needed. Which is why meth.Invoke(fAccounting, [2]); works. But on the failing line, you are giving it a pointer to the TRttiField itself, which is why meth.Invoke(vField, [1]); fails. TRttiMethod derives from TObject, so the code compiles, but it is the wrong TObject to pass in.

    You need the TObject from reading the value of the TRttiField (the value of the fAccounting field), eg:

    meth.Invoke(vField.GetValue(Self), [1]);