[VB6] IEnumVARIANT / For Each support without a typelib
In my own projects I use a typelib and a custom interface to do the same thing, (comparable to .NET and Olaf's examples) which might seem overly complex, so here's an example that gets the job done without any dependencies. It also serves as a good example of creating a Lightweight COM Object that's less complex than Curland's examples (which are always over-complicated). It should be easy enough to adapt to your own custom collections.
Code:
'
' MEnumerator.bas
'
' Implementation of IEnumVARIANT to support For Each in VB6
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Option Explicit
Private Type TENUMERATOR
VTablePtr As Long
References As Long
Enumerable As Object
Index As Long
Upper As Long
Lower As Long
End Type
Private Enum API
NULL_ = 0
S_OK = 0
S_FALSE = 1
E_NOTIMPL = &H80004001
E_NOINTERFACE = &H80004002
E_POINTER = &H80004003
#If False Then
Dim NULL_, S_OK, S_FALSE, E_NOTIMPL, E_NOINTERFACE, E_POINTER
#End If
End Enum
Private Declare Function FncPtr Lib "msvbvm60" Alias "VarPtr" (ByVal Address As Long) As Long
Private Declare Function GetMem4 Lib "msvbvm60" (Src As Any, Dst As Any) As Long
Private Declare Function CopyBytesZero Lib "msvbvm60" Alias "__vbaCopyBytesZero" (ByVal Length As Long, Dst As Any, Src As Any) As Long
Private Declare Function CoTaskMemAlloc Lib "ole32" (ByVal cb As Long) As Long
Private Declare Sub CoTaskMemFree Lib "ole32" (ByVal pv As Long)
Private Declare Function IIDFromString Lib "ole32" (ByVal lpsz As Long, ByVal lpiid As Long) As Long
Private Declare Function SysAllocStringByteLen Lib "oleaut32" (ByVal psz As Long, ByVal cblen As Long) As Long
Private Declare Function VariantCopyToPtr Lib "oleaut32" Alias "VariantCopy" (ByVal pvargDest As Long, ByRef pvargSrc As Variant) As Long
Private Declare Function InterlockedIncrement Lib "kernel32" (ByRef Addend As Long) As Long
Private Declare Function InterlockedDecrement Lib "kernel32" (ByRef Addend As Long) As Long
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Public Function NewEnumerator(ByRef Enumerable As Object, _
ByVal Upper As Long, _
Optional ByVal Lower As Long _
) As IEnumVARIANT
' Class Factory
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Static VTable(6) As Long
If VTable(0) = NULL_ Then
' Setup the COM object's virtual table
VTable(0) = FncPtr(AddressOf IUnknown_QueryInterface)
VTable(1) = FncPtr(AddressOf IUnknown_AddRef)
VTable(2) = FncPtr(AddressOf IUnknown_Release)
VTable(3) = FncPtr(AddressOf IEnumVARIANT_Next)
VTable(4) = FncPtr(AddressOf IEnumVARIANT_Skip)
VTable(5) = FncPtr(AddressOf IEnumVARIANT_Reset)
VTable(6) = FncPtr(AddressOf IEnumVARIANT_Clone)
End If
Dim This As TENUMERATOR
With This
' Setup the COM object
.VTablePtr = VarPtr(VTable(0))
.References = 1
Set .Enumerable = Enumerable
.Lower = Lower
.Index = Lower
.Upper = Upper
End With
' Allocate a spot for it on the heap
Dim pThis As Long
pThis = CoTaskMemAlloc(LenB(This))
If pThis Then
' CopyBytesZero is used to zero out the original
' .Enumerable reference, so that VB doesn't mess up the
' reference count, and free our enumerator out from under us
CopyBytesZero LenB(This), ByVal pThis, This
DeRef(VarPtr(NewEnumerator)) = pThis
End If
End Function
Private Function RefToIID$(ByVal riid As Long)
' copies an IID referenced into a binary string
Const IID_CB As Long = 16& ' GUID/IID size in bytes
DeRef(VarPtr(RefToIID)) = SysAllocStringByteLen(riid, IID_CB)
End Function
Private Function StrToIID$(ByRef iid As String)
' converts a string to an IID
StrToIID = RefToIID$(NULL_)
IIDFromString StrPtr(iid), StrPtr(StrToIID)
End Function
Private Function IID_IUnknown() As String
Static iid As String
If StrPtr(iid) = NULL_ Then _
iid = StrToIID$("{00000000-0000-0000-C000-000000000046}")
IID_IUnknown = iid
End Function
Private Function IID_IEnumVARIANT() As String
Static iid As String
If StrPtr(iid) = NULL_ Then _
iid = StrToIID$("{00020404-0000-0000-C000-000000000046}")
IID_IEnumVARIANT = iid
End Function
Private Function IUnknown_QueryInterface(ByRef This As TENUMERATOR, _
ByVal riid As Long, _
ByVal ppvObject As Long _
) As Long
If ppvObject = NULL_ Then
IUnknown_QueryInterface = E_POINTER
Exit Function
End If
Select Case RefToIID$(riid)
Case IID_IUnknown, IID_IEnumVARIANT
DeRef(ppvObject) = VarPtr(This)
IUnknown_AddRef This
IUnknown_QueryInterface = S_OK
Case Else
IUnknown_QueryInterface = E_NOINTERFACE
End Select
End Function
Private Function IUnknown_AddRef(ByRef This As TENUMERATOR) As Long
IUnknown_AddRef = InterlockedIncrement(This.References)
End Function
Private Function IUnknown_Release(ByRef This As TENUMERATOR) As Long
IUnknown_Release = InterlockedDecrement(This.References)
If IUnknown_Release = 0& Then
Set This.Enumerable = Nothing
CoTaskMemFree VarPtr(This)
End If
End Function
Private Function IEnumVARIANT_Next(ByRef This As TENUMERATOR, _
ByVal celt As Long, _
ByVal rgVar As Long, _
ByRef pceltFetched As Long _
) As Long
Const VARIANT_CB As Long = 16 ' VARIANT size in bytes
If rgVar = NULL_ Then
IEnumVARIANT_Next = E_POINTER
Exit Function
End If
Dim Fetched As Long
With This
Do Until .Index > .Upper
VariantCopyToPtr rgVar, .Enumerable(.Index)
.Index = .Index + 1&
Fetched = Fetched + 1&
If Fetched = celt Then Exit Do
rgVar = PtrAdd(rgVar, VARIANT_CB)
Loop
End With
If VarPtr(pceltFetched) Then pceltFetched = Fetched
If Fetched < celt Then IEnumVARIANT_Next = S_FALSE
End Function
Private Function IEnumVARIANT_Skip(ByRef This As TENUMERATOR, ByVal celt As Long) As Long
IEnumVARIANT_Skip = E_NOTIMPL
End Function
Private Function IEnumVARIANT_Reset(ByRef This As TENUMERATOR) As Long
IEnumVARIANT_Reset = E_NOTIMPL
End Function
Private Function IEnumVARIANT_Clone(ByRef This As TENUMERATOR, ByVal ppEnum As Long) As Long
IEnumVARIANT_Clone = E_NOTIMPL
End Function
Private Function PtrAdd(ByVal Pointer As Long, ByVal Offset As Long) As Long
Const SIGN_BIT As Long = &H80000000
PtrAdd = (Pointer Xor SIGN_BIT) + Offset Xor SIGN_BIT
End Function
Private Property Let DeRef(ByVal Address As Long, ByVal Value As Long)
GetMem4 Value, ByVal Address
End Property
Last edited by DEXWERX; Jul 20th, 2018 at 02:32 PM.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Originally Posted by VBClassic04
Can it be modified to handle strings?
Sorry, i'm not an author of this thread, but what do you mean of "handle strings"? If your code is about to enumerate string characters - than of course it can be used.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Hi DEXWERX.
I found a crash of VB IDE when trying to put breakpoint on Print I line in form. Then drag your V variable to watch window and press Stop (end program execution).
Last edited by hwoarang; Nov 18th, 2017 at 02:02 AM.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Halting execution in the IDE with the stop button doesn't give things a chance to tear-down properly. The enumerator is completely managed within the program and not by VB. It requires a call to the reference release method in the MEnumerator module. That module is most likely already removed from memory, so if the enumerator is attempting to release itself it doesn't have a valid memory location to call. Or if the enumerator just ends up being leaked memory maybe the IDE fails with that. Either way it's not a good outcome.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
That crash happens only when ending up a program before last item is enumerated. I assume that memory is not valid when releasing (did no research yet). Anyway - I do not think that it is critical case for now, since enumeration itself works perfectly. Possible crash is a rare case when you stop execution in the middle of process.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Originally Posted by hwoarang
Hi DEXWERX.
I found a crash of VB IDE when trying to put breakpoint on Print I line in form. Then drag your V variable to watch window and press Stop (end program execution).
As killian pointed out, it's definitely a teardown issue, similar to subclassing. If I have time later this week I will try and track down the exact point it crashes. Even when you hit stop - the IDE seems to allow code in static modules to continue executing (even after being reset), so there may be an easy fix.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Originally Posted by DEXWERX
As killian pointed out, it's definitely a teardown issue, similar to subclassing. If I have time later this week I will try and track down the exact point it crashes. Even when you hit stop - the IDE seems to allow code in static modules to continue executing (even after being reset), so there may be an easy fix.
Hopefully I noticed that by accident when debuging FTypes project. But first issue there was that IEnumVARIANT_Skip, IEnumVARIANT_Reset and IEnumVARIANT_Clone were not included into methods table (so no reference exist) like from your sample of minimal implementation. Including that methods with return of not implemented flag allows IDE Watch feature to work, otherwise - crash right after you added a variable and try to look at content.
So next issue (as i already noticed - not critical) - is stop inside of foreach loop.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Originally Posted by hwoarang
Hopefully I noticed that by accident when debuging FTypes project. But first issue there was that IEnumVARIANT_Skip, IEnumVARIANT_Reset and IEnumVARIANT_Clone were not included into methods table (so no reference exist) like from your sample of minimal implementation. Including that methods with return of not implemented flag allows IDE Watch feature to work, otherwise - crash right after you added a variable and try to look at content.
So next issue (as i already noticed - not critical) - is stop inside of foreach loop.
ah good info! the minimal implementation took a few shortcuts too many, to be viable for the IDE.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Hi, DEXWERX.
I found a bug and fixed it. The issue is in IEnumVARIANT_Next method. And this issues is referenced to a case when you loop some collection and do remove an element inside iteration.
Code:
Private Function IEnumVARIANT_Next(ByRef This As TENUMERATOR, ByVal lCelt As Long, ByVal lVar As Long, ByVal lFetched As Long) As Long
'------------------------------------------------------------------------------------------------------------------------------------------'
'
' NOTES: Requires enumerable class to have Count and Item properties.
'
'------------------------------------------------------------------------------------------------------------------------------------------'
Dim c As Long
With This
c = .uEnumerable.Count - 1&
If .lIndex > .lUpper Then
IEnumVARIANT_Next = 1&
Else
If c < .lUpper Then
.lIndex = .lIndex - 1&
.lUpper = c
End If
VariantCopy lVar, .uEnumerable.Item(.lIndex)
.lIndex = .lIndex + 1&
End If
End With
End Function
You can see that new version compares initial Count passed in lUpper structure member with actual Count. If second is less - we decrement lIndex and update lUpper to correctly proceed next iteration (and exit loop if needed).
Of course uEnumerable is now mandatory to have Count property. I do not think it is a problem.
I also have in mind a case when somebody removes several items at once inside single iteration ...
All above seems to be true only for lightweight version
Regards
Last edited by hwoarang; Apr 24th, 2018 at 12:44 PM.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Originally Posted by hwoarang
Hi, DEXWERX.
I found a bug and fixed it. The issue is in IEnumVARIANT_Next method. And this issues is referenced to a case when you loop some collection and do remove an element inside iteration.
You can see that new version compares initial Count passed in lUpper structure member with actual Count. If second is less - we decrement lIndex and update lUpper to correctly proceed next iteration (and exit loop if needed).
Of course uEnumerable is now mandatory to have Count property. I do not think it is a problem.
I also have in mind a case when somebody removes several items at once inside single iteration ...
All above seems to be true only for lightweight version
Regards
Cool but I wouldn't call it a bug. This is normal for enumerators (any language/platform)
Removing/adding during enumeration is undefined behavior, and should never be done.
The best thing would be to throw an exception if iterating to the next item after a list is modified.
It makes sense when you realize enumeration can be used over any list type.
Typically you can clone a list, and enumerate over the clone if this is desired, but still avoiding iterating over a modified list.
An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and the next call to MoveNext or Reset throws an InvalidOperationException.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Yes, I'm aware of that. The reason of standard iterators do not recheck bounds actually is thread safety.
I do not think that modifying a collection inside a loop should never be done because sometimes it is pretty painfull to determine item(s) to be removed.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
what it the difference between skills this thread indicate and the below
Code:
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Attribute NewEnum.VB_MemberFlags = "40"
Set NewEnum = mCol.[_NewEnum]
End Property
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Originally Posted by loquat
what it the difference between skills this thread indicate and the below
Code:
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Attribute NewEnum.VB_MemberFlags = "40"
Set NewEnum = mCol.[_NewEnum]
End Property
The difference is that in your sample you need to declare a private Collection variable inside your custom class to use enumeration supported by this collection out of the box. Thus you need to store all your elements inside this collection for futher enumerating. Sometimes this can be pretty hard overload that affects performance and maintanability.
DEXWERX opposite provides an implementation of Enumerable interface that can be used by your custom class (that is supposed to support enumeration) without embedded Collection variable.
Last edited by hwoarang; May 1st, 2018 at 11:03 AM.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
@DEXWERX
Code:
Private Type TENUMERATOR
VTablePtr As Long
References As Long
Enumerable As Object
Index As Long
Upper As Long
Lower As Long
End Type
Where did you find info about the ENUMERATOR Object ? I have looked in the MS documentation for the IEnumVARIANT Interface but I couldn't find any info about the enumerator object.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Originally Posted by JAAFAR
@DEXWERX
Code:
Private Type TENUMERATOR
VTablePtr As Long
References As Long
Enumerable As Object
Index As Long
Upper As Long
Lower As Long
End Type
Where did you find info about the ENUMERATOR Object ? I have looked in the MS documentation for the IEnumVARIANT Interface but I couldn't find any info about the enumerator object.
Thanks for the great code.
That's the layout of the internal variables the object has. It's not restricted. Though it is recommended to have the RefCount variable right after VTablePtr. Seems like a common thing in VBx objects. However, it's not a rule or so.
Re: [VB6] IEnumVARIANT / For Each support without a typelib
Originally Posted by JAAFAR
@DEXWERX
Code:
Private Type TENUMERATOR
VTablePtr As Long
References As Long
Enumerable As Object
Index As Long
Upper As Long
Lower As Long
End Type
Where did you find info about the ENUMERATOR Object ? I have looked in the MS documentation for the IEnumVARIANT Interface but I couldn't find any info about the enumerator object.
This struct is entirely user-defined internal object state with only requirement for the first member to be a pointer to interface VTable to be considered (a pointer to) a valid COM interface.
This is exactly like having
Private m_lReferenced As Long
Private m_oEnumberable As Object
Private m_lIndex As Long
. . . and so on private variables inside an ordinary VB6 class.
These member variables consitute the internal object state, the order of variables does not matter for clients, the type of the variable too, its all an implementation detail that remains encapsulated/hidden from outside.
Struct TENUMERATOR does *not* match any system-supplied structure, it's an implementation detail that OP of this thread decided to use as it's most convenient for his original pupose -- implementing an IEnumVARIANT forwarding proxy.