Articles Self destructing object instance by Dalija Prasnikar

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,439
Credits
574
Self destructing object instance by Dalija Prasnikar
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.
Код:
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:
  1. construction - TInterfacedObject.Create -> RefCount = 0
  • executing NewInstance
  • executing chain of constructors
  • executing AfterConstruction chain
  1. 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;

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]