20

Given a Record:

MyRecord = record
    Company: string;
    Address: string;
    NumberOfEmplyees: integer;

can you write a function call like

function UpdateField(var FieldName: string; FieldValue: variant): bool;

so that:

UpdateField('Company', 'ABC Co');

would update MyRecord.Company to 'ABC Co'?

I looked for an example but everything I found is for a database. Any help pointing me in the right direction is appreciated.

Thanks, Charles

Fabricio Araujo
  • 3,868
  • 1
  • 25
  • 40
Charles
  • 315
  • 2
  • 9
  • 4
    +1 This is a very good and clearly asked question. – David Heffernan Jun 22 '11 at 21:02
  • But if there's a need for updating fields like that, then maybe the data should be stored in a way that allows for such updates. I mean, you could work then with fields stored as strings in the format similar to `Name=Value`, and for that use TStringList or TIniFile. What am I missing? – Andriy M Jun 23 '11 at 07:34
  • @Charles Will you accept an answer? Or have you any more trouble? – Arnaud Bouchez Jul 04 '11 at 14:27

2 Answers2

12

What Delphi 7 RTTI knows, and can be retrieved from TypeInfo(aRecordType), is:

  • The record type name;
  • The record global size;
  • The offset and type of each reference-counted variable within the record (string/variant/widestring/dynamic array/other nested record containing reference-counted variables).

The latest information is necessary to free the memory used by each reference-counted variables inside the record, or copy the record content, at run-time. The initialization of the record is also performed either in compiler-generated code (if the record is created on the stack), either via _InitializeRecord() method, either with a global fill to 0 when a class or a dynamic array is instanciated.

It's the same for both record and object types, in all version of Delphi.

You can note that there is a bug in modern version of Delphi (including Delphi 2009 and 2010 at least), which sometimes don't create the code for initializing objects on stack. You'll have to use record instead, but it will break compatibility with previous version of Delphi. :(

Here are the structure used for storing this RTTI data:

type
  TFieldInfo = packed record
    TypeInfo: ^PDynArrayTypeInfo; // information of the reference-counted type
    Offset: Cardinal; // offset of the reference-counted type in the record
  end;
  TFieldTable = packed record
    Kind: byte;
    Name: string[0]; // you should use Name[0] to retrieve offset of Size field
    Size: cardinal;  // global size of the record = sizeof(aRecord)
    Count: integer;  // number of reference-counted field info
    Fields: array[0..0] of TFieldInfo; // array of reference-counted field info
  end;
  PFieldTable = ^TFieldTable;

Using this data, here is for instance what you can do:

For instance, here is how two records of the same type can be compared, using this RTTI:

/// check equality of two records by content
// - will handle packed records, with binaries (byte, word, integer...) and
// string types properties
// - will use binary-level comparison: it could fail to match two floating-point
// values because of rounding issues (Currency won't have this problem)
function RecordEquals(const RecA, RecB; TypeInfo: pointer): boolean;
var FieldTable: PFieldTable absolute TypeInfo;
    F: integer;
    Field: ^TFieldInfo;
    Diff: cardinal;
    A, B: PAnsiChar;
begin
  A := @RecA;
  B := @RecB;
  if A=B then begin // both nil or same pointer
    result := true;
    exit;
  end;
  result := false;
  if FieldTable^.Kind<>tkRecord then
    exit; // raise Exception.CreateFmt('%s is not a record',[Typ^.Name]);
  inc(PtrUInt(FieldTable),ord(FieldTable^.Name[0]));
  Field := @FieldTable^.Fields[0];
  Diff := 0;
  for F := 1 to FieldTable^.Count do begin
    Diff := Field^.Offset-Diff;
    if Diff<>0 then begin
      if not CompareMem(A,B,Diff) then
        exit; // binary block not equal
      inc(A,Diff);
      inc(B,Diff);
    end;
    case Field^.TypeInfo^^.Kind of
      tkLString:
        if PAnsiString(A)^<>PAnsiString(B)^ then
          exit;
      tkWString:
        if PWideString(A)^<>PWideString(B)^ then
          exit;
      {$ifdef UNICODE}
      tkUString:
        if PUnicodeString(A)^<>PUnicodeString(B)^ then
          exit;
      {$endif}
      else exit; // kind of field not handled
    end;
    Diff := sizeof(PtrUInt); // size of tkLString+tkWString+tkUString in record
    inc(A,Diff);
    inc(B,Diff);
    inc(Diff,Field^.Offset);
    inc(Field);
  end;
  if CompareMem(A,B,FieldTable.Size-Diff) then
    result := true;
end;

So for your purpose, what Delphi 7 RTTI could let you know at runtime, is the position of every string within a record. Using the code above, you could easily create a function using the field index:

     procedure UpdateStringField(StringFieldIndex: integer; const FieldValue: string);

But you simply don't have the needed information to implement your request:

  • The field names are not stored within the RTTI (only the global record type name, and even not always AFAIK);
  • Only reference-counted fields have an offset, not other fields of simple type (like integer/double...).

If you really need this feature, the only solution under Delphi 7 is to use not records, but classes.

On Delphi 7, if you create a class with published fields, you'll have all needed information for all published fields. Then you can update such published field content. This is what the VCL runtime does when unserializing the .dfm content into class instances, or with an ORM approach.

Arnaud Bouchez
  • 40,947
  • 3
  • 66
  • 152
7

You need modern versions of Delphi to do what you ask for without resorting to manually coding the lookups, e.g. via a table.

The updated RTTI introduced in Delphi 2010 can support what you are looking for, but there's nothing in Delphi 7 that will do this for records.

Mason Wheeler
  • 77,748
  • 42
  • 247
  • 453
David Heffernan
  • 572,264
  • 40
  • 974
  • 1,389