VBA: Workaround To Emulate AddressOf Operator In A Class Module
You can use some assembly language to break limitations of vb, of course, the pros and cons of which are up to you. I'm just a porter. There's a function GetClassProcAddress:
Private Function GetClassProcAddress(ByVal SinceCount As Long) As Long Dim i As Long, jmpAddress As Long CopyMemory i, ByVal ObjPtr(Me), 4 ' get vtable CopyMemory i, ByVal i + (SinceCount - 1) * 4 + &H1C, 4 ' CopyMemory jmpAddress, ByVal i + 1, 4 ' The function address obtained is actually a table, a jump table GetClassProcAddress = i + jmpAddress + 5 ' Calculate jump relative offset to get the actual addressEnd Function
Parameter SinceCount
: From the top function or attribute of a class module, which function is it?
When the function being searched is a public function, its value is the number of functions calculated from the top, such as a public function WndProc written at the top of the class module, then pass 1 if it is the second public function or property, then pass 2 in turn... Note that when calculating, the public property should also be calculated.
When the function being searched is a local function, that is to say, if it is a Private modified function, the parameter value is the number of all public functions + the index of this private function. Also calculated from the top, including attributes as well.
Unfortunately, I would say that we could not use it directly. Some parameters will be added to the function after compiling, like vTable pointer. So we need to construct a small function -> class function.
Private Function GetClassProcAddr(obj As Object, ByVal Index As Long, _ Optional ByVal ParamCount As Long = 4, Optional ByVal HasReturnValue As Boolean) As Long Static lReturn As Long, pReturn As Long Static AsmCode(50) As Byte Dim i As Long, pThis As Long, pVtbl As Long, pFunc As Long pThis = ObjPtr(obj) CopyMemory pVtbl, ByVal pThis, 4 CopyMemory pFunc, ByVal pVtbl + (6 + Index) * 4, 4 pReturn = VarPtr(lReturn) For i = 0 To UBound(AsmCode) 'fill nop AsmCode(i) = &H90 Next AsmCode(0) = &H55 'push ebp AsmCode(1) = &H8B: AsmCode(2) = &HEC 'mov ebp,esp AsmCode(3) = &H53 'push ebx AsmCode(4) = &H56 'push esi AsmCode(5) = &H57 'push edi If HasReturnValue Then AsmCode(6) = &HB8 'mov offset lReturn CopyMemory AsmCode(7), pReturn, 4 AsmCode(11) = &H50 'push eax End If For i = 0 To ParamCount - 1 'push dword ptr[ebp+xx] AsmCode(12 + i * 3) = &HFF AsmCode(13 + i * 3) = &H75 AsmCode(14 + i * 3) = (ParamCount - i) * 4 + 4 Next i = i * 3 + 12 AsmCode(i) = &HB9 'mov ecx,this CopyMemory AsmCode(i + 1), pThis, 4 AsmCode(i + 5) = &H51 'push ecx AsmCode(i + 6) = &HE8 'call relative address CopyMemory AsmCode(i + 7), pFunc - VarPtr(AsmCode(i + 6)) - 5, 4 If HasReturnValue Then AsmCode(i + 11) = &HB8 'mov eax,offset lReturn CopyMemory AsmCode(i + 12), pReturn, 4 AsmCode(i + 16) = &H8B 'mov eax,dword ptr[eax] AsmCode(i + 17) = &H0 End If AsmCode(i + 18) = &H5F 'pop edi AsmCode(i + 19) = &H5E 'pop esi AsmCode(i + 20) = &H5B 'pop ebx AsmCode(i + 21) = &H8B: AsmCode(i + 22) = &HE5 'mov esp,ebp AsmCode(i + 23) = &H5D 'pop ebp AsmCode(i + 24) = &HC3 'ret GetClassProcAddr = VarPtr(AsmCode(0))End Function
Code Reference from: https://blog.csdn.net/lyserver/article/details/4224676
The usual way to solve the class module AddressOf
problem in VB6/VBA is to put the actual callback in a regular module and have it dispatch the call to the correct recipient.
E.g. for subclassing, the recipient can be looked up by hWnd
. E.g. for a timer that is not associated with a window, it can be looked up by idEvent
which the system will correctly generate for you if you pass zeroes to SetTimer
like you did.
In a standard module:
Option Explicit#If VBA7 ThenPrivate Declare PtrSafe Function SetTimer Lib "user32" _ (ByVal HWnd As LongPtr, ByVal nIDEvent As LongPtr, _ ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtrPrivate Declare PtrSafe Function KillTimer Lib "user32" _ (ByVal HWnd As LongPtr, byval uIDEvent As LongPtr) As Long#ElsePrivate Declare Function SetTimer Lib "user32" _ (ByVal HWnd As Long, ByVal nIDEvent As Long, _ ByVal uElapse As Long, ByVal lpTimerFunc As Long) As LongPrivate Declare Function KillTimer Lib "user32" _ (ByVal HWnd As Long, ByVal uIDEvent As Long) As Long#End IfPrivate mLookupByTimerId As CollectionPrivate mLookupByHandler As CollectionPublic Sub StartTimerForHandler(ByVal Handler As ITimer, ByVal DurationInMs As Long) If Handler Is Nothing Then Err.Raise 5, , "Handler must be provided" If mLookupByTimerId Is Nothing Then Set mLookupByTimerId = New Collection If mLookupByHandler Is Nothing Then Set mLookupByHandler = New Collection #If VBA7 Then Dim h As LongPtr #Else Dim h As Long #End If h = SetTimer(0, 0, DurationInMs, AddressOf TimerProc) If h = 0 Then Err.Raise 5, , "An error creating the timer" Else mLookupByTimerId.Add Handler, Str(h) mLookupByHandler.Add h, Str(ObjPtr(Handler)) End IfEnd SubPublic Sub KillTimerForHandler(ByVal Handler As ITimer) #If VBA7 Then Dim h As LongPtr #Else Dim h As Long #End If Dim key As String key = Str(ObjPtr(Handler)) h = mLookupByHandler(key) mLookupByHandler.Remove key mLookupByTimerId.Remove Str(h) KillTimer 0, hEnd Sub#If VBA7 ThenPrivate Sub TimerProc(ByVal HWnd As LongPtr, ByVal uMsg As Long, ByVal idEvent As LongPtr, ByVal dwTime As Long)#ElsePrivate Sub TimerProc(ByVal HWnd As Long, ByVal uMsg As Long, ByVal idEvent As Long, ByVal dwTime As Long)#End If Dim h As ITimer Set h = mLookupByTimerId(Str(idEvent)) h.TimerProc dwTimeEnd Sub
In a class named ITimer
:
Option ExplicitPublic Sub TimerProc(ByVal dwTime As Long)End Sub
The idea is that any class can then implement ITimer
and pass itself to StartTimerForHandler
. E.g. in a different class named DebugPrinter
:
Option ExplicitImplements ITimerPublic Sub StartNagging() Module1.StartTimerForHandler Me, 1000End SubPublic Sub StopNagging() Module1.KillTimerForHandler MeEnd SubPrivate Sub ITimer_TimerProc(ByVal dwTime As Long) Debug.Print dwTimeEnd Sub
And then somewhere else:
Option ExplicitPrivate Naggers(1 To 5) As DebugPrinterSub StartMassiveNagging() Dim i As Long For i = LBound(Naggers) To UBound(Naggers) Set Naggers(i) = New DebugPrinter Naggers(i).StartNagging NextEnd Sub