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.
