I have found a bug in VBA a few months ago and was unable to find a decent workaround. The bug is really annoying as it kind of restricts a nice language feature.
When using a Custom Collection Class it is quite common to want to have an enumerator so that the class can be used in a For Each loop. This can be done by adding this line:
Attribute [MethodName].VB_UserMemId = -4 'The reserved DISPID_NEWENUM
immediately after the function/property signature line either by:
- Exporting the class module, editing the contents in a text editor, and then importing back
- Using Rubberduck annotation
'@Enumeratorabove the function signature and then syncronizing
Unfortunately, on x64, using the above-mentioned feature, causes the wrong memory to get written and leads to the crash of the Application in certain cases (discussed later).
Reproducing the bug
CustomCollection class:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "CustomCollection"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
Private m_coll As Collection
Private Sub Class_Initialize()
Set m_coll = New Collection
End Sub
Private Sub Class_Terminate()
Set m_coll = Nothing
End Sub
Public Sub Add(v As Variant)
m_coll.Add v
End Sub
Public Function NewEnum() As IEnumVARIANT
Attribute NewEnum.VB_UserMemId = -4
Set NewEnum = m_coll.[_NewEnum]
End Function
Code in a standard module:
Option Explicit
Sub Main()
#If Win64 Then
Dim c As New CustomCollection
c.Add 1
c.Add 2
ShowBug c
#Else
MsgBox "This bug does not occur on 32 bits!", vbInformation, "Cancelled"
#End If
End Sub
Sub ShowBug(c As CustomCollection)
Dim ptr0 As LongPtr
Dim ptr1 As LongPtr
Dim ptr2 As LongPtr
Dim ptr3 As LongPtr
Dim ptr4 As LongPtr
Dim ptr5 As LongPtr
Dim ptr6 As LongPtr
Dim ptr7 As LongPtr
Dim ptr8 As LongPtr
Dim ptr9 As LongPtr
'
Dim v As Variant
'
For Each v In c
Next v
Debug.Assert ptr0 = 0
End Sub
By running the Main method, the code will stop on the Assert line in the ShowBug method and you can see in the Locals window that local variables got their values changed out of nowhere:

where ptr1 is equal to ObjPtr(c). The more variables are used inside the NewEnum method (including Optional parameters) the more ptrs in the ShowBug method get written with a value (memory address).
Needless to say, removing the local ptr variables inside the ShowBug method would most certainly cause the crash of the Application.
When stepping through code line by line, this bug will not occur!
More on the bug
The bug is not related with the actual Collection stored inside the CustomCollection. The memory gets written immediately after the NewEnum function is invoked. So, basically doing any of the following is not helping (tested):
- adding
Optionalparameters - removing all code from within the function (see below code showing this)
- declaring as
IUnknowninstead ofIEnumVariant - instead of
Functiondeclaring asProperty Get - using keywords like
FriendorStaticin the method signature - adding the DISPID_NEWENUM to a Let or Set counterpart of the Get, or even hiding the former (i.e. make the Let/Set private).
Let us try step 2 mentioned above. If CustomCollection becomes:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "CustomCollection"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
Public Function NewEnum() As IEnumVARIANT
Attribute NewEnum.VB_UserMemId = -4
End Function
and the code used for testing is changed to:
Sub Main()
#If Win64 Then
Dim c As New CustomCollection
ShowBug c
#Else
MsgBox "This bug does not occur on 32 bits!", vbInformation, "Cancelled"
#End If
End Sub
Sub ShowBug(c As CustomCollection)
Dim ptr0 As LongPtr
Dim ptr1 As LongPtr
Dim ptr2 As LongPtr
Dim ptr3 As LongPtr
Dim ptr4 As LongPtr
Dim ptr5 As LongPtr
Dim ptr6 As LongPtr
Dim ptr7 As LongPtr
Dim ptr8 As LongPtr
Dim ptr9 As LongPtr
'
Dim v As Variant
'
On Error Resume Next
For Each v In c
Next v
On Error GoTo 0
Debug.Assert ptr0 = 0
End Sub
running Main produces the same bug.
Workaround
Reliable ways, that I found, to avoid the bug:
Call a method (basically leave the
ShowBugmethod) and come back. This needs to happen before theFor Eachline is executed (before meaning it can be anywhere in the same method, not necessarily the exact line before):Sin 0 'Or VBA.Int 1 - you get the idea For Each v In c Next vCons: Easy to forget
Do a
Setstatement. It could be on the variant used in the loop (if no other objects are used). As in point 1 above, this needs to happen before theFor Eachline is executed:Set v = Nothing For Each v In c Next vor even by setting the collection to itself with
Set c = c
Or, passing the c parameterByValto theShowBugmethod (which, as Set, does a call to IUnknown::AddRef)
Cons: Easy to forgetUsing a separate
EnumHelperclass that is the only class ever used for enumerating:VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "EnumHelper" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = False Attribute VB_Exposed = False Option Explicit Private m_enum As IEnumVARIANT Public Property Set EnumVariant(newEnum_ As IEnumVARIANT) Set m_enum = newEnum_ End Property Public Property Get EnumVariant() As IEnumVARIANT Attribute EnumVariant.VB_UserMemId = -4 Set EnumVariant = m_enum End PropertyCustomCollectionwould become:VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "CustomCollection" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = False Attribute VB_Exposed = False Option Explicit Private m_coll As Collection Private Sub Class_Initialize() Set m_coll = New Collection End Sub Private Sub Class_Terminate() Set m_coll = Nothing End Sub Public Sub Add(v As Variant) m_coll.Add v End Sub Public Function NewEnum() As EnumHelper Dim eHelper As New EnumHelper ' Set eHelper.EnumVariant = m_coll.[_NewEnum] Set NewEnum = eHelper End Functionand the calling code:
Option Explicit Sub Main() #If Win64 Then Dim c As New CustomCollection c.Add 1 c.Add 2 ShowBug c #Else MsgBox "This bug does not occur on 32 bits!", vbInformation, "Cancelled" #End If End Sub Sub ShowBug(c As CustomCollection) Dim ptr0 As LongPtr Dim ptr1 As LongPtr Dim ptr2 As LongPtr Dim ptr3 As LongPtr Dim ptr4 As LongPtr Dim ptr5 As LongPtr Dim ptr6 As LongPtr Dim ptr7 As LongPtr Dim ptr8 As LongPtr Dim ptr9 As LongPtr ' Dim v As Variant ' For Each v In c.NewEnum Debug.Print v Next v Debug.Assert ptr0 = 0 End SubObviously, the reserved DISPID was removed from the
CustomCollectionclass.Pros: forcing the
For Eachon the.NewEnumfunction instead of the custom collection directly. This avoids any crash caused by the bug.Cons: always needing the extra
EnumHelperclass. Easy to forget to add the.NewEnumin theFor Eachline (would only trigger a runtime error).
The last approach (3) works because when c.NewEnum is executed the ShowBug method is exited and then returned before the invocation of the Property Get EnumVariant inside the EnumHelper class. Basically approach (1) is the one avoiding the bug.
What is the explanation for this behavior? Can this bug be avoided in a more elegant way?
EDIT
Passing the CustomCollection ByVal is not always an option. Consider a Class1:
Option Explicit
Private m_collection As CustomCollection
Private Sub Class_Initialize()
Set m_collection = New CustomCollection
End Sub
Private Sub Class_Terminate()
Set m_collection = Nothing
End Sub
Public Sub AddElem(d As Double)
m_collection.Add d
End Sub
Public Function SumElements() As Double
Dim v As Variant
Dim s As Double
For Each v In m_collection
s = s + v
Next v
SumElements = s
End Function
And now a calling routine:
Sub ForceBug()
Dim c As Class1
Set c = New Class1
c.AddElem 2
c.AddElem 5
c.AddElem 7
Debug.Print c.SumElements 'BOOM - Application crashes
End Sub
Obviously, the example is a bit forced but it is quite common to have a "parent" object containing a Custom Collection of "child" objects and the "parent" might want to do some operation involving some or all of the "children".
In this case it would be easy to forget to do a Set statement or a method call before the For Each line.
What is happening
It appears that the stack frames are overlapping although they should not. Having enough variables in the
ShowBugmethod prevents a crash and the values of the variables (in the caller subroutine) are simply changed because the memory they refer to is also used by another stack frame (the called subroutine) that was added/pushed later at the top of the call stack.We can test this by adding a couple of
Debug.Printstatements to the same code from the question.The
CustomCollectionclass:And the calling code, in a standard .bas module:
By running

MainI get something like this in the Immediate Window:The address of the
NewEnumreturn value is clearly at a memory address in between theptr0andptr9variables of theShowBugmethod. So, that is why the variables get values out of nowhere, because they actually come from the stack frame of theNewEnummethod (like the address of the object's vtable or the address of theIEnumVariantinterface). If the variables would not be there, then the crash is obvious as more critical parts of memory are being overwritten (e.g. the frame pointer address for theShowBugmethod). As the stack frame for theNewEnummethod is larger (we can add local variables for example, to increase the size), the more memory is shared between the top stack frame and the one below in the call stack.What happens if we workaround the bug with the options described in the question? Simply adding a

Set v = Nothingbefore theFor Each v In cline, results into:Showing both previous value and the current one (bordered blue), we can see that the
NewEnumreturn is at a memory address outside of theptr0andptr9variables of theShowBugmethod. It seems that the stack frame was correctly allocated using the workaround.If we break inside the

NewEnumthe call stack looks like this:How
For EachinvokesNewEnumEvery VBA class is derived from IDispatch (which in turn is derived from IUnknown).
When a
For Each...loop is called on an object, that object'sIDispatch::Invokemethod is called with adispIDMemberequal to -4. A VBA.Collection already has such a member but for VBA custom classes we mark our own method withAttribute NewEnum.VB_UserMemId = -4so that Invoke can call our method.Invokeis not called directly if the interface used in theFor Eachline is not derived fromIDispatch. Instead,IUnknown::QueryInterfaceis called first and asked for the IDispatch interface. In this caseInvokeis obviously called only after IDispatch interface is returned. Right here is the reason why usingFor Eachon an Object declaredAs IUnknownwill not cause the bug regardless if it is passedByRefor if it is a global or class member custom collection. It simply uses workaround number 1 mentioned in the question (i.e. calls another method) although we cannot see it.Hooking Invoke
We can replace the non-VB
Invokemethod with one of our own in order to investigate further. In a standard.basmodule we need the following code to hook:and we run the
Main2method (standard .bas module) to produce the bug:Notice that more dummy ptr variables are needed to prevent the crash as the stack frame for
IDispatch_Invokeis bigger (hence, the memory overlap is bigger).By running the above, I get:

The same bug occurs although the code never reaches the
NewEnummethod due to the hooking of theInvokemethod. The stack frame is again wrongfully allocated.Again, adding a
Set v = Nothingbefore theFor Each v In cresults into:The stack frame is allocated correctly (bordered green). This indicates that the issue is not with the
NewEnummethod and also not with our replacementInvokemethod. Something is happening before ourInvokeis called.If we break inside our

IDispatch_Invokethe call stack looks like this:One last example. Consider a blank (with no code) class
Class1. If we runMain3in the following code:The bug simply does not occur. How is this different from running
Main2with our own hookedInvoke? In both casesDISP_E_MEMBERNOTFOUNDis returned and noNewEnummethod is called.Well, if we look at the previously shown call stacks side by side:

we can see that the non-VB
Invokeis not pushed on the VB stack as a separate "Non-Basic Code" entry.Apparently, the bug only occurs if a VBA method is called (either NewEnum via the original non-VB Invoke or our own IDispatch_Invoke). If a non-VB method is called (like the original IDispatch::Invoke with no following NewEnum) the bug does not occur as in
Main3above. No bug occurs when runningFor Each...on a VBA Collection within the same circumstances either.The bug cause
As all the above examples suggest, the bug can be summarized with the following:
For EachcallsIDispatch::Invokewhich in turn callsNewEnumwhile the stack pointer has not been incremented with the size of theShowBugstack frame. Hence, same memory is used by both frames (the callerShowBugand the calleeNewEnum).Workarounds
Ways to force the correct incrementation of the stack pointer:
For Eachline) e.g.Sin 1For Eachline):IUnknown::AddRefby passing the argumentByValIUnknown::QueryInterfaceby using thestdole.IUnknowninterfaceSetstatement which will call eitherAddReforReleaseor both (e.g.Set c = c). Could also callQueryInterfacedepending on the source and target interfacesAs suggested in the EDIT section of the question, we don't always have the possibility to pass the Custom Collection class
ByValbecause it could simply be a global variable, or a class member and we would need to remember to do a dummySetstatement or to call another method beforeFor Each...is executed.Solution
I still could not find a better solution that the one presented in the question, so I am just going to replicate the code here as part of the answer, with a slight tweak.
EnumHelperclass:CustomCollectionwould now become something like:You would just need to call with
For Each v in c.NewEnumAlthough, the
EnumHelperclass would be an extra class needed in any project implementing a custom collection class, there are a couple of advantages as well:Attribute [MethodName].VB_UserMemId = -4to any other custom collection class. This is even more useful for users that do not have RubberDuck installed ('@Enumeratorannotation), as they would need to export, edit the .cls text file and import back for each custom collection classItemsEnumand aKeysEnumat the same time. BothFor Each v in c.ItemsEnumandFor Each v in c.KeysEnumwould workEnumHelperclass would be called beforeInvokeis calling member ID -4For Each v in c.NewEnumand instead useFor Each v in cyou would just get a runtime error which would be picked up in testing anyway. Of course you could still force a crash by passing the result ofc.NewEnumto another methodByRefwhich would then need to execute aFor Eachbefore any other method call orSetstatement. Highly unlikely you would ever do thatEnumHelperclass for all the custom collection classes you might have in a project