Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I handle DLL errors in VBA?

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?

I'm most interested in:

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

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

Motivation

There are 2 main reasons I'm asking about this:

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

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

like image 564
Greedo Avatar asked May 21 '19 11:05

Greedo


People also ask

How do I fix error loading DLL in Excel?

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.

How do you call a DLL in Excel VBA?

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.

What is DLL in VBA?

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.


2 Answers

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
like image 113
S Meaden Avatar answered Sep 24 '22 18:09

S Meaden


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.

like image 27
Joe Avatar answered Sep 23 '22 18:09

Joe