Lazy Object / Weak Reference

Sometimes a class needs to hold a reference to the object that “owns” it – i.e. the object that created it. When this happens, the owner object often needs to hold a reference to all the “child” objects it creates. If we say Class1 is the “parent” and Class2 is the “child”, we get something like this:

'Class1
Option Explicit
Private children As VBA.Collection

Public Sub Add(ByVal child As Class2)
    Set child.Owner = Me
    children.Add child
End Sub

Private Sub Class_Initialize()
    Set children = New VBA.Collection
End Sub

Private Sub Class_Terminate()
    Debug.Print TypeName(Me) & " is terminating"
End Sub

And Class2 might look like this:

'Class2
Option Explicit
Private parent As Class1

Public Property Get Owner() As Class1
    Set Owner = parent
End Property

Public Property Set Owner(ByVal value As Class1)
    Set parent = value
End Property

Private Sub Class_Terminate()
    Debug.Print TypeName(Me) & " is terminating"
End Sub

The problem might not be immediately apparent to untrained eyes, but this is a memory leak bug – this code produces no debug output, despite the Class_Terminate handlers:

'Module1
Option Explicit

Public Sub Test()
    Dim foo As Class1
    Set foo = New Class1
    foo.Add New Class2
    Set foo = Nothing
End Sub

Both objects remain in memory and outlive the Test procedure scope! Depending on what the code does, this could easily go from “accidental sloppy object management” to a serious bug leaving a ghost process running, with Task Manager being the only way to kill it! How do we fix this?

Not keeping a reference to Class1 in Class2 would fix it, but then Class2 might not be working properly. Surely there’s another way.

Suppose we abstract away the very notion of holding a reference to an object. Suppose we don’t hold an object reference anymore, instead we hold a Long integer that represents the address at which we’ll find the object pointer we’re referencing. To put it in simpler words, instead of holding the object itself, we hold a ticket that tells us where to go find it when we need to use it. We can do this in VBA.

First we define an interface that encapsulates the idea of an object reference – IWeakReference, that simply exposes an Object get-only property:

'@Description("Describes an object that holds the address of a pointer to another object.")
'@Interface
Option Explicit

'@Description("Gets the object at the held pointer address.")
Public Property Get Object() As Object
End Property

Then we implement it with a WeakReference class. The trick is to use CopyMemory from the Win32 API to take the bytes at a given address and copy them into an object reference we can use and return.

For an easy-to-use API, we give the class a default instance by toggling the VB_PredeclaredId attribute, and use a factory method to create and return an IWeakReference given any object reference: we take the object’s object pointer using the ObjPtr function, store/encapsulate that pointer address into a private instance field, and implement the IWeakReference.Object getter such that if anything goes wrong, we return Nothing instead of bubbling a run-time error.

VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "WeakReference"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Option Explicit
Implements IWeakReference

#If Win64 Then
Private Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (hpvDest As Any, hpvSource As Any, ByVal cbCopy As LongPtr)
#Else
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (hpvDest As Any, hpvSource As Any, ByVal cbCopy As Long)
#End If

Private Type TReference
#If VBA7 Then
    Address As LongPtr
#Else
    Address As Long
#End If
End Type

Private this As TReference

'@Description("Default instance factory method.")
Public Function Create(ByVal instance As Object) As IWeakReference
    With New WeakReference
        .Address = ObjPtr(instance)
        Set Create = .Self
    End With
End Function

Public Property Get Self() As IWeakReference
    Set Self = Me
End Property

#If VBA7 Then
Public Property Get Address() As LongPtr
#Else
Public Property Get Address() As Long
#End If
    Address = this.Address
End Property

#If VBA7 Then
Public Property Let Address(ByVal Value As LongPtr)
#Else
Public Property Let Address(ByVal Value As Long)
#End If
    this.Address = Value
End Property

Private Property Get IWeakReference_Object() As Object
' Based on Bruce McKinney's code for getting an Object from the object pointer:

#If VBA7 Then
    Dim pointerSize As LongPtr
#Else
    Dim pointerSize As Long
#End If

    On Error GoTo CleanFail
    pointerSize = LenB(this.Address)

    Dim obj As Object
    CopyMemory obj, this.Address, pointerSize

    Set IWeakReference_Object = obj
    CopyMemory obj, 0&, pointerSize

CleanExit:
    Exit Property

CleanFail:
    Set IWeakReference_Object = Nothing
    Resume CleanExit
End Property

Now Class2 can hold an indirect reference to Class1, like this:

'Class2
Option Explicit
Private parent As IWeakReference

Public Property Get Owner() As Class1
    Set Owner = parent.Object
End Property

Public Property Set Owner(ByVal Value As Class1)
    Set parent = WeakReference.Create(Value)
End Property

Private Sub Class_Terminate()
    Debug.Print TypeName(Me) & " is terminating"
End Sub

Now Module1.Test produces the expected output, and the memory leak is fixed:

Class1 is terminating
Class2 is terminating

25 thoughts on “Lazy Object / Weak Reference”

  1. Good article!
    But please tell me, why do you on the one hand use “#If Win64” and on the other “#If VBA7” to distinguish between “Long” and “LongPtr”?

    Like

    1. TBH the conditional compilation isn’t bullet-proof here; a 64-bit Windows running VBA6 probably wouldn’t be able to compile it, precisely because of the “#If Win64” check you’re highlighting (PtrSafe only exists in VBA7). Anyway the idea is to make sure we’re using the correct size for the object pointer; hard-coding a Long wouldn’t work with all possible configurations.

      Like

  2. Just a thought; because your collection (children) has a link/reference to Class2, the terminate events won’t be fired. They will be fired when you first set your collection to Nothing before setting foo to Nothing

    Like

    1. In this example, yes. I’ll triple-check again, but the reason I wrote this (perhaps easily abused) class was precisely because of a scenario where nulling the parent reference in the child instance, did not untie the knot; that case was an event provider custom class wrapping a dynamic MSForm control, with the form having a private WithEvents reference to the child class. I took it that explicitly nulling references wasn’t consistently reliable and proceeded to make the object references indirect like this, and the problem was resolved – I’ll probably make a follow-up post, or edit this one later, when I have a more “real-world” piece of example code for it… on the other hand, making the reference explicitly “weak” removes the need to explicitly set it to Nothing, so.. I’m not sure whether to consider it abuse or over-complexifying the situation, vs. making such intertwined references safe to use without needing to think of nulling them.

      Like

  3. FYI, the only Win32 API functions (of all the ones that I use) that require “#If Win64” are “GetWindowLong” & “SetWindowLong” (within the “#If VBA7” condition). Otherwise, “#If VBA7” is the main difference because it introduced “LongPtr”.

    Like

  4. Q. What is the difference between using ‘CopyMemory obj, 0&, pointerSize” or replacing it with with “Set obj = nothing”?…and why does it crash Excel (after class1 terminates)?

    Like

    1. Interesting – I haven’t experienced any crashes with the code exactly as it is, on both 32 and 64 bit hosts, although I did toy a bit with that code: omitting the last CopyMemory call immediately crashes everything. I figured there was a reason Bruce McKinney did it that way, so I left it alone – wouldn’t “Set obj = Nothing” confuse the reference-counting though? The object wasn’t created by normal means, it shouldn’t be destroyed by normal means either. Are you saying *this code* crashes? What version+bitness of Excel are you using, in what OS+bitness? Or is it “Set obj = Nothing” that’s crashing?

      Like

      1. Using “Set obj = Nothing” crashed Excel every time, but Bruce’s method was fine. I’m using Win 7(64bit)/Excel 2013(32-bit).

        I had to add a few lines of code, so that I could access the “Owner” property of the child, to actually trigger “IWeakReference_Object” to run. This included a “Property Get Item(ByVal Index As Long) As Object” in Class1.

        Like

      2. What you have is the obj is a managed variable “wasn’t create by normal means”. With managed variables, you’re responsible for destroying them before they go out of scope before VBA attempts to clean it up. Expect for a hard landing if VBA is attempting to clean up a managed variable.

        They are destroyed/disposed of by zero filling them.

        The simple rule is if you create a managed variable you must also destroy it. 🙂

        Like

  5. Tried using this with a reference to a Workbook in a class that is cached, in order to avoid a phantom VBE reference hanging around when the Wb is closed. It works great for that purpose, but a subsequent test of the reference leads to a crash instead of a clean failure. Any suggestions?
    Am using 64-bit Office 365.

    Like

    1. Hmm, I intended this to use with custom VBA classes, which don’t involve COM objects that are owned by the host application… there is likely something else going on here. I’d be curious to see the original code with the ghost instance.. are you working in Excel or creating an Excel.Application object? Are you accessing VBE objects? Is the VBE Extensibility library referenced? It’s very easy to make “ghost” objects with chained member calls under these circumstances. With a good MCVE, that would make a great question on Stack Overflow!

      Like

  6. I made some dummy classes and test cases that illustrate the problem. Will be a detailed SO question, but I agree an interesting one, so will post one at some point later. What does MCVE stand for though??

    Like

  7. Great article and very interesting topic regarding memory leaks and the deconstructor Class_Terminate() not firing. Will have to dig up where I read that with Preclared classes you require to write your own deconstructor to do the cleanup. The Lazy Object/Weak Reference seems a bit of “hack” to avoid memory leaks thou an important topic that’s easily overlooked. On that note, I’m sure my memory requires some cleaning up as keep forgetting things. 😀

    Like

  8. Wondering if combining this topic with ideas you share elsewhere (“There Is No Workbook”, I think) is something worth doing or if there are pitfalls in it. Also you mentioned above that this is “easily abused” – How so? You also said you might revisit this and provide a more “real world” version…
    Here’s the code before the change I’m considering:
    ‘Class2
    Option Explicit
    Private parent As Class1
    Public Property Get Owner() As Class1
    Set Owner = parent
    End Property

    And then after:
    ‘Class2
    Option Explicit
    Private Type TModel
    Parent As Class1
    End Type
    Private this as TModel
    Public Property Get Parent() As Class1
    Set Parent = this.Parent
    End Property

    Like

  9. Just re-reading this, and wondered whether you’d be able to write something about object disposal and circular references. Stuff like how to use events to break parent/child (children) cycles like this one – and ensure children are terminated when the parent is. Also whether you should ever use `Unload` or `Set obj = Nothing`.

    Like

  10. I tried using this concept in a class I created (more-knowledgeable friends have informed me that my class is a decorator pattern). When I tried to replace “Public WithEvents Form As Access.Form” with “Public WithEvents Form As IWeakReference”, I got “Compile error: Object does not source automation events”. Does this WeakReference work with events?

    Like

    1. No, it doesn’t make sense to declare an object WithEvents as IWeakReference. As the error statement says, IWeakReference doesn’t have any automation events, since it’s just an interface for a generic object. When you use WithEvents, the object being declared has to refer to a class that exposes at least one event; there are no events declared in IWeakReference. You would still use “Public WithEvents Form As Access.Form” or whatever the full class is; if there is a parent/child relationship you would declare the parent as IWeakReference. TBH, your desire to use this concept in a Form confuses me, but I don’t actually use Access’ front end for anything, so who knows.

      Liked by 1 person

  11. Thanks for this article and many others. And for RubberDuck of course.
    I needed a similar solution but one that does not do API calls when retrieving the Object. Retrieving the object using 2 external API calls per each method call is adding much more time compared to plain VBA calls (when calling millions of times for example).
    So I decided to create a WeakObject class that does just that. No API calls when retrieving the object. Just 2 initial API calls to setup a couple of Variants and that’s it. Hope this is useful:
    https://github.com/cristianbuse/VBA-WeakReference

    Liked by 1 person

Leave a comment