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 propert2024 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.