2

I'm expriencing some weird problems in one of my projects. What is the most strange is that this only happens in this project and I can't recreate it in another.

Luckilly this is a tiny project (intended to provide an answer to one of the questions here on SO - still not finished) so I managed to figure out that it has to do something with destroying of TImage component I have placed on my form at design time and set a BMP image to it also at design time.

The AV I get is: Project Project1.exe raised exception class $C0000005 with message 'acces violation at 0x00407430: read of adress 0xfffffffc'.

Last three Call stacks are:
Vcl.Graphics.TBitmapCanvas.Destroy
Vcl.Graphics.TCanvasDestroy
System.TObject.Free

Also Delphi puts me in System.pas unit in TObbject.Free method on line "Destroy".

procedure TObject.Free;
begin
  if Self <> nil then
{$IFDEF AUTOREFCOUNT}
    __ObjRelease;
{$ELSE}
    Destroy;
{$ENDIF}
end;

Value of Self at this time is shown only as ().

But why this AV only happens if I store some data into my multidimensional array.
I can create and set the size of the multidimensional array at runtime and everyting is fine. But as soon as I change data of some items in this multidimensional array I get AV when closing my application.

Here is my full source code:

unit Unit2;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;

type
  TSubImage = record
    LeftBound: Integer;
    RightBound: Integer;
    TopBound: Integer;
    BottomBound: Integer;
  end;

  TForm2 = class(TForm)
    Button1: TButton;
    ListBox1: TListBox;
    Image1: TImage;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form2: TForm2;
  SubImages: Array of TSubImage;

implementation

{$R *.dfm}

procedure TForm2.Button1Click(Sender: TObject);
var X,Y,I: Integer;
    RegionMask: Array of Array of Integer;
begin
  SetLength(RegionMask,Image1.Width+1,Image1.Height+1);
  for Y := 0 to Image1.Height do
  begin
    for X := 0 to Image1.Width do
    begin
      if Image1.Canvas.Pixels[X,Y] <> clFuchsia then
      begin
        //Check left pixel
        if X > 0 then
        begin
          if RegionMask[X-1,Y] <> 0 then
          begin
            RegionMask[X,Y] := RegionMask[X-1,Y];
            //Check to se if pixel X position is leftwards to subimages left bound
            if Subimages[RegionMask[X,Y]].LeftBound > X then
              //Move subimage left bound to match pixel X position
              Subimages[RegionMask[X,Y]].LeftBound := X;
            //Check to se if pixel X position is rightwards to subimages right bound
            if Subimages[RegionMask[X,Y]].RightBound < X then
              //Move subimage right bound to match pixel X position
              Subimages[RegionMask[X,Y]].RightBound := X;
            //Check to se if pixel Y position is upwards to subimages top bound
            if Subimages[RegionMask[X,Y]].TopBound > Y then
              //Move subimage top bound to match pixel Y position
              Subimages[RegionMask[X,Y]].TopBound := Y;
            //Check to se if pixel Y position is downwards to subimages bottom bound
            if Subimages[RegionMask[X,Y]].BottomBound < Y then
              //Move subimage bottom bound to match pixel Y position
              Subimages[RegionMask[X,Y]].BottomBound := Y;
          end;
        end;
        //Check top pixel
        if Y > 0 then
        begin
          if RegionMask[X,Y-1] <> 0 then
          begin
            RegionMask[X,Y] := RegionMask[X,Y-1];
            //Check to se if pixel X position is leftwards to subimages left bound
            if Subimages[RegionMask[X,Y]].LeftBound > X then
              //Move subimage left bound to match pixel X position
              Subimages[RegionMask[X,Y]].LeftBound := X;
            //Check to se if pixel X position is rightwards to subimages right bound
            if Subimages[RegionMask[X,Y]].RightBound < X then
              //Move subimage right bound to match pixel X position
              Subimages[RegionMask[X,Y]].RightBound := X;
            //Check to se if pixel Y position is upwards to subimages top bound
            if Subimages[RegionMask[X,Y]].TopBound > Y then
              //Move subimage top bound to match pixel Y position
              Subimages[RegionMask[X,Y]].TopBound := Y;
            //Check to se if pixel Y position is downwards to subimages bottom bound
            if Subimages[RegionMask[X,Y]].BottomBound < Y then
              //Move subimage bottom bound to match pixel Y position
              Subimages[RegionMask[X,Y]].BottomBound := Y;
          end;
        end;
        //Create new region
        if RegionMask[X,Y] = 0 then
        begin
          SetLength(SubImages,Length(SubImages)+1);

          //If I comment out this line no exception is raised on closing the from
          RegionMask[X,Y] := Length(SubImages);

          //Set subimage initial bounds which are coordinates of one pixel
          //since we created new region for this pixel
          SubImages[RegionMask[X,Y]].LeftBound := X;
          SubImages[RegionMask[X,Y]].RightBound := X;
          SubImages[RegionMask[X,Y]].TopBound := Y;
          SubImages[RegionMask[X,Y]].BottomBound := Y;
        end;
      end;
    end;
  end;
  Form2.Caption := IntToStr(Length(SubImages)-1);
  for I := 0 to Length(Subimages)-1 do
  begin
    ListBox1.Items.Add(IntToStr(SubImages[I].LeftBound)+','+
                       IntToStr(SubImages[I].RightBound)+','+
                       IntToStr(SubImages[I].TopBound)+','+
                       IntToStr(SubImages[I].BottomBound));
  end;
  SetLength(RegionMask,0,0);
  RegionMask := nil;
end;

end.
SilverWarior
  • 7,372
  • 2
  • 16
  • 22
  • `for Y := 0 to Image1.Height` looks odd. Surely `for Y := 0 to Image1.Height-1`. Likewise X loop. Also, why would you operate with a GUI control? Sure a bitmap is what you need. – David Heffernan Aug 31 '14 at 13:22
  • 1
    Also, you are running with range checking on I presume? – David Heffernan Aug 31 '14 at 13:44
  • Yes that is one error in my code but it doesen't have any affect to the AV I'm getting. Why I'm I using GUI? This is the verry first implementation where I'm not concerned about speed, but only about the results. In the end it is my intention to use TBtimap and ScanLine instead of accesing seperate pixels by Canvas.Pixel[X,Y] method. But now ?im wondering why is this error happenning and why only if I store some data into my Dynamic Multidimensional Array which has no connection with TImage except that I initialy set its size to Image dimensions. – SilverWarior Aug 31 '14 at 13:51
  • No it apears that I didn't have range checking enabled before (I thought it is enabled by default). Enabling range checking has shown many errors in my code. Actually when I have been referencing the Subimages I was always off by one item. So fixing those errors seems to fixed my AV problem but to be honest I don't know how. – SilverWarior Aug 31 '14 at 14:08
  • Always debug with range checking on – David Heffernan Aug 31 '14 at 16:26
  • Do you know a good way to configure Delphi XE3 or newer to always have Range Checkings enabled for every new project? – SilverWarior Aug 31 '14 at 16:39
  • http://stackoverflow.com/questions/10134658/how-to-set-default-compiler-options-for-xe2 – David Heffernan Aug 31 '14 at 16:49
  • @SilverWarior "fixing those errors seems to fixed my AV problem but to be honest I don't know how" - it's because you're overwriting random memory. You don't know what's located there, but it could be anything. In this particular situation, it caused enough damage to cause an AV later, probably when it tried to use the contents of the memory that had been overwritten. – David Aug 31 '14 at 17:39

2 Answers2

5

You've got a one-off error in these lines. Consider the first time you add an element to SubImages. The length of SubImages is 1, but the only element in the array is SubImages[0]. You set RegionMask[X,Y] to 1, and then use that value to index the array. So you are trying to access one item beyond the end of the array.

   SetLength(SubImages,Length(SubImages)+1);

      RegionMask[X,Y] := Length(SubImages);

      SubImages[RegionMask[X,Y]].LeftBound := X;
David Dubois
  • 3,842
  • 3
  • 18
  • 36
  • Thanks for your answer. I just figured that out a fw minutes back when David asked me if I used Range Checkings. I haven't used them. SO after enabling them I quickly realized my mistakes. But still I don't know how this could affect destruction of the TImage component. – SilverWarior Aug 31 '14 at 14:18
  • 3
    Once you start changing memory out of range you can affect any aspect of anything that is stored in memory. Where is your array in memory? What's just beyond it? – David Dubois Aug 31 '14 at 15:34
1

Do you know a good way to configure Delphi XE3 or newer to always have Range Checkings enabled for every new project? – SilverWarior

I have a "default" project. So, I never start a new project, instead I copy that one. That "default" project does a lot of extra stuff for me, including improved support for TApplication.

And you are indeed right, NO application/projects should ever be started without Overflow Checking, Range Checking and Assertions being turned on by default (for Debug version).

Well, there are more settings that should be turned on by default, for proper debugging.


Overflow checking

This will check certain integer arithmetic operations (+, -, *, Abs, Sqr, Succ, Pred, Inc, and Dec) for overflow. For example, after a + (addition) operation the compiler will insert additional binary code that verifies that the result of the operation is within the supported range.

An "integer overflow" occurs when an operation on an integer variable produces a result that is outside the range of that variable. For example, if an integer variable is declared as a 16-bit signed integer, its value can range from -32768 to 32767. If an operation on this variable produces a result greater than 32767 or less than -32768, an integer overflow has occurred.

When an integer overflow occurs, the result of the operation is undefined and can lead to undefined behavior in the program: • Wrap-around The result might result in a wrapped-around value. This means that the number 32768 will be actually stored as 1 since it is 1 unit higher than the highest value we can store (32767). • Truncation The result may be truncated or otherwise modified to fit within the range of the integer type. For example, the number 32768 will be actually stored as 32767 since that is the highest value we can store.

Undefined program behavior is one of the worst kind of errors, because it is not an error easy to reproduce. Therefore, it is difficult to track and repair.

There is a small price to pay if you activate this: the speed of the program will decrease slightly.

IO checking

Checks the result of an I/O operation. If an I/O operation fails, an exception is raised. If this switch is off, we must check for I/O errors manually. There is a minor price to pay if you activate this: the speed of the program will decrease, but insignificantly because the few microseconds introduced by this check is nothing compared with the millisecond-range time required by the I/O operation itself (the hard drives are slow).

Range Checking

The Delphi Geek calls this “The most important Delphi setting” and I totally agree. It checks if all array and string indexing expressions are within the defined bounds. It also checks that all assignments to scalar and subrange variables are within range.

Here is an example of code that would ruin our life if Range Checking would not be available:

Type 
    Pasword= array [1..10] of byte; // we define an array of 10 elements
…
x:= Pasword[20];       // Range Checking will prevent the program from accessing element 20 (ERangecheckError exception is raised). Security breach avoided. Error log automatically sent to the programmer. Bruce Willis saves everyone.

Enabling Runtime Error Checking

To activate the Runtime Error Checking, go to Project Options and check these three boxes:

Enabling the Runtime Error Checking in ‘Project Options’ enter image description here

Assertions

A good programmer MUST use assertions in its code to increase the quality and stability of the program. Seriously man! You really need to use them.

Assertions are used to check for conditions that should always be true at a certain point in the program, and to raise an exception if the condition is not met. The Assert procedure, which is defined in the SysUtils unit, is typically used to perform assertions.

You can think of assertions as poor man’s unit testing. I strongly advise you to look deeper into assertions. They are very useful and do not require as much work as unit testing.

Typical example:

SysUtils.Assert(Input <> Nil, ‘The input should not be nil!’);

But for the program to check our assertions, we need to active this feature in the Project Settings -> Compiler Options, otherwise they will simply be ignored, like they are not there in our code. Make sure that you understand the implications of what I just said! For example, we screw up badly if we call routines that have side effects in the Assert. In the example below, during Debugging when the assertions are on, the Test() function will be executed and 'This was executed' will appear in the Memo. However, during Release more, that text will not appear in the memo because now Assert is simply ignored. Congratulations we just made the program to behave differently in debug/release mode ☹.

function TMainForm.Test: Boolean;
begin
 Result:= FALSE;
 mmo.Lines.Add('This was executed');
end;

procedure TMainForm.Start;
VAR x: Integer;
begin
 x:= 0;
 if x= 0
 then Assert(Test(), 'nope');
end;

Here are a few examples of how it can be used:

1 To check if an input parameter is within the 0..100 range:

procedure DoSomething(value: Integer);
begin
  Assert((value >= 0) and (value <= 100), 'Value out of range');
  …
end;

2 To check if a pointer is not nil before using it:

Var p: Pointer;
Begin
  p := GetPointer;
  Assert(Assigned(p), 'Pointer is nil');
   …
End;

3 To check if a variable has a certain value before proceeding:

var i: Integer;
begin
   i := GetValue;
   Assert(i = 42, 'Incorrect response to “What is the answer to life”!');
  …
end;

Assertions can also be disabled by defining the NDEBUG symbol in the project options or by using the {$D-} compiler directives.

We can also use the Assert as a more elegant way of handling errors and exceptions in some cases, as it can be more readable and it also includes a custom message, that would help the developer understand what went wrong.

Personally, I use it a lot at the top of my routines to check if the parameters are valid.

Activating this feature will (naturally) make your program slower because… well, there is one extra line of code to execute.

Nothing comes for free

Everything nice comes with a price (fortunately a small price in our case): enabling Runtime error checking and Assertions slows down our program and makes it somewhat larger.

Computers today have lots of RAM so the slight increase in size is irrelevant, so, let’s put that aside. But let’s look at the speed, because that is not something we can easily ignore:

Type                 Disabled   Enabled
Range checking       73ms         120ms
Overflow checking    580ms        680ms
I/O checking         Not tested   Not tested

As we can see the program's speed is strongly impacted by these runtime checking. If we have a program where speed is critical, we better activate “Runtime error checking” during debugging only. What I do, I also leave it active in the first release and wait a few weeks. If no bugs are reported, then I release an update in which “Runtime error checking” is off.

Personally, I leave the “IO checking” always active. The performance hit because of this check is microscopic.


Big warning:
If you have an existing project that was not so nicely written, and you activate any of the Runtime Error checking below, your program may will crash more often than usual. No, the Runtime Error checking routines did not break your program. It was always broken – you just didn’t know. The Runtime Checking routines are now finding all those places where the code is fishy and shitty and smelly. The sole purpose of Runtime Checking is to find bugs in your program.

Gabriel
  • 20,797
  • 27
  • 159
  • 293