Declaring Variables Memory Leaks - variables

I am wondering what would be the most correct way to deal with memory when using VBScript. Should declare all variables right before I use them? The beginning of the program? I understand global vs local, however in my script all variables are local. I know that memory leaks will never be a problem when writing in VBScript 99.9% of the time, but I am also curious as to the 'best' way to clear and release memory within a script. By 'best' I mean, the timing of clearing variables/objects (right after you are done using them vs the end of the script), etc.
An example:
Dim fso: Set fso = CreateObject("Scripting.FileSystemObject")
Dim arrList : Set arrList = CreateObject("System.Collections.ArrayList")
Dim objDict: Set objDic = CreateObject( "Scripting.Dictionary")
Dim objEmail : Set objEmail = CreateObject("CDO.Message")
Dim someArray(), x, y, z, item

It's best practice to declare all variables, but not for the reason you assume. VBScript is sufficiently good at cleaning up after itself, so memory leaks usually aren't an issue. Most of the time you don't even need to release objects (Set var = Nothing) because they're automatically destroyed when leaving the context.
The reason why you still want to declare your variables is that you want to use Option Explicit in your scripts (which enforces variable declarations), so that you can avoid problems due to mistyped or otherwise uninitialized variables. Without Option Explicit VBScript would automagically create missing variables and initialize them with an empty/zero value. Silly example:
Dim foo : foo = 3
Dim bar : bar = 1
Do
bar = bar + fo 'mistyped variable, initilized as empty/0
Loop Until bar > 10
WScript.Echo bar
Running the above would create an infinite loop. If you add Option Explicit the script will instead immediately terminate with a runtime error:
C:\path\to\your.vbs(5, 3) Microsoft VBScript runtime error: Variable is undefined: 'fo'

The VBScript garbage collector runs at the end of every line to clear implicit variables and at the end of every procedure (end sub, end function, and end property) to clear explicit variables. Objects are similar but have added constraints. It works similar to VBA's garbage collector. By contrast JScript waits until 30,000 objects have gone out of scope before running and freeing memory.
An implicit variable is an unnamed variable - msgbox LCase(UCase("String") has two implicit variables - the result of UCase("String") and that is passed to LCase(implicitVar1) which returns implicitVar2 which is passed to Msgbox. An Explict variable is declared either by DIM or just by using it as in A=5 which creates an explicit variable called A.
VBScript, on the other hand, has a much simpler stack-based garbage collector. Scavengers are added to a stack when they come into scope, removed when they go ou t of scope, and any time an object is discarded it is immediately freed.
https://blogs.msdn.microsoft.com/ericlippert/2003/09/17/how-do-the-script-garbage-collectors-work/
VBScript’s garbage collector is completely different. It runs at the end of every statement and procedure, and does not do a search of all memory. Rather, it keeps track of everything allocated in the statement or procedure; if anything has gone out of scope, it frees it immediately
https://blogs.msdn.microsoft.com/ericlippert/2004/12/22/t4-vbscript-and-the-terminator/
Also
https://blogs.msdn.microsoft.com/ericlippert/2004/04/28/when-are-you-required-to-set-objects-to-nothing/
https://blogs.msdn.microsoft.com/ericlippert/2004/03/01/syntax-semantics-micronesian-cults-and-novice-programmers/
The CPU is a stack based machine (and VBScript a stack based virtual machine). When the CPU calls a function the calling program puts the parameters on the stack and the return address, adjust the stack frame and does a jump. The callee function creates local variables on the stack and also places the return value on it. When it returns the stack pointer is adjusted back to where it was which automatically frees all the above.

Related

Can I safely check when a COM object has been released in VBA?

I've been reviewing some code for creating weak-references in VBA by manually dereferencing object pointers without calling IUnknown::AddRef, and I've found a bug that I can't explain. I could come up with a minimal reproducible example using the pure API calls, but I think it's easier just to demonstrate using the WeakReference class from that review. Here's how I can crash Excel:
Dim strongRef As Range
Set strongRef = [A1]
Dim weakRef As New WeakReference
weakRef.Object = strongRef
Debug.Assert strongRef.address = weakRef.Object.address 'fine
Set strongRef = Nothing
Debug.Assert weakRef.Object Is Nothing 'fine if step through, crashes if F5
This is buggy behaviour; the WeakReference class is designed in such a way that once the parent reference is destroyed, the weak reference should return Nothing rather than attempt blindly to dereference the parent ObjPtr which would now be pointing to an invalid object instance. The way it does this is explained in detail in the linked question, but essentially it caches the parent object's VTable pointer, then uses this to check the VTable pointer is still valid before every dereference. Basically the class relies on the fact that when the parent object goes out of scope, its memory is reclaimed and so the VTable pointer is overwritten with something else.
That should stop this kind of bug. However it doesn't, and I'm wondering why...
It was my understanding that
Set strongRef = Nothing
Calls IUnknown::Release
This sets the ref count to zero, the object goes out of scope
The object is responsible for releasing its own instance memory, so it uses the this pointer (first arg to IUnknown::Release) to zero the instance memory (including the VTable pointer) and free it for use by the VBA memory allocator again
Finally the value at VarPtr(strongRef) is set to zero to indicate it is a null object reference
However I think the bug is happening because the instance memory is not reset as soon as the reference count hits zero, so perhaps VBA's implementation of IUnknown::Release marks the memory as "dirty" to be cleared up at a later date by an asynchronous garbage collector? I'm just guessing here. The thing is, if I step through the code line by line then it works fine, or if you hold the WeakReference in a child class then it works fine (see the examples in the linked post).
UPDATE
I just tried, with a custom VBA class for strongRef, e.g.
Class1
Option Explicit
Static Property Get address() As Double
Dim value As Double
If value = 0 Then value = [RandBetween(1,1e10)]
address = value
End Property
...then I don't get a crash! So it's definitely something to do with specific implementations of IUnknown::Release and is probably why the author of that code never noticed the bug.

What better for memory usage: define constants in subs or in module

I understand that is some of VB basics, but can't figure out how to ask google:
How VBA manage constants? In some languages compiler replace them by values. But how that works in VBA? Will it matter if i declare them in sub/function, not in global space. Especially, if a sub/function is called many times in runtime.
I'm often declare function name and some other strings as constants in sub/function space - it's easier for me to read my code.
For example: SUB_NAME in Get_AppExcel_Ref() in code below, to use it for logging error events.
If the Get_AppExcel_Ref() will be called couple times while programm running - SUB_NAME will be allocated in memory once, on first run, or on every call? Or maybe there is some kind perfomance issue and better declare SUB_NAME as global.
Private Sub Get_AppExcel_Ref(ByRef appObject As Object)
Const SUB_NAME As String = "Get_AppExcel_Ref"
On Error GoTo ERR_NOT_OPENNED
Set appObject = GetObject(, "Excel.Application")
Exit Sub
ERR_NOT_OPENNED:
If Err.Number = 429 Then
Err.Clear
Set appObject = CreateObject("Excel.Application")
Else
Call LOG.printLog("open Excel", Err, SUB_NAME)
End If
End Sub
'LOG - user class type variable, printLog params: Description, Error, Source Name
Declaring your Const as global will make no sense, as it would display the same string, no matter where you would use it.
You could declare it as a global variable (for example to save the extra parameter to you logging routine) and assign the name of the routine, but you would have the name of you routine as a (constant) string in your code also (so same amount of memory used). And at the end, it will completely mess up your logic, because when calling a subroutine, the content will be overwritten and when after that call an error occurs, your log will show the wrong routine name. So don't go that path.
As Paul Ogilivie writes in his comments, think about a constant as a read-only variable - and don't waste any thoughts about the exaxt implementation (but I think it is save to assume that the string is put only once in memory). You have more than enough memory available for your VBA code, and string handling is so fast that you will never experience any runtime issues.
My credo: Use everyhing as it fits your needs as programmer best - and readability is an important aspect to this. Don't care too much about memory consumption or runtime speed - except if you really hit problems. And if you do, these are most likely caused by other things.
Firs of all, welcome to SO.
I think the VBA compiler does also replace constants with their values, as with other languages. So they are not the same as variables.
I don't think a constant is necessary here. I only tend to use them for parameters, not just to replace any string.
Your code would be just fine like this:
LOG.printLog "open Excel", Err, "GetAppExcelRef"

Access an application and instantiate an object

I am trying to create an instance of a specific class called ExtraScreen from a referenced library application called EXTRA. How can I use the SendKeys function from the ExtraScreen class?
So far I tried this:
Dim software As EXTRA.ExtraScreen
software.SendKeys ("a")
The result is Error:
Object variable or With block variable not set.
You have to also Set it to something:
Dim software As EXTRA.ExtraScreen
Set software = CreateObject("EXTRA.ExtraScreen")
or
Dim software As New EXTRA.ExtraScreen
You have declared an object variable with a specific, early-bound type - which means if the code can compile & run, then the project has a reference to the type library.
Dim software As EXTRA.ExtraScreen
Dim statements aren't executable: you can't put a breakpoint on a Dim statement. All they do is allocate a spot of a given size in memory. In this case, it reserves a spot wide enough to hold an object reference - and nothing else.
On execution, the first statement to execute is this:
software.SendKeys ("a")
But the problem is, if you put a breakpoint here and inspect the Locals toolwindow, you'll find that the software object contains Nothing. In other languages this is known as a "null reference" - the object variable is not set: there's a reserved spot for holding an object reference, but the spot is empty.
You use the New keyword to create an instance of a class - i.e. to create an object. And in VBA object reference assignments require the Set keyword:
Set software = New EXTRA.ExtraScreen
Now if you run that line and inspect your locals, you'll find that software isn't Nothing anymore, and you can inspect its state / properties.
Once an object variable holds a proper object reference, you can legally invoke its members:
software.SendKeys "a"
You can never invoke anything on a Nothing object reference: an object variable that is Nothing is, well, nothing: it's not an object, therefore it has no members to invoke. VBA runtime responds by throwing run-time error 91 "Object (or With block variable) not Set".
The "With block variable" part is referring to the With keyword, which can also hold objects. For example you could do this:
With software
.SendKeys "a"
End With
And you'd get the exact same error if software isn't Set. Consider dropping the local variable altogether, if it only ever needs to live as a local variable inside some specific procedure:
With New EXTRA.ExtraScreen
.SendKeys "a"
End With
In this case the With block is holding the object reference; at End With, the object is gone (avoid jumping in & out of With blocks, specifically for that reason).

Unexpected OutOfMemoryException in ILNumerics

The following VB .net code gives me an out of memory exception. Does anybody knows why?
Dim vArray As ILArray(Of Double) = ILMath.rand(10000000)
Using ILScope.Enter(vArray)
For i As Integer = 1 To 100
vArray = ILMath.add(vArray, vArray)
Next
End Using
Thank you very much.
In this toy example you can simply remove the artificial scope and it will run fine:
Dim vArray As ILArray(Of Double) = ILMath.rand(10000000)
For i As Integer = 1 To 100
vArray = ILMath.add(vArray, vArray)
Next
Console.WriteLine("OK: " + vArray(0).ToString())
Console.ReadKey()
However, in a more serious situation, ILScope will be your friend. As stated on the ILNumerics page an artificial scope ensures a deterministic memory management:
All arrays created inside the scope are disposed once the block was
left.
Otherwise one had to rely on the GC for cleanup. And, as you know, this involves a gen 2 collection for large objects – with all disadvantages in terms of performance.
In order to be able to dispose the arrays they need to be collected and tracked somehow. Whether or not this qualifies for the term 'memory leak' is rather a philosophical question. I will not go into it here. The deal is: after the instruction pointer runs out of the scope these arrays are taken care of: their memory is put into the memory pool and will be reused. As a consequence, no GC will be triggered.
The scheme is especially useful for long running operations and for large data. Currently, the arrays are released only AFTER the scope block was left. So if you create an algorithm/ loop which requires more memory than available on your machine you need to clean up DURING the loop already:
Dim vArray As ILArray(Of Double) = ILMath.rand(10000000)
For i As Integer = 1 To 100
Using ILScope.Enter
vArray.a = ILMath.add(vArray, vArray)
' ...
End Using
Next
Here, the scope cleans up the memory after each iteration of the loop. This affects all local arrays assigned within the loop body. If we want an array value to survive the loop iteration we can assign to its .a property as shown with vArray.a.

When should an Excel VBA variable be killed or set to Nothing?

I've been teaching myself Excel VBA over the last two years, and I have the idea that it is sometimes appropriate to dispose of variables at the end of a code segment. For example, I've seen it done in this bit adapted from Ron de Bruin's code for transferring Excel to HTML:
Function SaveContentToHTML (Rng as Range)
Dim FileForHTMLStorage As Object
Dim TextStreamOfHTML As Object
Dim TemporaryFileLocation As String
Dim TemporaryWorkbook As Workbook
...
TemporaryWorkbook.Close savechanges:=False
Kill TemporaryFileLocation
Set TextStreamOfHTML = Nothing
Set FileForHTMLStorage = Nothing
Set TemporaryWorkbook = Nothing
End Function
I've done some searching on this and found very little beyond how to do it, and in one forum post a statement that no local variables need to be cleared, since they cease to exist at End Sub. I'm guessing, based on the code above, that may not be true at End Function, or in other circumstances I haven't encountered.
So my question boils down to this:
Is there somewhere on the web that explains the when and why for variable cleanup, and I just have not found it?
And if not can someone here please explain...
When is variable cleanup for Excel VBA necessary and when it is not?
And more specifically... Are there specific variable uses (public variables?
Function-defined variables?) that remain loaded in memory for longer
than subs do, and therefor could cause trouble if I don't clean
up after myself?
VB6/VBA uses deterministic approach to destoying objects. Each object stores number of references to itself. When the number reaches zero, the object is destroyed.
Object variables are guaranteed to be cleaned (set to Nothing) when they go out of scope, this decrements the reference counters in their respective objects. No manual action required.
There are only two cases when you want an explicit cleanup:
When you want an object to be destroyed before its variable goes out of scope (e.g., your procedure is going to take long time to execute, and the object holds a resource, so you want to destroy the object as soon as possible to release the resource).
When you have a circular reference between two or more objects.
If objectA stores a references to objectB, and objectB stores a reference to objectA, the two objects will never get destroyed unless you brake the chain by explicitly setting objectA.ReferenceToB = Nothing or objectB.ReferenceToA = Nothing.
The code snippet you show is wrong. No manual cleanup is required. It is even harmful to do a manual cleanup, as it gives you a false sense of more correct code.
If you have a variable at a class level, it will be cleaned/destroyed when the class instance is destructed. You can destroy it earlier if you want (see item 1.).
If you have a variable at a module level, it will be cleaned/destroyed when your program exits (or, in case of VBA, when the VBA project is reset). You can destroy it earlier if you want (see item 1.).
Access level of a variable (public vs. private) does not affect its life time.
VBA uses a garbage collector which is implemented by reference counting.
There can be multiple references to a given object (for example, Dim aw = ActiveWorkbook creates a new reference to Active Workbook), so the garbage collector only cleans up an object when it is clear that there are no other references. Setting to Nothing is an explicit way of decrementing the reference count. The count is implicitly decremented when you exit scope.
Strictly speaking, in modern Excel versions (2010+) setting to Nothing isn't necessary, but there were issues with older versions of Excel (for which the workaround was to explicitly set)
I have at least one situation where the data is not automatically cleaned up, which would eventually lead to "Out of Memory" errors.
In a UserForm I had:
Public mainPicture As StdPicture
...
mainPicture = LoadPicture(PAGE_FILE)
When UserForm was destroyed (after Unload Me) the memory allocated for the data loaded in the mainPicture was not being de-allocated. I had to add an explicit
mainPicture = Nothing
in the terminate event.