Let's build a transpiler! Part 22
This is the twenty-second post in a series of building a transpiler.You can find the previous ones here.
Last time I said we would delve deep into IEnumVARIANT interface.
Every time you For Each something,
For Each Month In Months
(...)
Next
VB compiles it with something (that may looks) like this.:
Dim Iterator As IEnumVARIANT
Dim Done As Long
Rem Months must implement IEnumVARIANT interface.
Rem Iterator has its reference count incremented in the call to [NewEnum].
Set Iterator = Months.NewEnum()
Do
If IsObject(Month) Then Set Month = Nothing Else Month = Empty
Rem The call to [Next] method below requests one element.
Rem Month is passed ByRef so it can be assigned with the next element in sequence.
Rem Done is also passed ByRef.
Iterator.Next 1, Month, Done
If Done = 1 Then
Rem Iterator's reference count is decremented.
Set Iterator = Nothing
Exit Do
End If
(...)
Loop
So, one is left to think that in order to For Each any VB6 class, we'll just have to slap an Implements IEnumVARIANT and implement its methods.
After all, there are only four of them: Clone, Next, Reset, and Skip.
Unfortunatelly, it is not that simple. IEnumVARIANT uses types that are not available to VB (unsigned longs and pointers). Its methods are not COM ones - they do not return HRESULTs -, so VB has no way to deal with them. It also has a method called Next, that's a VB keyword. So, no luck.
You can resort to external typelibs or use a Collection and delegate to its NewEnum method.
Some time ago I thought that I should try it the hard way. After a lot of trial and error (and some crashes) I managed to have something that is re-usable.
Also, it is really enlightning to run the code step-by-step inside VB's IDE.
It is comprised of two files, VariantEnumerator.cls and VariantEnumeratorHome.bas.
I won't explain them as they have pretty advanced stuff, but note that, having them in your VB project, you can make your class For Eachable.
First, VariantEnumerator.cls:
Option Explicit
Private Declare Function HeapAlloc Lib "kernel32" (ByVal hHeap As LongPtr, ByVal dwFlags As Long, ByVal dwBytes As Long) As LongPtr
Public Event NextItem(ByVal Qty As Long, ByRef Items As Variant, ByRef Returned As Long, ByRef Data As Variant)
Public Event Skip(ByVal Qty As Long, ByRef Data As Variant)
Public Event Reset(ByRef Data As Variant)
Public Event Clone(ByRef Obj As Variant, ByRef Data As Variant)
Public Function NewEnum(ByVal ParentObj As Object) As IUnknown
Dim Ptr As LongPtr
Dim Obj As IEnumVariantType
IncRefCount ParentObj
Ptr = HeapAlloc(GetProcessHeap, dwFlags:=0, dwBytes:=Len(Obj))
With Obj
.VTable = Ptr + 4
.QueryInterface = GetProc(AddressOf QueryInterfaceEntry)
.AddRef = GetProc(AddressOf AddRefEntry)
.Release = GetProc(AddressOf ReleaseEntry)
.NextItem = GetProc(AddressOf NextEntry)
.Skip = GetProc(AddressOf SkipEntry)
.Reset = GetProc(AddressOf ResetEntry)
.Clone = GetProc(AddressOf CloneEntry)
.Count = 1
.Ptr = Ptr
.Ref = ObjPtr(Me)
.Parent = ObjPtr(ParentObj)
End With
Rem Copy structure to the allocated memory
CopyMemory Destination:=ByVal Ptr, Source:=VarPtr(Obj), Length:=Len(Obj)
Rem Return pointer as an IUnknown.
CopyMemory NewEnum, Source:=VarPtr(Ptr), Length:=Len(Ptr)
End Function
Friend Sub OnNextItem(ByVal Qty As Long, ByRef Items As Variant, ByRef Returned As Long, ByRef Data As Variant)
RaiseEvent NextItem(Qty, Items, Returned, Data)
End Sub
Friend Sub OnSkip(ByVal Qty As Long, ByRef Data As Variant)
RaiseEvent Skip(Qty, Data)
End Sub
Friend Sub OnReset(ByRef Data As Variant)
RaiseEvent Reset(Data)
End Sub
Friend Sub OnClone(ByRef Obj As Variant, ByRef Data As Variant)
RaiseEvent Clone(Obj, Data)
End Sub
Private Function GetProc(ByRef Proc As LongPtr) As LongPtr
GetProc = Proc
End Function
Private Sub IncRefCount(ByRef Obj As Object)
Dim Dummy As Object
Dim Nil As LongPtr
Set Dummy = Obj
CopyMemory Destination:=Dummy, Source:=VarPtr(Nil), Length:=Len(Nil)
End Sub
Second, VariantEnumeratorHome.bas:
Option Explicit
Option Private Module
Private Declare Function HeapFree Lib "kernel32" ( _
ByVal hHeap As LongPtr, _
ByVal dwFlags As Long, _
ByRef lpMem As LongPtr _
) As Long
Public Declare Function GetProcessHeap Lib "kernel32" () As LongPtr
Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
ByRef Destination As Any, _
ByVal Source As LongPtr, _
ByVal Length As Long _
)
Public Type IEnumVariantType
VTable As LongPtr 'Address of the "virtual table" below.
QueryInterface As LongPtr 'Interface IUnknown.
AddRef As LongPtr 'Interface IUnknown.
Release As LongPtr 'Interface IUnknown.
NextItem As LongPtr 'Interface IEnumVARIANT.
Skip As LongPtr 'Interface IEnumVARIANT.
Reset As LongPtr 'Interface IEnumVARIANT.
Clone As LongPtr 'Interface IEnumVARIANT.
Count As Long 'Reference counter.
Ptr As LongPtr 'Pointer to this structure's allocated memory.
Ref As LongPtr 'Reference to VariantEnumerator.
Data As Variant 'Container to user's data.
Parent As LongPtr 'Reference to object being iterated.
End Type
Public Function QueryInterfaceEntry(ByRef This As IEnumVariantType, ByVal iid As Long, ByRef ppvObject As Long) As Long
Rem Increment reference count.
This.Count = This.Count + 1
Rem Return pointer to IEnumVariantType structure.
ppvObject = VarPtr(This)
End Function
Public Function AddRefEntry(ByRef This As IEnumVariantType) As Long
Rem Increment reference count.
This.Count = This.Count + 1
Rem Return it.
AddRefEntry = This.Count
End Function
Public Function ReleaseEntry(ByRef This As IEnumVariantType) As Long
Rem Decrement reference count.
This.Count = This.Count - 1
Rem Return it.
ReleaseEntry = This.Count
Rem If there's no more references, deallocates IEnumVariantType's memory.
If This.Count = 0 Then
DecRefCount This.Parent
HeapFree GetProcessHeap, 0, This.Ptr
End If
End Function
Public Function NextEntry( _
ByRef This As IEnumVariantType, _
ByVal celt As Long, _
ByRef rgvar As Variant, _
ByVal pceltFetched As Long _
) As Long
If celt = 0 Then celt = 1
GetEnumerator(This.Ref).OnNextItem celt, rgvar, pceltFetched, This.Data
Rem If qunatity of returned items is lower than what has been asked, iteration is over.
If pceltFetched < celt Then NextEntry = 1
End Function
Public Function SkipEntry(ByRef This As IEnumVariantType, ByVal celt As Long) As Long
GetEnumerator(This.Ref).OnSkip celt, This.Data
End Function
Public Function ResetEntry(ByRef This As IEnumVariantType) As Long
GetEnumerator(This.Ref).OnReset This.Data
End Function
Public Function CloneEntry(ByRef This As IEnumVariantType, ByRef ppEnum As IEnumVARIANT) As Long
GetEnumerator(This.Ref).OnClone ppEnum, This.Data
End Function
Private Function GetEnumerator(ByRef Ptr As LongPtr) As VariantEnumerator
Dim Obj As VariantEnumerator
Dim Res As VariantEnumerator
Dim Nil As LongPtr
Rem Copy pointer to a temporary object.
CopyMemory Destination:=Obj, Source:=VarPtr(Ptr), Length:=Len(Ptr)
Rem Get the legal object.
Set Res = Obj
Rem Free the ilegal object.
CopyMemory Destination:=Obj, Source:=VarPtr(Nil), Length:=Len(Nil)
Rem Return the "rehydrated" object.
Set GetEnumerator = Res
End Function
Private Sub DecRefCount(ByRef Ptr As LongPtr)
Dim Dummy As Object
CopyMemory Destination:=ObjPtr(Dummy), Source:=Ptr, Length:=Len(Ptr)
End Sub
We'll see an example of using it next week.
Andrej Biasic
2020-12-23