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.
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;
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
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?
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
NewInstance
AfterConstruction
chain assigning to initial strong reference Intf := ...
_AddRef -> RefCount = 1
To understand actual problem we need to dig deeper in construction sequence, particularly NewInstance
and AfterConstruction
methods
class function TInterfacedObject.NewInstance: TObject;
begin
Result := inherited NewInstance;
TInterfacedObject(Result).FRefCount := 1;
end;
procedure TInterfacedObject.AfterConstruction;
begin
AtomicDecrement(FRefCount);
end;
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.
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 RSP-20406.
AfterConstruction
to constructorinherited
at the end of AfterConstruction
method instead of the beginning.AtomicIncrement(FRefCount)
at the beginning and AtomicDecrement(FRefCount)
at the end of AfterConstruction
(you cannot use _Release
because it will destroy the object)[weak]
attribute with [unsafe]
(this can only be done if TFoo
instance lifetime will never exceed TBar
instance lifetimeIf you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With