Jump to content
John Kouraklis

How to pass an unknown record to a function as argument

Recommended Posts

Hi,

I want to write a function and allow the user of the function to pass a record that I do not know in advance. Then, the idea is I iterate through the fields in the function.

 

Something like this:

function pass (aRec: record): boolean;

 

But a 'record' can't be used like this.

 

I've thought of two approaches but without being able to make them work:

 

1. Use of pointers:

 

function pass (aRP: Pointer): boolean;

begin

....

/// here I do not know how to (cast the pointer to any record and) iterate using RTTI

/// I think this can not be done as the cast seems very arbitrary

...

end;

 

2. A kind of adapter record:

type

  TAdapterRec<T> = record

    instance: T

   constructor Create (aRec: T);

 end;

 

and then tried to declare the function:

function pass (aRec: TAdapterRec<T>): boolean;

but generics like these are not allowed.

 

Anyone can help with this? I would like to avoid using classes.

 

Thanks

 

 

Share this post


Link to post

Try overloaded functions

function pass(aRec: TMyRecord1): Boolean; overload;

function pass(aRec: TMyRecord2): Boolean; overload;

...

Share this post


Link to post

Make the function a static class function of a record:

type
  TMyPassHandler = record
  public
    class function Pass<T:record>(const Value: T): Boolean; static;
  end;

class function TMyPassHandler.Pass<T>(const Value: T): Boolean;
begin
  Result := False;
  { do whatever is needed }
end;

Most of the time the compiler should be able to infer the proper type, so a call would look like:

TMyPassHandler.Pass(myRec);

 

  • Like 2

Share this post


Link to post
2 hours ago, dummzeuch said:

function pass(var aRec): boolean;

or

function pass(const aRec): boolean;

 

How will you use RTTI with this?

Share this post


Link to post
2 hours ago, Uwe Raabe said:

Make the function a static class function of a record:


type
  TMyPassHandler = record
  public
    class function Pass<T:record>(const Value: T): Boolean; static;
  end;

class function TMyPassHandler.Pass<T>(const Value: T): Boolean;
begin
  Result := False;
  { do whatever is needed }
end;

Most of the time the compiler should be able to infer the proper type, so a call would look like:


TMyPassHandler.Pass(myRec);

 

If the goal is to use RTTI then it seems questionable to use compile time generics. Will result in generic bloat. May as well pass the address of the record and the type info.

 

Or if you want to ensure type safety have the generic method call a further helper method that is non-generic and receives the address of the record and the type info.  That avoids the bloat and gives benefit of type safety.

Share this post


Link to post

The goal is to extract the fields and their values from the record. 

 

The class function approach looks more promising but it looks such "openness" makes things complicated. 

 

I think I will pass the list of the fields directly rather than the record itself...:classic_blush::classic_blush::classic_blush:

 

Thanks everyone

Edited by John Kouraklis

Share this post


Link to post
4 hours ago, dummzeuch said:

function pass(var aRec): boolean;

or

function pass(const aRec): boolean;

 

 

1 hour ago, David Heffernan said:

How will you use RTTI with this?

I would be great if this were possible...

Share this post


Link to post
1 hour ago, John Kouraklis said:

 

I would be great if this were possible...

What about that :

type
  TRecord1 = record
  private
  var
    FTypeInfo: Pointer; // this must be the first field and all record must implement it.
  public
  var
    FField1: Integer; // Record fields.
    class function Create: TRecord1; static;
  end;

  TRecord2 = record
  private
  var
    FTypeInfo: Pointer;
  public
  var
    FField1: Integer;
    FField2: string;
    class function Create: TRecord2; static;
  end;

procedure DoSomething(const Rec);
type
  TRecordHack = record
    FTypeInfo: Pointer; // since FTypeInfo is the first field in all records, we can hack it !
  end;

  PRecordHack = ^TRecordHack;
var
  PHack: PRecordHack;
  TypeInfo: Pointer;
  LCntx: TRttiContext;
  LType: TRttiType;
begin
  PHack := PRecordHack(@Rec);
  TypeInfo := PHack^.FTypeInfo;
  // RTTI:
  LCntx := TRttiContext.Create();
  try
    LType := LCntx.GetType(TypeInfo);
    ShowMessage(LType.Name);
  finally
    LCntx.Free();
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  a: TRecord1;
  b: TRecord2;
begin
  a := TRecord1.Create();
  DoSomething(a);
  b := TRecord2.Create();
  DoSomething(b);
end;

{ TRecord1 }

class function TRecord1.Create: TRecord1;
begin
  Result.FTypeInfo := TypeInfo(TRecord1);
end;

{ TRecord2 }

class function TRecord2.Create: TRecord2;
begin
  Result.FTypeInfo := TypeInfo(TRecord2);
end;

 

  • Like 2

Share this post


Link to post

@Mahdi Safsafi Very cool. Thanks for this

 

A couple of questions: 

1. Do you need to create/Free TRTTIContext? All the books/articles I've seen they just use the record

 

2. The TypeInfo variable in DoSomething conflicts with TypeInfo from RTTI. It should use a different name, right?

 

3. How do I get the value of the fields now? I tried this:

for lFields in LType.GetFields do
	Writeln(' '+lFields.Name + ': '+lFields.GetValue(TypeInfo(Rec)).AsString);

but the instance in GetValue is not correct. 

 

Thanks again

Share this post


Link to post
6 hours ago, David Heffernan said:

How will you use RTTI with this?

I missed the part about RTTI in the question.

Share this post


Link to post
Quote

1. Do you need to create/Free TRTTIContext? All the books/articles I've seen they just use the record

I saw many people using TRTTIContext without freeing it. That's wrong ! The Free function wasn't there for nothing. I just check it and found that it assigns a nil to interface !

procedure TRttiContext.Free;
begin
  FContextToken := nil;
end;

You should always use Free when a record implements it even if it does nothing !!! It might do nothing currently ... but maybe the developer introduce it to use it later or it does something on other configuration (ARM ?)!

As an example, from the Win32 api FlushInstructionCache function does nothing on x86 but it's important on ARM ! 

Quote

2. The TypeInfo variable in DoSomething conflicts with TypeInfo from RTTI. It should use a different name, right?

My bad ... Sorry.

Quote

3. How do I get the value of the fields now? I tried this:

You're missing "@" !

Writeln(' '+lFields.Name + ': '+lFields.GetValue(TypeInfo(@Rec)).AsString);

I just played a little with the above code :


uses
  System.SysUtils,
  System.Rtti,
  System.TypInfo,
  System.Generics.Collections;

type
  TRecord1 = record
  private
  var
    FTypeInfo: Pointer; // this must be the first field and all record must implement it.
  public
  var
    FField1: Integer; // Record fields.
    class function Create: TRecord1; static;
  end;

  TRecord2 = record
  private
  var
    FTypeInfo: Pointer;
  public
  var
    FField1: Integer;
    FField2: string;
    FField3: Boolean;
    class function Create: TRecord2; static;
  end;

procedure DoSomething(const Rec);
type
  TRecordHack = record
    FTypeInfo: Pointer; // since FTypeInfo is the first field in all records, we can hack it !
  end;

  PRecordHack = ^TRecordHack;
var
  PHack: PRecordHack;
  LTypeInfo: Pointer;
  LCntx: TRttiContext;
  LType: TRttiType;
  LField: TRttiField;
  LValue: TValue;
  LKind: TTypeKind;
  SValue: string;
begin
  PHack := PRecordHack(@Rec);
  LTypeInfo := PHack^.FTypeInfo;

  // RTTI:
  LCntx := TRttiContext.Create();
  try
    LType := LCntx.GetType(LTypeInfo);
    Writeln(Format('record (%s):', [LType.Name]));
    for LField in LType.GetFields do
    begin
      if (LField.Visibility = mvPrivate) then   // skip private (FTypeInfo).
        Continue;
      LValue := LField.GetValue(@Rec);
      LKind := LValue.Kind;
      case LKind of
        tkInteger:
          SValue := IntToStr(LValue.AsInteger);
        tkString, tkUString:
          SValue := LValue.AsString;
        tkEnumeration:
          SValue := GetEnumName(LValue.TypeInfo, TValueData(LValue).FAsSLong);
      else
        SValue := '??';
      end;
      Writeln(Format('  field (%s) = %s', [LField.Name, SValue]));
    end;
  finally
    LCntx.Free();
  end;
end;

{ TRecord1 }

class
  function TRecord1.Create: TRecord1;
begin
  Result.FTypeInfo := TypeInfo(TRecord1);
end;

{ TRecord2 }

class
  function TRecord2.Create: TRecord2;
begin
  Result.FTypeInfo := TypeInfo(TRecord2);
end;

var
  a: TRecord1;
  b: TRecord2;

begin
  a := TRecord1.Create();
  a.FField1 := 1;
  DoSomething(a);
  b := TRecord2.Create();
  b.FField1 := -10;
  b.FField2 := 'foo';
  DoSomething(b);
  Readln;
end.

 

  • Like 1

Share this post


Link to post
6 minutes ago, Mahdi Safsafi said:

I saw many people using TRTTIContext without freeing it. That's wrong !

Like me. It's "created" once at startup to generate the rtti cache and stored in a global variable and never "created" again or "free"'d. Single threaded app.

 

Share this post


Link to post
22 minutes ago, Attila Kovacs said:

Like me. It's "created" once at startup to generate the rtti cache and stored in a global variable and never "created" again or "free"'d. Single threaded app.

 

You read my explanation. From now, you' ve no excuses to use it without freeing it 😉 I'm watching you. 

Share this post


Link to post
48 minutes ago, Mahdi Safsafi said:

You read my explanation. From now, you' ve no excuses to use it without freeing it 😉 I'm watching you. 

You are quite wrong. No point freeing it. No point even creating it. 

  • Like 1

Share this post


Link to post
32 minutes ago, David Heffernan said:

You are quite wrong. No point freeing it. No point even creating it. 

I believe that you didn't understand me clearly. My answer wasn't about whether is pointless or not ! my answer was about the coding style (and I gave an example about the Win32 api). 

Suppose I made a library that have a record like this :


type
  TDummy = record
    // ...
  public
    function Create(): TDummy;
    procedure Free();
  end;

  { TDummy }

function TDummy.Create: TDummy;
begin
  // FOR NOW ... do nothing here   !
end;

procedure TDummy.Free;
begin
  // on this platform do nothing for now !
  // feature platform do something.
end;

Currently Create and Free do nothing on Win32(x86). Yep there is no need to use them. But I use them just for future compatibility.

Now after 1 year, I decided to port my library to let say WinRT or MIPS. Now my Free must do something here (really)!

Quote

procedure TDummy.Free;
begin
 {$IF DEFINED(MIPS) || DEFINED(WINRT)}

   DoSeriousJob(); 

{$ENDIF}
end;

Now, from your approach, you must track all variables and free them if you want your code to run successfully on WinRT, MIPS.

For me, because I used them on all configurations, I wan't going to track them !

 

Also, is there any guaranties that "FContextToken" won't change to something else on future ? 

Share this post


Link to post

TRttiContext.Free does already something. It kills the RTTI cache which I'm trying to avoid. But go ahead and create/free it every single time if you wish.

Share this post


Link to post
54 minutes ago, Mahdi Safsafi said:

I believe that you didn't understand me clearly. My answer wasn't about whether is pointless or not ! my answer was about the coding style (and I gave an example about the Win32 api). 

Suppose I made a library that have a record like this :



type
  TDummy = record
    // ...
  public
    function Create(): TDummy;
    procedure Free();
  end;

  { TDummy }

function TDummy.Create: TDummy;
begin
  // FOR NOW ... do nothing here   !
end;

procedure TDummy.Free;
begin
  // on this platform do nothing for now !
  // feature platform do something.
end;

Currently Create and Free do nothing on Win32(x86). Yep there is no need to use them. But I use them just for future compatibility.

Now after 1 year, I decided to port my library to let say WinRT or MIPS. Now my Free must do something here (really)!

Now, from your approach, you must track all variables and free them if you want your code to run successfully on WinRT, MIPS.

For me, because I used them on all configurations, I wan't going to track them !

 

Also, is there any guaranties that "FContextToken" won't change to something else on future ? 

No point planning for a future you can't see. The real problem was the stupid design in adding Create and Free methods that serve no purpose and confusing so many people. 

 

Best strategy is to use a singleton for the context. 

Share this post


Link to post

@Mahdi Safsafi Thanks! the hack helps me a lot.

 

About the debate regarding Create/Free, it looks to me it is good practice. 

 

Similar to this, I've seen people always but always setting interfaces to nil as a matter of coding style

Share this post


Link to post

Hi,

 

I wrote this code using XE. It might help you.

  type
  TMyRecord = Record
    item1: string;
    item2: Integer;
    Item3: Currency
  End;
  
  TForm68 = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    function GetProperties( aTypeInfo : PTypeInfo; var aRec ) : String;
  end;
  
  implementation

{$R *.dfm}

procedure TForm68.FormCreate(Sender: TObject);
var
  R : TMyRecord;
begin
   Caption := GetProperties( TypeInfo(TMyRecord), R );
end;

function TForm68.GetProperties(aTypeInfo: PTypeInfo; var aRec): String;
var
  rtc : TRttiContext;
  lTyp: TRttiType;
  lFld : TRttiField;
begin
  rtc := TRttiContext.Create;
  lTyp := rtc.GetType(aTypeInfo);
  for lFld in lTyp.GetFields do
    Result := Result + ' '+ lFld.Name;
end;

As you can see, all you need is Typeinfo.  You can pass aRec as pointer if you need an instance to assign values.
The result is obviously: Item1 item2 item3

 

Hope this helps,

 

Share this post


Link to post
On 2/11/2019 at 12:18 PM, David Heffernan said:

If the goal is to use RTTI then it seems questionable to use compile time generics. Will result in generic bloat.

Not much, as this is only one function. And it is more elegant than passing a pointer and type info.

 

I would then make this function call a private function with the pointer obtained from the parameter and the typeinfo obtained from T.

 

That way, there will hardly be any bloat. And if this function is not critical, you will never notice a difference.

Edited by Rudy Velthuis

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

×