Here is a great example why you should NEVER use with
.
We were contacted by a client who claimed that our product (EurekaLog) was erasing his image in a program. In particular, it was claimed that the client's code "worked great" until EurekaLog was added to the application. After adding EurekaLog, the previously "working" code stopped working, clearing the image instead (e.g. the code's result was blank instead of expected image).
Simplified code looks really simple:
Buffer := TBitmap.Create;
try
Buffer.SetSize(64, 64);
Buffer.Canvas.StretchDraw(Rect(0, 0, 64, 64), Bitmap);
Bitmap.SetSize(64, 64);
Bitmap.Canvas.Draw(0, 0, Buffer);
finally
Buffer.Free;
end;
Do you see a problem in that code?
We have found that the problem is that code accessed the already deleted TBitmapImage
:
procedure TBitmap.Draw(ACanvas: TCanvas; const Rect: TRect);
// ...
begin
with Rect, FImage do
begin
// ...
StretchBlt(ACanvas.FHandle, Left, Top, Right - Left, Bottom - Top,
Canvas.FHandle, 0, 0, FDIB.dsbm.bmWidth,
FDIB.dsbm.bmHeight, ACanvas.CopyMode);
// ...
end;
end;
Do you see a bug in the code now?
The bug is using the with
operator.
Indeed, using the with
operator causes the Delphi optimizer to store a reference to the evaluated expression in with
somewhere. And since we are talking about an object (which are stored by reference, and not by value), it is not the object's data that is stored, but only a reference to the data. Therefore, any data change (including their deletion/clearing) from the sub-procedures called in the code will go unnoticed. In particular:
// FImage is being cached by with
with Rect, FImage do
// ...
// FImage was deleted (recreated), but reference to the deleted object is being stored by with
Canvas.RequiredState(csAllValid);
// ...
// FImage.FDIB references the deleted object
StretchBlt(ACanvas.FHandle, Left, Top, Right - Left, Bottom - Top,
Canvas.FHandle, 0, 0, FDIB.dsbm.bmWidth,
FDIB.dsbm.bmHeight, ACanvas.CopyMode);
// ...
And the worst thing about this with
operator is that the debugger is not able to interpret it correctly. For example, the calculation of expressions by mouse hovering does not work. And if we try to calculate the values of the specified expressions (for example, FImage.FDIB.dsbm.bmWidth
), then the debugger will, of course, return the correct value from the new FImage
object, and not from the removed old one, which is preserved by the with
statement.
Values of the StretchBlt
's function argument were one of the things that I have checked first! But due to this "feature" of the debugger, I could not see the problem, although I had it right before my eyes. I had to go the long way.
Conclusion: our customer has found a bug in VCL. Congratulations!