Monday, January 2, 2006

Why do I get AVs when creating an object?

There are three main reasons why you would get an AV on object creation:
  1. Error in the manner the Create method is called.
  2. Error in the Create method of the object.
  3. Other error in the program that is manifested during create.

1) Error in the manner the Create method is called.

This is probably the most frequent problem with people new to creating objects dynamically in Delphi. When you create an object variable in the var block of a procedure, you are actually allocating memory for a pointer. The first thing you need to do before actually using the object is to create an instance of it, or cause the pointer to point to something meaningful. Most people realize this is done with the create method. The problem is in getting the call correct. Example one below is syntactically correct but it doesn't work. Why? Because variable Obj1 is an uninitialized pointer. The correct way to call Create is shown in examples two, three and four.
procedure example1; { How not to do it! }
var
obj1: TMyObject;
begin
obj1.Create; { This will result in a AV }
obj1.SomeMethodCall;
obj1.Free;
end;

procedure example2; { This works }
var
obj2: TMyObject;
begin
obj2 := TMyObject.Create; { This is correct }
try
obj2.SomeMethodCall;
finally
obj2.Free;
end;
end;

procedure example3; { This works }
var
obj3: TMyObject;
begin
obj3 := TMyObject.Create; { This is correct }
with obj3 do
try
SomeMethodCall;
finally
Free;
end;
end;

procedure example4; { This is for trivial use of a temporary object }
begin
with TMyObject.Create do
try
SomeMethodCall;
finally
Free;
end;
end;

2) Error in the Create method of the object.

This case would be most probable when using a new object that has not been thoroughly debugged and there is a problem somewhere in the constructor or a subsequently called method. General debugging techniques need to be applied to the object in question to find this type of problem. A good principle to follow when creating an object is to simultaneously create a test unit for that object. The test unit should provide opportunities to verify all aspects of an object's functionality. Doing so will provide a limited environment in which to find problems and will increase confidence of an object's trustworthiness when it is used in a larger context.

When debugging new objects of your own descended from TComponent, one thing to keep in mind is any published property that is an object, must be created in the constructor. This is because the streaming mechanism will try to assign values to the object after the Create has been called. Another related item to remember is that items that are dependent on streamed properties need to be handled in the Loaded procedure, not the Create. An indication that there is a problem with uninitialized properties is a AV when trying to access the property in the object inspector.

Example one shows both problems. The component writer tried to be good and conserve resources by not creating the bitmap until it was set. This works as long as the property is not published or the component is only created at run time. However, it fails when it is a published property and created at design time. The object inspector uses the write method (SetBitmap) and so it is created OK during design time. However, when the project is run, the streaming mechanism tells the object to load itself, bypassing SetBitmap. The same thing happens when the project is closed and later reopened.

The Area property is a contrived example of having to use the Loaded property. If this was a real component you would want to either compute it each time and not worry about storing it or compute it in an assignment procedure for the Height and Length properties.
interface

type
TExample1 = class(TComponent)
private
FBitmap: TBitmap;
FArea,
FWidth,
FHeight: Word;
procedure SetBitmap(value: TBitmap);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property Bitmap: TBitmap read FBitmap write SetBitmap;
property Area: Word read FArea;
property Width: Word read FWidth write FWidth;
property Height: Word read FHeight write FHeight; end;

implementation

constructor TExample1.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FArea := Width * Height; { Width and Height haven't been assigned yet }
end;

procedure TExample1.SetBitmap(value: TBitmap);
begin
if not Assigned(FBitmap) then
FBitmap := TBitmap.Create;
FBitmap.Assign(value);
end;

destructor TExample1.Destroy;
begin
FBitmap.Free;
inherited Destroy;
end;

Example two shows how to initialize the object and use Loaded correctly.
interface

type
TExample2 = class(TComponent)
private
FBitmap: TBitmap;
FArea,
FWidth,
FHeight: Word;
procedure SetBitmap(value: TBitmap);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Loaded; override;
published
property Bitmap: TBitmap read FBitmap write SetBitmap;
property Area: Word read FArea;
property Width: Word read FWidth write FWidth;
property Height: Word read FHeight write FHeight;
end;

implementation

constructor TExample2.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FBitmap := TBitmap.Create;
end;

procedure TExample2.SetBitmap(value: TBitmap);
begin
FBitmap.Assign(value);
end;

destructor TExample2.Destroy;
begin
FBitmap.Free;
inherited Destroy;
end;

procedure TExample2.Loaded;
begin
inherited Loaded;
FArea := Width * Height;
end;

3) Other error in the program that is manifested during create.

One thing to always keep in mind when dealing with an AV problem: the problem may not be where the error currently occurs. An AV in essence says that your program tried to access memory that does not belong to it. If something in the program clobbered memory, you may just now be trying to access memory with a pointer that was corrupted earlier during execution.

One way to eliminate this as a possibility, and a good technique in general, is to verify an object's integrity in a stand-alone unit test. For every object that you create, also create a project that tests just the object without any other interactions. This builds confidence that the object is functioning correctly. If the object crashes in the test program, you know the object or test program is at fault, a limited set of circumstances. If the object works by itself but crashes when included in a larger system, that is a clue it may not be the object but something else. Just remember though, it does not rule out the object, it just removes suspicion. Something can corrupt memory and not provide any visible signs... for a while.

No comments: