The API declaration:
Private Declare Function CallWindowProc Lib "user32.dll" Alias "CallWindowProcA" ( _
ByVal lpPrevWndFunc As Long, _
ByVal HWnd As Long, _
ByVal msg As Long, _
ByVal wParam As Long, _
ByVal lParam As Long) As Long
will crash Excel when provided with a non-existent function pointer for the lpPrevWndFunc
parameter.
Similarly,
Private Declare Sub RtlMoveMemory Lib "kernel32" (ByRef Destination As LongPtr, _
ByRef Source As LongPtr, _
ByVal Length As Long)
isn't happy when Destination
or Source
don't exist.
I think these errors are memory access violations. I assume Windows tells the caller that it's doing something it can't1 - perhaps it sends a message to Excel and there's no handler for it? MSDN has this to say about it:
System errors during calls to Windows dynamic-link libraries (DLL) or Macintosh code resources do not raise exceptions and cannot be trapped with Visual Basic error trapping. When calling DLL functions, you should check each return value for success or failure (according to the API specifications), and in the event of a failure, check the value in the Err object's LastDLLError property. LastDLLError always returns zero on the Macintosh. (emphasis my own)
But in these instances I have no values to check for errors, I just get a crash.
1: Provided it catches the error which it might not always, if say a memory rewrite is valid but undefined. But certainly writing to restricted memory or calling fake pointers should be caught before they're executed right?
What causes this crash (both how it is triggered, and what exactly the mechanism is behind it - how does Excel know it needs to be crashing?). What is the message channel over which these errors are being communicated, and can I intercept them with VBA code?
Whether the crash can be pro-actively (i.e. sanitising inputs etc.) or retro-actively (handling errors) prevented.
I think (1) will likely shed light on (2) and vice-versa
Anyway, if anyone knows how to handle API errors like these without Excel crashing, or how to avoid them happening, or anything that would be fab. On Error Resume Next
doesn't seem to work...
Sub CrashExcel()
On Error Resume Next 'Lord preserve us
'Copy 300 bytes from one non existent memory pointer to another
RtlMoveMemory ByVal 100, ByVal 200, 300
On Error Goto 0
Debug.Assert Err.LastDllError = 0 'Yay no errors
End Sub
There are 2 main reasons I'm asking about this:
Developing code (the process of debugging etc.) is made much harder when Excel crashes every time I make a mistake. This is not something that can be solved by simply getting it right myself (and exposing a different interface to client code, which uses my existing correct implementations of API calls) because I rarely get it right first time!
I would like to create robust code which is able to handle errors in user input (e.g. invalid function pointers or memory write locations). This can be dealt with to an extent by, for example, abstracting function pointers away into callable classes, but that's not a general solution for other kinds of dll errors (and still doesn't deal with 1.)
Specifically, I'm trying to develop a friendly interface to wrap WinAPI timers. These require callback functions to be registered with them, which (given the limitations of VBA) have to come in the form of Long
function pointers (generated with the AddressOf
keyword).
The callbacks come from user code and may be invalid. The whole point of my wrapping is to improve stability of the API calls, and this is one area that needs improvement.
The memory copy problem is probably out of scope of this question, it's to do with making generators in VBA, but I think the same error handling techniques would be applicable there too, and it makes for an easier example.
I also get errors and crashes from the Timer API generating too many unhandled messages for Excel. Once again I wonder, how does Windows tell Excel "Time to crash now", why can't I intercept that instruction and deal with the error myself (i.e. Kill all the Timers I made & flush the message queue)?
Close the Excel file opened(not the excel window) - from the lowest X on the right corner. Open the macro using the Ribbon and then option Open. Get into the dev mode(ALT+F11) and go to Tools->References then unselect the broken Excel library(the one with MISSING:) Select the correct library from the list and click OK.
You can access DLL functions and commands in VBA by using the Declare statement. This statement has one syntax for commands and one for functions. The optional Public and Private keywords specify the scope of the imported function: the entire Visual Basic project or just the Visual Basic module, respectively.
DLLs (dynamic-link libraries) are portable libraries that can be created by one application and used by another. In particular one can create a DLL in C then have its functions run from Excel by calling them from VBA.
Some of these Windows API calls can be dangerous. If you want to ship Windows API functionality as a library feature then it would be courteous not to expose you clients to such danger. So, you are best to implement your own interface layer.
Below is code that ships the Windows Timer API as a library feature that is safe for use because it passes string names of callback code instead of pointers.
This code was first published on my blog. Also on that blog post I discuss alternatives to Application.Run if you want options.
Option Explicit
Option Private Module
'* Brought to you by the Excel Development Platform blog
'* First published at https://exceldevelopmentplatform.blogspot.com/2019/05/vba-make-windows-timer-as-library.html
Private Declare Function ApiSetTimer Lib "user32.dll" Alias "SetTimer" (ByVal hWnd As Long, ByVal nIDEvent As Long, _
ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Private Declare Function ApiKillTimer Lib "user32.dll" Alias "KillTimer" (ByVal hWnd As Long, ByVal nIDEvent As Long) As Long
Private mdicCallbacks As New Scripting.Dictionary
Private Sub SetTimer(ByVal sUserCallback As String, lMilliseconds As Long)
Dim retval As Long ' return value
Dim lUniqueId As Long
lUniqueId = mdicCallbacks.HashVal(sUserCallback) 'should be unique enough
mdicCallbacks.Add lUniqueId, sUserCallback
retval = ApiSetTimer(Application.hWnd, lUniqueId, lMilliseconds, AddressOf TimerProc)
End Sub
Private Sub TimerProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal idEvent As Long, _
ByVal dwTime As Long)
ApiKillTimer Application.hWnd, idEvent
Dim sUserCallback As String
sUserCallback = mdicCallbacks.Item(idEvent)
mdicCallbacks.Remove idEvent
Application.Run sUserCallback
End Sub
'****************************************************************************************************************************************
' User code below
'****************************************************************************************************************************************
Private Sub TestSetTimer()
SetTimer "UserCallBack", 500
End Sub
Private Function UserCallBack()
Debug.Print "hello from UserCallBack"
End Function
From a comment:
Surely if I make an erroneous API call, something's gotta let Excel/my code know that it was bad
Not necessarily. If you ask an API function (e.g. RtlMoveMemory
) to overwrite memory at a location for which you supply a pointer, it will cheerfully attempt to do so. Then a number of things could happen:
If the memory is not writable (e.g. code), then you will be lucky enough to get an access violation which will terminate the process before it can do any more damage.
If the memory happens writable, it will be overwritten and thus corrupted, after which all bets are off.
From your comment:
I'm designing code to attach user supplied callback functions
An alternative would be to design an interface with methods that your client code can implement. Then require the client to pass an instance of a class that implements that interface.
If your clients are VBA, then an easy way to define an interface is to create a public VBA class module with one or more empty methods. By convention, you should name this class with an I
(for interface) prefix - e.g. IMyCallback
. The empty method(s) (Subs or Functions) can have any signature you want, but I'll keep it simple:
Example:
Class module name: IMyCallback
Option Explicit
Public Sub MyMethod()
End Sub
Alternatively, and better if your clients use languages other than VBA, you can use IDL to define the interface, compile it into a type library, and reference the type library from your VBA project. I won't go into that any further here but ask another question if you want to follow it up.
Then your clients should create a class (VBA class module) that implements this interface in whatever way they choose, e.g. by creating a class module ClientCallback
:
Class module name: ClientCallback
Option Explicit
Implements IMyCallback
Private Sub IMyCallback_MyMethod()
' Client adds his implementation here
End Sub
You then expose an argument of type IMyCallback
and your client can pass an instance of his class.
Your method:
Public Sub RegisterCallback(Callback as IMyCallback)
...
End Sub
Client code:
Dim objCallback as New ClientCallback
RegisterCallback Callback
…
You can then implement your own callback function that is called from a Timer, and safely call the client code via the interface.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With