8

I need make use of several Windows API functions in a Class that I am developing, for a hobby project. Several of these functions require the use of the AddressOf Operator, but as per Microsoft Documentation, its use in a Class Module is prohibited.

Does anyone know of a function or some standard method that can emulate the the AddressOf Operator, or is that even possible?

Background
The app centers around functions that are called from the worksheet, which are then used to instantiate a class and call a method using the SetTimer WinAPI function.

You might say: "Well, you could just use Application.OnTime", and you would be right, IF the function was NOT called from the worksheet. For good reason, Excel's calculation engine explicitly ignores calls to Application.OnTime, (if the caller be the worksheet); however, SetTimer happens to work regardless.

I want to avoid the clunky implementation of placing a public function in a standard module, (which would be dependent on an instance of the class), where I WOULD be able to use the AddressOf Operator, albeit in an ugly, un-encapsulated way.

Edit: As mentioned in the comments, initially, I intentionally did not disclose exactly what I was trying to do to avoid hearing "you shouldn't do that", lol. I have a working class that does exactly what I want it to do, (i.e. return arrays to the worksheet using the standard method of Ctrl+Shift+Enter), but I wanted to try and emulate the Dynamic Array Functions that are currently being beta tested by the Excel dev team, which do not require you select the range and enter an array via Ctrl+Shift+Enter. I knew if I asked something like "how can I return an array to the WorkSheet from a UDF without Ctrl+Shift+Enter", everyone would provide existing answers and/or shame me, (I would do the same if someone else asked, lol), for asking how to implement something that contradicts the way Excel's calculation engine was intended to function.

Saying that, I also have yet another version of my class that uses the QueryTable object to place data in the sheet and works much like the Dynamic Array Functions. I am probably going to post each implementation on Code Review to see how I could improve them/gain some insight to which would be the most stable implementation, etc.

Private Declare Function SetTimer Lib "user32" _
        (ByVal HWnd As Long, ByVal nIDEvent As Long, 
         ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long

Public Function Method1(varValsIn As Variant) As Variant
  
  Dim lngWindowsTimerID As Long
        
        'doing some stuff
        
        'call API function after doing some stuff
        lngWindowsTimerID = SetTimer(0&, 0&, 1, AddressOf DoStuff)

End Sub 

Private Sub DoStuff
    'Stuff to do
End Sub
rickmanalexander
  • 599
  • 1
  • 6
  • 17
  • Without more context, it is very hard to give you an answer (other than "why can't you put it in module and call it from the class?" – Sam Jun 20 '19 at 18:28
  • @Sam I don't want to put the method (which is obviously dependent on an instance of the class) in a standard module. However, I suppose providing an example couldn't hurt. – rickmanalexander Jun 20 '19 at 18:36
  • @rickmanalexander what arguments are returned to callback function from WinAPI? – omegastripes Jun 20 '19 at 18:41
  • *the calling function which is used to instantiate the class and call a method is a UDF called from the worksheet* - sounds like the root of the problem: you're abusing UDFs and making them do something they're clearly not meant to be doing. UDFs are intended to be *pure functions*, not side-effecting hacks that schedule other calls to other side-effecting hacks. This has all ingredients of a classic X-Y problem. Instead of trying to hack up a solution for not-the-real-problem Y, why not tell us more about the actual problem (X) you're trying to solve? – Mathieu Guindon Jun 20 '19 at 19:15
  • 4
    In other words: implement `stuff` in a *macro* rather than a UDF, and the "problem" *poofs* away in a glittering purple haze of magical unicorn dust. – Mathieu Guindon Jun 20 '19 at 19:22
  • @MathieuGuindon Well I won't deny that I intentionally didn't disclose exactly what I was trying to do to avoid hearing "you shouldn't do that", lol. I have a working class that does exactly what I want it to do, (returns arrays to the worksheet using the standard method of `Ctrl+Shift+Enter`), but I wanted to try and emulate the `Dynamic Array Functions` that are currently being beta tested by the Excel dev team, which don't require you select the range and enter an array via `Ctrl+Shift+Enter`. I knew if I asked "how can I modify the sheet from a UDF", everyone would say, "it can't be done". – rickmanalexander Jun 20 '19 at 19:36
  • 1
    That's very cool, DA functions are beyond awesome - and they're *natively* implemented, too... I can't think of a nice/clean way to achieve anything anywhere near that functionality in a VBA UDF.. and even if a hacked-up solution ends up "working", I can't help thinking you'll be sacrificing performance, stability, and power. Out of curiosity, which DA function(s) are you trying to emulate? – Mathieu Guindon Jun 20 '19 at 19:46
  • That said... isn't the UDF itself defined in a standard module? Is `Method1` the UDF? You could have a standard module e.g. `DynamicArrayFunctionHelper` with `Option Private Module` specified, where `DoStuff` could go. – Mathieu Guindon Jun 20 '19 at 19:51
  • 1
    It's possible but messy. See [VB6-SelfTimer-class-module](http://www.vbforums.com/showthread.php?527281-VB6-SelfTimer-class-module-2008-06-15) – Florent B. Jun 20 '19 at 20:35
  • @MathieuGuindon I was afraid you would say that. I figured as much. And I wouldn't say that I am trying to emulate a specific function per-say, I am more so trying to integrate the concept of the functions into an addin that I am building. As it stands, I pass my data to from a UDF to a class that can return 1 value, or an array of values from SQL based on data supplied in a range. My hope was that I could do so without my non excel savvy users having to pre-select a range and press `Ctrl+Shift+Enter`. Saying that, Do you think it could be done non-natively, via callbacks to a `com-addin`? – rickmanalexander Jun 20 '19 at 21:00

2 Answers2

8

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 address
End Function

Parameter SinceCount: From the top function or attribute of a class module, which function is it?

  1. 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.

  2. 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

Drake Wu
  • 6,927
  • 1
  • 7
  • 30
  • First of all, this is absolutely excellent info, truly top notch!! Secondly, how in the world did you figure this out? I am super interested in compiler internals, assemblers, and how the two interact with client side code, so if you could point me to where I could learn more of this kind of thing I would be eternally grateful. Before posting this question on SO, I was wondering if I could use `CopMemory` along with combinations of the `pointer functions`, (`VarPtr, ObjPtr,` and `StrPtr`) to achieve this, but I just didn't have the knowledge of how the addresses would be calculated. – rickmanalexander Jun 21 '19 at 11:04
  • 1
    I am not amused to be honest. The `static AsmCode()` is reused between procedure calls, the asm stub is rewritten each time the function is called for the next object, so the handlers registered for the previous objects will all be firing at the last processed object, that is, provided it has the same shape, otherwise it will just crash. – GSerg Jun 21 '19 at 13:33
  • @GSerg, `static AsmCode()` is not an issue if the procedure is placed in a class and called by `Class_Initialize`. – Florent B. Jun 21 '19 at 14:15
  • 3
    Looks like the code from this answer is copy/past from https://blog.csdn.net/lyserver/article/details/4224676 without attribution to its original author. – Florent B. Jun 21 '19 at 14:15
  • @FlorentB. It is an issue because it accepts `obj As Object` and places its `ObjPtr` in the asm stub. The asm stub will be shared between all instances of that class. – GSerg Jun 21 '19 at 14:48
  • 2
    @GSerg, nope static variable declared in classes are not shared between instances of the same class. – Florent B. Jun 21 '19 at 14:54
  • 2
    @FlorentB. ouch, nice find! – Mathieu Guindon Jun 21 '19 at 15:03
  • @GSerg I had noticed the `Static` declarations, but figured they could just be replaced with standard `Dim` statements to eliminate the cognitive load and potential issues brough by the implications of `Static`... no? – Mathieu Guindon Jun 21 '19 at 15:11
  • 2
    @MathieuGuindon With standard `Dim` statements at the class level, yes (and that would have not [confused](https://stackoverflow.com/questions/56691881/vba-workaround-to-emulate-addressof-operator-in-a-class-module/56699160?noredirect=1#comment99975117_56699160) me). Not at the procedure level though, otherwise you'd be [returning addresses of local variables](https://stackoverflow.com/a/6445794/11683). – GSerg Jun 21 '19 at 15:23
  • @FlorentB. Yes, but I cannot provide a 3rd-part link, So I metion that **I'm just a porter**, And you can get the function details by googling "GetClassProcAddr" – Drake Wu Jun 22 '19 at 12:26
  • I was looking back at this post and I started wondering: Could some variant of this (or some of the ideas in it) be used to access the `Call Stack` similar to the way *vbWatchDog* does?? – rickmanalexander Oct 04 '19 at 14:44
8

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 Then

Private Declare PtrSafe Function SetTimer Lib "user32" _
  (ByVal HWnd As LongPtr, ByVal nIDEvent As LongPtr, _
   ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr

Private Declare PtrSafe Function KillTimer Lib "user32" _
  (ByVal HWnd As LongPtr, byval uIDEvent As LongPtr) As Long

#Else

Private Declare Function SetTimer Lib "user32" _
  (ByVal HWnd As Long, ByVal nIDEvent As Long, _
   ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long

Private Declare Function KillTimer Lib "user32" _
  (ByVal HWnd As Long, ByVal uIDEvent As Long) As Long

#End If


Private mLookupByTimerId As Collection
Private mLookupByHandler As Collection

Public 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 If

End Sub

Public 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, h
End Sub

#If VBA7 Then
Private Sub TimerProc(ByVal HWnd As LongPtr, ByVal uMsg As Long, ByVal idEvent As LongPtr, ByVal dwTime As Long)
#Else
Private 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 dwTime
End Sub

In a class named ITimer:

Option Explicit

Public 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 Explicit

Implements ITimer

Public Sub StartNagging()
  Module1.StartTimerForHandler Me, 1000
End Sub

Public Sub StopNagging()
  Module1.KillTimerForHandler Me
End Sub

Private Sub ITimer_TimerProc(ByVal dwTime As Long)
  Debug.Print dwTime
End Sub

And then somewhere else:

Option Explicit

Private Naggers(1 To 5) As DebugPrinter

Sub StartMassiveNagging()
  Dim i As Long

  For i = LBound(Naggers) To UBound(Naggers)
    Set Naggers(i) = New DebugPrinter
    Naggers(i).StartNagging
  Next

End Sub
GSerg
  • 76,472
  • 17
  • 159
  • 346