2007-08Aug-10

by Christof Wollenhaupt, Foxpert


In this article I will outline a base class that doesn't only offer support in avoiding dangling references, it also is capable of detecting them automatically. If you incorporate these concepts into your own base classes and don't ignore the class' warning messages, you should be able to avoid dangling references almost completely. It's hard to avoid them entirely, because there are always errors in Visual FoxPro that cause dangling references. Older versions, for instance, had a bug in PEMSTATUS() which still causes many developers not to put more than one PEMSTATUS() call in each IF statement. Current versions have difficulties with FOR EACH and WITH…ENDWITH.

You can download the entire class here.

The following _Custom class bases on Custom. To be effective, this code must be added to all base classes, not just custom. As most classes do this class also needs a new additional properties:

   lDoneInit = .F.             && Init executed

   lDonePostInit = .F.         && Postinit executed

   lDoneCleanup = .F.          && CleanUp executed

   lDoneDestroy = .F.          && Destroy executed

   lIsClassObject = .T.        && Is this really an object?

   lReady = .F.                && Object fully initialized

   cID = ""                    && ID for reference tracking

  

   lNeedsPostInit = .F.

We'll get back to these propert2023 foxpert GmbH on. Let's start with the Init event:

Procedure Init

 

   This.lIsClassObject = .F.

  

   This.cID = Sys(2015)

   StrToFile( ;

      Ttoc(Datetime())+[,]+This.cID+[,"INIT","]+Sys(1272,This)+;

      ["]+Chr(13)+Chr(10), ;

      Addbs(Home())+"ReferenceTracking.Txt", ;

      .T. ;

   )

  

   If Program(-1) > 1

      If Right(Upper(Program(Program(-1)-1)),5) == ".INIT"

         Debugout Sys(1272,This)+": Init overidden?"

      Endif

   Endif

  

   Local llOK

   llOK = This.DoInit()

   Assert Vartype(m.llOK)=="L" MESSAGE "wrong data type"

  

   If not This.lNeedsPostInit

      This.lReady = m.llOK

   Endif

 

   Assert not This.lDoneInit MESSAGE "repeated initialization"

   This.lDoneInit = .T.

  

   Assert This.IsValidReference(This) MESSAGE "THIS invalid"

   If not m.llOK

      This.Cleanup()

   Endif

  

Return m.llOK

The lIsClassObject property offers an easy way to detect if a reference points to a class object. In a class object all properties have the default value of a class. Hence, lIsClassObject must be .T. in a class object. First thing we do in Init is to set this property to .F. Now we can use this property to distinguish class and instance objects.

One way to find dangling references turned out to be reference tracking. What identifies a dangling object is that its Destroy event hasn't yet been fired. I personally haven't yet encountered a situation in which Destroy fired and the object was still in memory. By logging the Init as well as the Destroy event we can easily find out what objects are currently in memory. If you discover objects in this list that you assumed to be released you can specifically look why this particular object is hanging around in memory.

Reference tracking and all other measurements base on the idea that the code in Init is the first code to be executed. For this reason you shouldn't override the Init method. Instead the object offers a DoInit method that you can use instead. This method checks if it has been called by another Init method to support you in detecting whether you overrode the Init event by accident. Unfortunately, the calling method can also be Init if you create an object in the Init method. Because the code cannot determine with certainty if the calling Init method is it's own child class code, or code from a different object, it issues a warning using DEBUGOUT.

It's always a good idea to notify developers of definite problems using ASSERT. However, fore warnings that probably indicate a problem, but might also a valid case, you rather use DEBUGOUT. Otherwise developers would be tortured with a serious of unjustified ASSERT dialog and easily pick the "Ignore all" button to get rid of them. That, however, is exactly the wrong thing to do.

Because we set a flag in Init to indicate that the event has been executed, we can also check if Init is executed multiple times. That's the case if you have multiple DODEFAULTS() in your code, but also happens due to some bugs in VFP.

IsValidReference is a method that checks if a reference is valid. Not only does it check if the value points to an object at all, it also ensures that reference doesn't point to a class object. Normally, you shouldn't ever get a class object reference. However, if you use an expression in the property definition and this expression uses THIS, then you have a class reference. If you save the reference somewhere else, you automatically have a dangling reference and increase the likelihood of a crash.

In Visual FoxPro 8 and later there's actually code that performs the same check. When you attempt to assign a class reference to a property, Visual FoxPro raises error 2070: Cannot assign a class value to this member. Nonetheless, some bugs in Visual FoxPro can still cause class references. These bugs usually appear in a combination of EVALUATE() and nested container class hierarchies, or in errors with reference counting. This method doesn't prevent such class references, but immediately notifies you when they occur. Additionally, the code checks if the object has already been released. In that case you shouldn't call method of the object, either.

Function IsValidReference

LParameter toReference

     

   If Vartype(m.toReference) # "O"

      Return .F.

   Endif

  

   If PemStatus(m.toReference,"lIsClassObject",5)

      If m.toReference.lIsClassObject

         Return .F.

      Endif

   Endif

  

   If PemStatus(m.toReference,"lDoneCleanup",5)

      If m.toReference.lDoneCleanup

         Return .F.

      Endif

   Endif

 

   If PemStatus(m.toReference,"lDoneDestroy",5)

      If m.toReference.lDoneDestroy

         Return .F.

      Endif

   Endif

 

Return .T.

In the Init event you can only access those objects that are beneath the current object in the object hierarchy. In a grid's Init, for instance, you can only access the column and all contained objects. Accessing other objects might appear to work, but frequently causes dangling references or to references pointing to class objects. Therefore, you should move all initializations that depend on other objects into a separate method. Call this method at the end of the instantiation process, such as from the Init event of the form.

Procedure PostInit

 

   If Program(-1) > 1

      If Right(Upper(Program(Program(-1)-1)),9) == ".POSTINIT"

         Debugout Sys(1272,This)+": Postinit overidden?"

      Endif

   Endif

  

   Assert not This.lDonePostInit MESSAGE "repeated PostInit"

  

   Assert This.lNeedsPostInit MESSAGE ;

      "lNeedsPostInit must be .T. "

  

   Local llOK

   llOK = This.DoPostInit()

   Assert Vartype(m.llOK)=="L" MESSAGE "wrong datatype"

   This.lReady = m.llOK

  

   This.ValidateReferences()

 

   This.lDonePostInit = .T.

 

EndProc

Just like in the case of Init sub-classed code goes into the DoPostInit method instead of PostInit. This way you don't need to deal with multiple calls and the like which force you to repeat a good portion of the code every time you create a subclass.

The lReady flag is set when the object is completely initialized. If a class requires that you call PostInit, it's only ready after PostInit has completed successfully. On the other hand, if there are no dependencies with other objects, the object is fully initialized right after the Init event succeeded. The lNeedsPostInit flag distinguishes both cases. By default it is .F. because classes do not have dependencies right in the Init.

Once the user defined code has been executed, ValidateReferences checks if all references also point to valid objects. This way you can find objects that shut themselves down preliminarily. Basically, this method iterates through all properties and tests if the property refers to another object. Every object is then checked with IsValidReference whether it's a valid object. Every element of an array is validated. To avoid that code is being executed, this code only checks those properties that don't have an access method.

Function ValidateReferences

 

   Assert This.IsValidReference(This) MESSAGE "THIS invalid"

 

   Local laMember[1], lnMember, loReference, lnCount, lnItem

   lnCount = 0

   For lnMember = 1 to AMembers(laMember,This)

      If PemStatus(This,laMember[m.lnMember]+"_Access",5)

         Loop

      Endif

      If not PemStatus(This,laMember[m.lnMember],4)

         Loop

      Endif

      If Type("Alen(This."+laMember[m.lnMember]+")") == "N"

         For lnItem=1 to Alen(This.&laMember[m.lnMember])

            loReference = This.&laMember[m.lnMember][m.lnItem]

            If Vartype(m.loReference) == "O"

               lnCount = m.lnCount + 1

               Assert This.IsValidReference(m.loReference) ;

                  MESSAGE "loReference invalid"

            Endif

         Endfor

         loReference = NULL

      Else

         loReference = GetPem(This,laMember[m.lnMember])

         If Vartype(m.loReference) == "O"

            lnCount = m.lnCount + 1

            Assert This.IsValidReference(m.loReference) ;

               MESSAGE "loReference invalid"

         Endif

         loReference = NULL

      Endif

   Endfor

  

Return m.lnCount

Once the object has loaded successfully, you can call the IsReady() method to determine if it's still usable:

Function IsReady

 

   If not This.IsValidReference(This)

      Return .F.

   Endif

  

   If not This.lDoneInit

      Return .F.

   Endif

  

   If not This.lReady

      Return .F.

   Endif

  

   Local llIsReady

   llIsReady = This.DoIsReady()

   Assert Vartype(m.llIsReady)=="L" MESSAGE "wrong data type"

   If not m.llIsReady

      Return .F.

   Endif

  

Return .T.

To detect errors as early as possible, you should use

ASSERT This.IsReady()

in every method. When you access other objects, you should also check if the object is still valid. If objects have been released in the wrong order you'll notice that in time and do not suffer from using half-initialized objects.

To avoid performance issues at runtime you should encapsulate this and similar code using #IF…#ENDIF. It's a good idea to define a constant like _DEBUG that you set either to .T. or to .F. This way you can create test versions that perform extensive validation without affecting your users that receive the release version.

At some point you don't need an object anymore and can release it. It's important to take into account that VFP only releases an object when all references to this object have been removed. Additionally, you should avoid that Visual FoxPro releases other objects while destroying one object. In other words, in the Destroy event it is already too late to deal with references. Cleaning up an object must happen before VFP attempts to release it. One approach is to define a Release method for all objects. Call this method explicitly release an object without waiting for Visual FoxPro to decide that the object should go:

Procedure Release

 

   Assert This.IsValidReference(This) MESSAGE "THIS invalid"

  

   This.Cleanup()

   Release THIS

  

Endproc

The CleanUp method turns the object into a releasable state. That means, it still exists, but it doesn't have any connection to other objects and it shouldn't be called afterwards. This method takes care of calling the CleanUp method of all contained object. If you implemented this method consequently in all classes in your framework, you only need to call the Release method of the outermost object, to turn the entire set of object into a release state. All dependent objects are cleaned up automatically.

Procedure CleanUp

 

   This.lReady = .F.

  

   If This.lDoneCleanup

      Return

   Endif

 

   This.DoCleanup()

  

   Local laMember[1], lnMember, loReference

   For lnMember = 1 to AMembers(laMember,This,2)

      loReference = GetPem(This,laMember[m.lnMember])

      Assert This.IsValidReference(m.loReference) ;

         MESSAGE "loReference invalid"

      If PemStatus(m.loReference,"Cleanup",5)

         loReference.Cleanup()

      Endif

      loReference = NULL

   Endfor

  

   This.ValidateReferences()

   This.DoNullify()

   Assert This.ValidateReferences()==0 MESSAGE ;

      "There are still references"

  

   If VarType(Version(4))=="C" and Version(4) >= "08.00"

      UnbindEvents(This)

   Endif

  

   This.lDoneCleanup = .T.

 

EndProc

Once you called CleanUp the object isn't available anymore. The lReady property is set back to .F. IsReady() returns .F. from now on, triggering any ASSERT that you implemented to test references. Cleaning up happens in three steps. The first step is calling DoCleanup. That's the right place for code that shuts down an object. If the object opened a SQL connection, you can close it here. If the object has been registered with a toolbar, you can now deregister.

The next step is cleaning up all contained objects by calling there CleanUp method, if they have one. The final step consists in removing all object reference in the object that point to other objects. That's the purpose of the DoNullify method. In sub classes you put there for every object property code like this:

ASSERT VARTYPE(This.oReference)=="O"

This.oReference = NULL

An ASSERT is only useful for such references that should contain objects at that time. You use it to detect if another object released itself earlier than expected. In this case the property would be NULL and VARTYPE() would return "X".

Before and after releasing object references they are validated. The purpose of this validation is basically to detect class references. The second call to ValidateReferences also detects properties that you forgot to set to NULL in DoNullify. In Visual FoxPro 8 and later we also use UNBINDEVENTS() to avoid potential dangling references. The Release method isn't the only one to release objects. In container classes such as forms, toolbars or containers there's also the RemoveObject method. If you remove objects this way they also have to be cleaned up before you can remote them. The following code in RemoveObject takes care of that:

Procedure RemoveObject

LParameter tcName

 

   Assert This.IsReady() MESSAGE "THIS not ready"

 

   Assert Vartype(m.tcName)=="C" MESSAGE "wrong type"

   Assert PemStatus(This,m.tcName,5) MESSAGE "wrong parameter"

 

   Local loReference

   loReference = GetPem(This,m.tcName)

   Assert This.IsValidReference(m.loReference) ;

      MESSAGE "loReference invalid"

   If PemStatus(m.loReference,"Cleanup",5)

      loReference.Cleanup()

   Endif

   loReference = NULL

  

   DoDefault(m.tcName)

   NoDefault

  

EndProc

As usual this code checks the object reference of the object that is about to be removed. There are actual bugs in Visual FoxPro that exchange objects with their class object all of the sudden. This way you don't avoid these issues, but you detect them as early as possible. If you didn't call the CleanUp method before you released an object, you get a warning in the Destroy event

Procedure Destroy

 

   Assert not This.lDoneDestroy MESSAGE "Destroy called twice"

 

   Assert not Empty(This.cID) MESSAGE "missing ID"

   StrToFile( ;

      Ttoc(Datetime())+[,]+This.cID+[,"DESTROY","]+;

      Sys(1272,This)+["]+Chr(13)+Chr(10), ;

      Addbs(Home())+"ReferenceTracking.Txt", ;

      .T. ;

   )

  

   Local lcCommand, laStack[1], lnStack

   If VarType(Version(4))=="C" and Version(4) >= "07.00"

      lnStack = AStackInfo(laStack)

      Assert m.lnStack>1 MESSAGE "dangling reference"

      lcCommand = Left(Upper(GetWordNum(;

         laStack[m.lnStack-1,6],1)),4)

      Assert not InList(m.lcCommand,"CLEA","QUIT","CANC") ;

         MESSAGE "dangling reference"

   Endif

  

   If not This.lDoneCleanup

      Debugout Sys(1272,This)+": Release missing"

      This.Cleanup()

   Endif

   This.lDoneDestroy = .T.

 

EndProc

Destroy contains the counterpart of the registration in the Init event. Once the Destroy event has been called, we can expect that the object has actually been released. A corresponding line is written to the log file. Visual FoxPro since version 7.0 offers a particularly nice way of checking for dangling references. With ASTACKINFO() you can determine which command line lead to the Destroy event. If that’s a CLEAR ALL, QUIT or CANCEL you haven't properly released the object before. These commands trigger the internal cleanup code in Visual FoxPro that deletes all objects from memory. It's quite common that you get a general protection fault in exactly this situation.

Following the line "Better late than never", the Destroy event attempts to call the CleanUp method if you haven't called it before the object got destroyed. Sometimes this works, sometimes it's too late, and most times you never know. Objects should be released by calling the Release method. There might be cases in which this is not possible. For this reason, you only get a DEBUGOUT warning instead of an ASSERT dialog here.