Functions for handling weak pointers
As
we already stated in this blog, such a Zeroing ARC model has been
implemented in Objective C by Apple, starting with Mac OS X 10.7 Lion,
in replacement (and/or addition) to the previous manual memory handling
implementation pattern: in its Apple's flavor, ARC is available not only for
interfaces, but for objects, and is certainly more sophisticated than the basic
implementation available in the Delphi compiler: it is told (at least from the
marketing paper point of view) to use some deep knowledge of the software
architecture to provide an accurate access to all instances - whereas the
Delphi compiler just relies on a out-of-scope pattern. In regard to
classic garbage collector memory model, ARC is told to be much more
efficient, due to its deterministic nature: Apple's experts ensure that it does
make a difference, in term of memory use and program latency - which both are
very sensitive on "modest" mobile devices. In short, thanks to ARC, your phone
UI won't glitch during background garbage recycling. So mORMot
will try to offer a similar feature, even if the Delphi compiler does not
implement it (yet).
The main issue with reference counting is the potential circular
reference problem. This occurs when an interface
has a strong
pointer to another, but the target interface
has a strong pointer
back to the original. Even when all other references are removed, they still
will hold on to one another and will not be released. This can also happen
indirectly, by a chain of objects that might have the last one in the chain
referring back to an earlier object.
See the following interface
definition for instance:
IParent = interface procedure SetChild(const Value: IChild); function GetChild: IChild; function HasChild: boolean; property Child: IChild read GetChild write SetChild; end;
IChild = interface procedure SetParent(const Value: IParent); function GetParent: IParent; property Parent: IParent read GetParent write SetParent; end;
The following implementation will definitively leak memory:
procedure TParent.SetChild(const Value: IChild); begin FChild := Value; end; procedure TChild.SetParent(const Value: IParent); begin FParent := Value; end;
In Delphi, most common kind of reference-copy variables (i.e.
variant
, dynamic array or string
) solve this
issue by implementing copy-on-write. Unfortunately, this pattern is
not applicable to interface
, which are not value objects, but
reference objects, tied to an implementation class
, which can't be
copied.
One common solution is to use Weak pointers, by which the
interface
is assigned to a property without incrementing the
reference count. In order to easily create a weak pointer, the following
function was added to SQLite3Commons.pas
:
procedure SetWeak(aInterfaceField: PIInterface; const aValue: IInterface); begin PPointer(aInterfaceField)^ := Pointer(aValue); end;
Therefore, it could be used as such:
procedure TParent.SetChild(const Value: IChild); begin SetWeak(@FChild,Value); end; procedure TChild.SetParent(const Value: IParent); begin SetWeak(@FParent,Value); end;
But there are still some cases where it is not enough. Under normal
circumstances, a class
instance should not be deallocated if there
are still outstanding references to it. But since weak references don't
contribute to an interface
reference count, a class
instance can be released when there are outstanding weak references to it. Some
memory leak or even random access violations could occur. A debugging
nightmare...
In order to solve this issue, ARC's Zeroing Weak pointers come to
mind.
It means that weak references will be set to nil
when the object
they reference is released. When this happens, the automatic zeroing of the
outstanding weak references prevents them from becoming dangling pointers. And
voilà! No access violation any more!
In order to easily create a so-called zeroing weak pointer, the following
function was defined in SQLite3Commons.pas
:
procedure SetWeakZero(aObject: TObject; aObjectInterfaceField: PIInterface; const aValue: IInterface);
A potential use case could be:
procedure TParent.SetChild(const Value: IChild); begin SetWeakZero(self,@FChild,Value); end; procedure TChild.SetParent(const Value: IParent); begin SetWeakZero(self,@FParent,Value); end;
We also defined a class helper
around the TObject
class, to avoid the need of supplying the self
parameter, but
unfortunately, the class helper
implementation is so buggy it
won't be even able to compile before Delphi XE version of the compiler. But it
will allow to write code as such:
procedure TParent.SetChild(const Value: IChild); begin SetWeak0(@FChild,Value); end;
For instance, the following code is supplied in the regression tests, and
will ensure that weak pointers are effectively zeroed when
SetWeakZero()
is used:
function TParent.HasChild: boolean; begin result := FChild<>nil; end;
Child := nil; // here Child is destroyed Check(Parent.HasChild=(aWeakRef=weakref),'ZEROed Weak');
Here, aWeakRef=weakref
is true
when
SetWeak()
has been called, and equals false
when
SetWeakZero()
has been used to assign the Child
element to its Parent
interface.
Weak pointers implementation details
The SetWeak()
function itself is very simple (see
above).
The Delphi RTL/VCL itself use similar code when necessary.
But the SetWeakZero()
function has a much more complex
implementation, due to the fact that a list of all weak references has to be
maintained per class
instance, and set to nil
when
this referring instance is released.
The mORMot implementation tries to implement:
- Best performance possible when processing the Zeroing feature;
- No performance penalty for other classes not involved within weak
references;
- Low memory use, and good scalability when references begin to define huge
graphs;
- Thread safety - which is mandatory at least on the server side of our
framework;
- Compatible with Delphi 6 and later (avoid syntax tricks like
generic
).
Some good existing implementations can be found on the Internet:
- Andreas Hausladen provided a classical and complete implementation
at http://andy.jgknet.de/blog/2009/06/weak-interface-references
using some nice tricks (like per-instance optional speed up using a void
IWeakInterface interface
whose VMT slot will refer to the
references list), is thread-safe and is compatible with most Delphi versions -
but it will slow down all TObject.FreeInstance
calls (i.e. within
Free / Destroy
) and won't allow any overriden
FreeInstance
method implementation;
- Vincent Parrett proposed at
http://www.finalbuilder.com/Resources/Blogs/PostId/410/WeakRefence-in-Delphi-solving-circular-interfac.aspx
a generic
-based solution (not thread-safe nor optimized for
speed), but requiring to inherit from a base class for any class
that can have a weak reference pointing to it;
- More recently, Stefan Glienke published at http://delphisorcery.blogspot.fr/2012/06/weak-interface-references.html
another generic
-based solution, not requiring to inherit from a
base class, but not thread-safe and suffering from the same limitations related
to TObject.FreeInstance
.
The implementation included within mORMot uses several genuine
patterns, when compared to existing solutions:
- It will hack the TObject.FreeInstance
at the class
VMT level, so will only slow down the exact class
which is used as
a weak reference, and not others (also its inherited classes
won't
be overriden) - and it will allow custom override of the virtual
FreeInstance
method;
- It makes use of our
TDynArrayHashed
wrapper to provide a very fast lookup of instances
and references, without using generic
definitions;
- The unused vmtAutoTable
VMT slot is used to handle the
class-specific orientation of this feature (similar to TSQLRecordProperties
lookup as implemented for DI-2.1.3), for best speed and memory use.
See the TSetWeakZeroClass
and TSetWeakZeroInstance
implementation in SQlite3Commons.pas
for the details.
Feedback is welcome in our forum, just as usual.