Dealing with Circular References

Why you get circular references in VB and two methods to prevent the problem.

Circular References Graphic

VB and COM makes a lot of things easy to do, particularly using and dealing with objects: you just create them and normally they clear themselves up automatically once you're finished with them. However, one side effect of the way COM works is that it is possible to create objects which cannot terminate properly: this problem is known as a "Circular Reference".

The effects of a circular reference can range from using more memory than you ought to (possibly to the extent that you get an Out Of Memory error) to much harsher effects when you are experimenting with API calls.

How VB Determines When to Terminate an Object

Normally, you don't need to worry about object termination. If you create an object using the New operator, you know that VB when the object goes out of scope VB will try to call its _Terminate method (if it has one) and clear up any memory associated with it. You can also set any object reference you have to Nothing which will do the same thing. So how does VB know when there are no outstanding references to the object? After all, you can run code like this:

Dim m_c As New Collection

Private Sub Form_Load()
   Dim c1 As New MyClass
   c1.Name = "This Will Terminate At The End Of Form_Load"
   Dim c2 As New MyClass
   c2.Name = c1.Name
   m_c.Add c2

End Sub ' c1 will terminate here, but c2 will not

The answer to this is provided by the fundamentals of COM objects. All VB objects are either explicitly created as COM objects (if they are exposed objects compiled into an ActiveX binary) or are created as such internally within VB. All COM objects must follow the COM contract, which means they must implement at least three methods:

  1. QueryInterface
  2. AddRef
  3. Release

The second two of these are of interest to how VB knows when to terminate an object. Whenever the AddRef method of a COM object is called, the object itself must increment an internal reference count by 1. Whenever Release is called, the object decrements the count. When the internal reference count of the object reaches zero then it can be terminated and any resources associated with it cleared up.

A C++ coder using a COM object has complete control over when AddRef and Release are called. This means that you can decide if a particular object always lives within the lifetime of another object, and decide not to call AddRef. This is considerably more flexibility than you have with VB. VB always calls AddRef and Release for you at the correct moments. That is fine, except it results in a problem called Circular References.

Creating a Circular Reference

A circular reference occurs when there are two objects which refer to each other. Say you have a class which manages a series of Worker objects that perform specific tasks, and the Worker objects need to notify their parent, or to interact with data held by the parent. What happens is this:

  • When you create the Parent object in VB, AddRef is called. Parent now has a reference count of 1
  • When Parent creates an instance of a Worker, AddRef is called on the Worker. Worker now also has a reference count of 1
  • Worker gets a reference to the Parent object to allow it to notify or interact with the data. The Parent object reference count is incremented to 2.
  • When Parent later goes out of scope, VB calls Release. This reduces the parent reference count to 1 (the reference held by the Worker). However, this now means that Parent still has a reference count of 1. Unless something can tell the Worker object to release its reference, both Parent and Worker will remain with reference counts until the application terminates. Remember that since the Parent does not enter the Class_Terminate method, there is nothing that will occur in the code to allow you to know that its main reference has been terminated and so it looks from Parent's point of view that it is still required, even though it is only a child class which is requiring it!

The only way that these objects can go out of scope is when the VB process finally shuts down. Then VB will terminate all the remaining objects it knows about, and the order in which that occurs is not known. Fine if the order doesn't affect the object VB is terminating, but not so good if you need to ensure if something terminates first (e.g. to close a handle).

The circular reference problem typically occurs when you're trying to create a strongly typed collection of objects for a control. When you modify elements of the object held in the collection typically this has to affect the control itself. However, if the child object has a reference to the control to notify the control about the change, then you have a circular reference.

Resolving Circular References

There are three ways you can go about preventing circular references. These are:

  1. Using a Late Bound events
  2. Providing a "Dispose" style method.
  3. Using Non-Counted References

Whilst the event method is a possible solution, it is not covered here. The problem with using events is that the events have to be wired up at design time. If you don't know how many child objects you're going to have then it is all but impossible to design a solution to fix this.

The other two solutions are demonstrated in the example projects and are covered in turn.

Providing a "Dispose" Style Method

The problem with a circular reference is there is no event which automatically occurs when the owner of the Parent object releases its reference. Clearly, however, you can provide a method in the Parent object which goes through any child Worker objects and terminates the object reference. By doing this, the Worker objects release their references on the parent, so the additional reference count is removed, and then the Parent object can terminate as soon as it goes out of scope.

This method is demonstrated in the Fix 1 download. However, whilst it works ok, it has two distinct disadvantages:

  1. The owner of the object needs to know when it releases its reference. That's fine for a DLL object, but very difficult for something like a control, because VB's form engine is actually in charge of when it wants to terminate the object.
  2. The user of the object needs to remember to call the "Dispose" style method. This isn't a very natural way to code in VB and easily forgotten.

Ideally there would be a way of coding these objects which could be used in a normal VB style by any user. Luckily there is, but you need to incorporate a few hacks.

Using Non-Counted References

This method is the equivalent of what a C++ coder would do when they knew they did not have to call AddRef or Release on an object reference because its scope could never go outside the bounds of the parent object. There are two ways you can go about doing this:

  1. Using object pointers.

    The object pointer technique, first demonstrated by Bruce McKinney in his legendary book Hardcore Visual Basic (a book that is still causing embarassment via my harddrive, as his frequent use of the term "Hardcore" seems to convince others I've been up to tricks on the Internet. Then again, things could be worse; it could have been called Teenage Beaver Visual Basic, although I suppose that would have been somewhat unlikely), is a tried and trusted technique on this site. The concept is simple: all COM objects are accessed through a pointer in memory, and this is returned by VB's ObjPtr function. Calling ObjPtr does not change the reference count. Therefore if you can somehow reconstruct an object from its pointer, then you have a method to store an object without changing its reference count.

    Luckily there is a way, as follows:

       ' To Retrieve the Object From the Long Value:
       Private Property Get ObjectFromPtr(ByVal lPtr As Long) As Object
       Dim objT As Object
           ' Bruce McKinney's code for getting an Object from the
           ' object pointer:
           CopyMemory objT, lPtr, 4
           Set ObjectFromPtr = objT
           CopyMemory objT, 0&, 4
       End Property

    The Fix 2 download demonstrates this technique.

  2. Using IUnknown

    If you have access to a Type Library which provides the basic IUnknown COM object interface, containing QueryInterface, AddRef and Release, you can cast your object to that type and call the Release method on it as soon as you have copied the object reference. This decrements the reference count by 1 and hence as soon as the parent reference goes out of scope Class_Terminate can be called.

    Note that VB itself appears to know about the IUnknown interface even though you won't see it in the object browser. Declaring something as IUnknown is equivalent to using As Object in VB, but VB does not allow you to call the AddRef or Release methods through this route (you'll get an error). To use IUnknown properly you need a Type Library, and you need to prefix the IUnknown type declaration with the Type Library's name, otherwise you get VB's mindless version.

    This version is demonstrated in the Fix 3 download. Looking at the code, it is a whole lot more VB-like than the Fix 2 version and actually almost quite nice! However, there's a downside. If the Worker classes are terminated prior to the Parent class, you get a GPF. Whether you can be entirely sure which order VB will terminate its objects, I'm not certain whether this a good idea.

Wrap Up

COM reference counting and the circular reference problem associated with it are one of the major bugbears with all COM related projects. You will note that the .NET Framework, like Java, does away with reference counting altogether and takes a completely different approach to managing objects in memory. That means you do have no choice but to consider the "Dispose" route if you need to be certain when objects terminate in .NET, but at least it is obvious that this will be a problem and there are certain constructs, such as the IDisposable interface and the using language features that make it more likely that these will be used correctly.

This article demonstrates how you get a circular reference and a number of techniques for removing the problem. Although all have a measure of danger, so do circular references with no attempt at resolution. If used judiciously, you should find the techniques presented here will stop these problems in their tracks.