4

I've hit an issue when trying to delete layers using Graphics32. It seems that unless you delete layers in reverse order (from the last added to the first) an exception is thrown. I created the simplest application to test this and it is repeatable every time.

I created a simple form with a TImgView32 component (properties all at default) then a button which does the following:

procedure TMainForm.btnDeleteTestClick(Sender: TObject);
var
  Layer1: TCustomLayer;
  Layer2: TCustomLayer;
begin
  Layer1 := TCustomLayer.Create(ImageView.Layers);
  Layer2 := TCustomLayer.Create(ImageView.Layers);

  Layer1.Free;
  Layer2.Free;
end;

If I reverse the order (Layer2.Free then Layer1.Free) it works fine, but this way round it crashes every time. It's also the same whether I use TCustomLayer, TPositionedLayer, TBitmapLayer, or whatever.

I've traved the error and the fault seems to originate here:

function TPointerMap.Delete(BucketIndex, ItemIndex: Integer): PData;
begin 
  with FBuckets[BucketIndex] do begin 
    Result := Items[ItemIndex].Data; 
    if FCount = 0 then Exit; 
    Dec(Count); 
    if Count = 0 then SetLength(Items, 0) 
    else if (ItemIndex < Count) then 
      Move(Items[ItemIndex + 1], Items[ItemIndex], (Count - ItemIndex - 1) * SizeOf(TPointerBucketItem)); 
  end; 
  Dec(FCount); 
end;

Any idea what is causing this or if I'm doing something wrong? I'm running Delphi XE, by the way.

Johan
  • 74,508
  • 24
  • 191
  • 319
C. Horton
  • 41
  • 1
  • 1
    Thanks for coming up with a simple example to test with! Testing in XE7 your code works without problems. Which version of Graphics32 are you using? Mine is "Graphics32Version = '2.0.0 alpha';" – Tom Brunberg May 13 '17 at 13:52
  • Hi Tom, thanks for looking at this. I'm using the latest version downloaded from here: https://github.com/graphics32/graphics32 Which is indeed 2.0.0 alpha – C. Horton May 13 '17 at 22:05
  • 1
    Then I can't really help you, sorry. You will need to do some debugging. Trace into the `Layer1.Free` routine, and look at which file/line the exception is raised. Btw. what are the details of the exception? – Tom Brunberg May 14 '17 at 08:29

1 Answers1

2

Here's the code for TCustomLayer.Destroy

destructor TCustomLayer.Destroy;
var
  I: Integer;
begin
  if Assigned(FFreeNotifies) then
  begin
    for I := FFreeNotifies.Count - 1 downto 0 do
    begin
      TCustomLayer(FFreeNotifies[I]).Notification(Self);
      if FFreeNotifies = nil then Break;
    end;
    FFreeNotifies.Free;
    FFreeNotifies := nil;
  end;
  SetLayerCollection(nil);  <<-- bug, see below.
  inherited;  <<---- See note below.
end;

Notice that there's a bug in SetLayerCollection.

Buggy code

procedure TCustomLayer.SetLayerCollection(Value: TLayerCollection);
begin
  if FLayerCollection <> Value then begin
    if Assigned(FLayerCollection) then begin
      if FLayerCollection.MouseListener = Self then
        FLayerCollection.MouseListener := nil;
      FLayerCollection.RemoveItem(Self);
    end;
    if Assigned(Value) then Value.InsertItem(Self);
  end;
  /// FLayerCollection is never set!
end;

The line SetLayerCollection(nil); does not actually set the LayerCollection! The internal FLayerCollection can suffer from a use after free condition, which is possibly what's happening to you.

Change the code for SetLayerCollection like so:

Bug fix

procedure TCustomLayer.SetLayerCollection(Value: TLayerCollection);
begin
  if FLayerCollection <> Value then begin
    if Assigned(FLayerCollection) then begin
      if FLayerCollection.MouseListener = Self then begin
        FLayerCollection.MouseListener := nil;
      end;
      FLayerCollection.RemoveItem(Self);
    end;
    if Assigned(Value) then begin
      Value.InsertItem(Self)
    end;
    FLayerCollection:= Value;  // add this line.
  end;
end;

Note
My hypothesis is that the following snippet causes the error:

  SetLayerCollection(nil);
  inherited;

SetLayerCollection(value); leaves FLayerCollection unchanged.
The inherited destructor somehow calls something having to do with LayerCollection.

Let me know if this fixes the error.

I've filed a new issue: https://github.com/graphics32/graphics32/issues/13

Update: issue is off by one error in TPointerMap.Delete
The actual issue is here: https://github.com/graphics32/graphics32/issues/14

The code for TPointerMap.Delete is incorrect:

function TPointerMap.Delete(BucketIndex, ItemIndex: Integer): PData;
begin
  with FBuckets[BucketIndex] do
  begin
    Result := Items[ItemIndex].Data;

    if FCount = 0 then Exit;   <<-- error: how can result be valid if count = 0?

    Dec(Count);
    if Count = 0 then
      SetLength(Items, 0)
    else
    if (ItemIndex < Count) then
       //Oops off by 1 error!  ---------------------------------------VVVVV
      Move(Items[ItemIndex + 1], Items[ItemIndex], (Count - ItemIndex - 1) * SizeOf(TPointerBucketItem));
  end;
  Dec(FCount); <<-- The use of with makes this statement confusing.
end;

The code should be changed as follows:

function TPointerMap.Delete(BucketIndex, ItemIndex: Integer): PData;
var
  Bucket: TPointerBucket ;
begin
    if FCount = 0 then Exit(nil);
    //Perhaps add some code to validate BucketIndex & ItemIndex?
    Assert(BucketIndex < Length(FBuckets));
    Bucket:= FBuckets[BucketIndex];
    if ItemIndex >= Bucket.
    Assert(ItemIndex < Length(Bucket.Items));
    Result := Bucket.Items[ItemIndex].Data;
    Dec(Bucket.Count);
    if Bucket.Count = 0 then
      SetLength(Bucket.Items, 0)
    else
     /// assume array like so: 0 1 2 3 4  , itemindex = 0
    /// result should be 1 2 3 4
    /// move(1,0,4) (because 4 items should be moved.
    /// Thus move (itemindex+1, intemindex, count-itemindex)
    if (ItemIndex < Bucket.Count) then
      Move(Items[ItemIndex + 1], Items[ItemIndex], (Bucket.Count - ItemIndex) * SizeOf(TPointerBucketItem));
  end;
  Dec(FCount);
end;
Johan
  • 74,508
  • 24
  • 191
  • 319
  • Hi - many thanks for looking into that. Unfortunately (although that does appear to be a bug!) it didn't solve my problem. I have traced into the code and it seems to be failing deeper - it's an SItemNotFound exception thrown in TPointerMap.Exists (TCustomLayer.Destroy -> TCustomLayer.SetLayerCollection -> TLayerCollection.RemoveItem -> TLayerCollection.Notify -> TMicroTilesRepaintOptimizer.LayerCollectionNotifyHandler -> TMicroTilesMap.GetData -> TPointerMap.GetData -> TPointMap.Exists) – C. Horton May 14 '17 at 20:59
  • This function is: function TPointerMap.Delete(BucketIndex, ItemIndex: Integer): PData; begin with FBuckets[BucketIndex] do begin Result := Items[ItemIndex].Data; if FCount = 0 then Exit; Dec(Count); if Count = 0 then SetLength(Items, 0) else if (ItemIndex < Count) then Move(Items[ItemIndex + 1], Items[ItemIndex], (Count - ItemIndex - 1) * SizeOf(TPointerBucketItem)); end; Dec(FCount); end; If I change (Count - ItemIndex - 1) in the Move call to (Count - ItemIndex) it seems to work but I'm not sure that's actually correct! – C. Horton May 14 '17 at 21:01
  • But it seemed the lookup was failing because the FBuckets[Index] wasn't being updated correctly. – C. Horton May 14 '17 at 21:02
  • @C.Horton See https://github.com/graphics32/graphics32/issues/14, You where correct. – Johan May 15 '17 at 09:40