Self destructing object instance by Dalija Prasnikar
Dalija Prasnikar - 31/Dec/2019
Dalija Prasnikar - 31/Dec/2019
[SHOWTOGROUPS=4,20]
Using reference counting classes always requires some caution. You have to pay attention to reference cycles, using interface reference for referencing such object instances, not calling FreeAndNil and more... it is quite a list...
But every once in a while, new ways of shooting yourself in the foot keep popping up... So here goes the story...
I have two reference counted classes that hold reference to each other instances. One of those references is marked as [weak] to prevent creating strong reference cycle.
But I cannot successfully finish construction of TBar object instance and exiting Test procedure triggers Access Violation exception at _IntfClear.
Stepping through debugger shows that TBar.Destroy is called before code reaches writeln(Assigned(Intf)) line and there is no exception during construction process.
Why is destructor called during construction of an object here and why there is no exception?
Reference counting overview
To understand what is happening here we need short overview of how Delphi ARC works on reference counted object instances (ones implementing some interface) under classic compiler.
Reference counting basically counts strong references to an object instance and when last strong reference to an object goes out of scope, reference count will drop to 0 and instance will be destroyed.
Strong references here represent interface references (object references and pointers don't trigger reference counting mechanism) and compiler inserts calls to _AddRef and _Release methods at appropriate places for incrementing and decrementing reference count. For instance, when assigning to interface _AddRef is called, and when that reference goes out of scope _Release.
Simplified those methods generally look like:
Construction of reference counted object instance looks like:
class function TInterfacedObject.NewInstance: TObject;
begin
Result := inherited NewInstance;
TInterfacedObject(Result).FRefCount := 1;
end;
procedure TInterfacedObject.AfterConstruction;
begin
AtomicDecrement(FRefCount);
end;
Why is initial reference count in NewInstance set to 1 and not to 0?
Initial reference count must be set to 1 because code in constructors can be complex and can trigger transient reference counting which could automatically destroy the object during the construction process before it has chance to be assigned to the initial strong reference that will keep it alive.
That initial reference count is then decreased in AfterConstruction and object instance reference count is properly set for further reference counting.
Problem
Real problem in this questions code is in fact that it triggers transient reference counting in AfterConstruction method after call to inherited which decreases initial object reference count back to 0. Because of that, object will have its count increased, then decreased to 0 and it will self destruct calling Destroy.
While object instance is protected from self destruction inside constructor chain, for a brief moment it will be in fragile state inside AfterConstruction method and we need to make sure that there is no code there that can trigger reference counting mechanism during that time.
Actual code that triggers reference counting in this case is hidden in rather unexpected place and it comes in form of [weak] attribute. So, the very thing that should prevent instance from participating in reference counting mechanism actually triggers it - this is a flaw in [weak] attribute design reported as Для просмотра ссылки Войдиили Зарегистрируйся.
Solution(s)
[/SHOWTOGROUPS]
Using reference counting classes always requires some caution. You have to pay attention to reference cycles, using interface reference for referencing such object instances, not calling FreeAndNil and more... it is quite a list...
But every once in a while, new ways of shooting yourself in the foot keep popping up... So here goes the story...
I have two reference counted classes that hold reference to each other instances. One of those references is marked as [weak] to prevent creating strong reference cycle.
Код:
unit Unit2;
interface
type
TFoo = class(TInterfacedObject)
private
[weak]
FRef: IInterface;
public
constructor Create(const ARef: IInterface);
end;
TBar = class(TInterfacedObject)
private
FFoo: IInterface;
public
constructor Create; virtual;
destructor Destroy; override;
procedure AfterConstruction; override;
end;
implementation
constructor TFoo.Create(const ARef: IInterface);
begin
inherited Create;
FRef := ARef;
end;
constructor TBar.Create;
begin
inherited;
end;
destructor TBar.Destroy;
begin
inherited;
end;
procedure TBar.AfterConstruction;
begin
inherited;
FFoo := TFoo.Create(Self);
end;
procedure Test;
var
Intf: IInterface;
begin
Intf := TBar.Create;
writeln(Assigned(Intf)); // TRUE as expected
end; // AV here
end.
But I cannot successfully finish construction of TBar object instance and exiting Test procedure triggers Access Violation exception at _IntfClear.
Код:
Exception class $C0000005 with message 'access violation at 0x0040e398: read of address 0x00000009'.
Stepping through debugger shows that TBar.Destroy is called before code reaches writeln(Assigned(Intf)) line and there is no exception during construction process.
Why is destructor called during construction of an object here and why there is no exception?
Reference counting overview
To understand what is happening here we need short overview of how Delphi ARC works on reference counted object instances (ones implementing some interface) under classic compiler.
Reference counting basically counts strong references to an object instance and when last strong reference to an object goes out of scope, reference count will drop to 0 and instance will be destroyed.
Strong references here represent interface references (object references and pointers don't trigger reference counting mechanism) and compiler inserts calls to _AddRef and _Release methods at appropriate places for incrementing and decrementing reference count. For instance, when assigning to interface _AddRef is called, and when that reference goes out of scope _Release.
Simplified those methods generally look like:
Код:
function TInterfacedObject._AddRef: Integer;
begin
Result := AtomicIncrement(FRefCount);
end;
function TInterfacedObject._Release: Integer;
begin
Result := AtomicDecrement(FRefCount);
if Result = 0 then
Destroy;
end;
Construction of reference counted object instance looks like:
- construction - TInterfacedObject.Create -> RefCount = 0
- executing NewInstance
- executing chain of constructors
- executing AfterConstruction chain
- assigning to initial strong reference Intf := ...
- _AddRef -> RefCount = 1
class function TInterfacedObject.NewInstance: TObject;
begin
Result := inherited NewInstance;
TInterfacedObject(Result).FRefCount := 1;
end;
procedure TInterfacedObject.AfterConstruction;
begin
AtomicDecrement(FRefCount);
end;
Why is initial reference count in NewInstance set to 1 and not to 0?
Initial reference count must be set to 1 because code in constructors can be complex and can trigger transient reference counting which could automatically destroy the object during the construction process before it has chance to be assigned to the initial strong reference that will keep it alive.
That initial reference count is then decreased in AfterConstruction and object instance reference count is properly set for further reference counting.
Problem
Real problem in this questions code is in fact that it triggers transient reference counting in AfterConstruction method after call to inherited which decreases initial object reference count back to 0. Because of that, object will have its count increased, then decreased to 0 and it will self destruct calling Destroy.
While object instance is protected from self destruction inside constructor chain, for a brief moment it will be in fragile state inside AfterConstruction method and we need to make sure that there is no code there that can trigger reference counting mechanism during that time.
Actual code that triggers reference counting in this case is hidden in rather unexpected place and it comes in form of [weak] attribute. So, the very thing that should prevent instance from participating in reference counting mechanism actually triggers it - this is a flaw in [weak] attribute design reported as Для просмотра ссылки Войди
Solution(s)
- If possible, move code that can trigger reference counting from AfterConstruction to constructor
- Call inherited at the end of AfterConstruction method instead of the beginning.
- Perform some explicit reference counting on your own by calling AtomicIncrement(FRefCount) at the beginning and AtomicDecrement(FRefCount) at the end of AfterConstruction (you cannot use _Release because it will destroy the object)
- Replace [weak] attribute with [unsafe] (this can only be done if TFoo instance lifetime will never exceed TBar instance lifetime
[/SHOWTOGROUPS]