Search code examples
delphidynamic-arraysrttidelphi-10.2-tokyo

DynArraySize() works correctly only up to arrays of 649 integer elements


I experienced a RTTI-related problem in Delphi 10.2 Update 2 and was able to track it down to a fewer amount of code (see below).

I have some TPersistent-descendant class TMyObj that publishes a property of type TArray<Integer>. When I recieve its value via GetDynArrayProp() and query its size via DynArraySize() this works only up to a size of exactly 649 elements. Above this special count some very big size value is returned.

Note, that my array is generated from an instance of TDictionary<Integer,Boolean>'s Keys property with its very own ToArray method. I also tried to modify TMyObj.GetDynArray so that it returns a instance of TArray<Integer> directly and it worked correctly. Thus, I think that could correlate in some mystical way.

What is wrong with my use of DynArraySize()? What's behind this mystical behaviour of dynamic arrays?

program RTTIPropDynArray;

{$APPTYPE CONSOLE}

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

type
  TMyDict  = TDictionary<Integer,Boolean>;
  TMyArray = TArray<Integer>;

  TMyObj = class(TPersistent)
  private
    FValues: TMyDict;
    function GetDynArray: TMyArray;
  public
    constructor Create(const ACount: Integer);
    destructor Destroy; override;
  published
    property DynArray: TMyArray read GetDynArray;
  end;

{ TMyObj }

constructor TMyObj.Create(const ACount: Integer);
begin
  FValues := TMyDict.Create;
  while FValues.Count < ACount do
    FValues.AddOrSetValue(Random(MaxInt), False);
end;

destructor TMyObj.Destroy;
begin
  FreeAndNil(FValues);
  inherited;
end;

function TMyObj.GetDynArray: TMyArray;
begin
  Result := FValues.Keys.ToArray;
end;

function Test(const ACount: Integer): Boolean;
var
  LInstance: TMyObj;
  LExpectedSize: Integer;
  LDynArraySize: Integer;
begin
  LInstance := TMyObj.Create(ACount);
  try
    LExpectedSize := Length(LInstance.DynArray);
    LDynArraySize := DynArraySize(GetDynArrayProp(LInstance, 'DynArray'));
    Result := LExpectedSize = LDynArraySize;
    if not Result then
      WriteLn(Format('Expected size: %d; DynArraySize: %d', [LExpectedSize, LDynArraySize]));
  finally
    LInstance.Free;
  end;
end;

var
  LCount: Integer;
begin
  Randomize;
  LCount := 1;
  while Test(LCount) do
    Inc(LCount);
  ReadLn;
end.

Solution

  • Short answer: Your code is broken

    Long answer:

    The call to the getter is creating a new array (see TEnumerable<T>.ToArrayImpl in System.Generics.Collections.pas) which is being deallocated in the epilogue of System.TypInfo.GetDynArrayProp (put a breakpoint there and look into the disassembler - it shows @DynArrayClear). Since there is no other reference to this array its memory gets deallocated (if you step into System.pas further you will see that it eventually ends up in _FreeMem). That means every call to this function is returning a dangling pointer!

    Now why do you get correct results in all prior calls? Coincidence - the memory has not been reallocated by anything else.

    Two possible solutions come into mind that don't involve rewriting the getter:

    • use the RTTI from System.Rtti.pas as TValue keeps the reference alive
    • write your own version of GetDynArrayProp that keeps the reference alive - but you have to make sure to always call DynArrayClear after or you create memory leaks

    Personally I would use the first one.