12

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

4

2 回答 2

10

来自评论:

当然,如果我进行了错误的 API 调用,一定要让 Excel/我的代码知道这很糟糕

不必要。如果您要求 API 函数(例如RtlMoveMemory)覆盖您为其提供指针的位置的内存,它会很高兴地尝试这样做。然后可能会发生很多事情:

  • 如果内存不可写(例如代码),那么您很幸运会遇到访问冲突,这将在进程造成更多损害之前终止进程。

  • 如果内存发生可写,它将被覆盖并因此损坏,之后所有赌注都将关闭。

从您的评论中:

我正在设计代码以附加用户提供的回调函数

另一种方法是设计一个带有您的客户端代码可以实现的方法的接口。然后要求客户端传递一个实现该接口的类的实例。

如果您的客户是 VBA,那么定义接口的一种简单方法是使用一个或多个空方法创建一个公共 VBA 类模块。I按照惯例,您应该使用(用于接口)前缀来命名此类- 例如IMyCallback。空方法(Subs 或 Functions)可以有你想要的任何签名,但我会保持简单:

例子:

Class module name: IMyCallback

Option Explicit

Public Sub MyMethod()

End Sub

或者,如果您的客户使用 VBA 以外的语言,则更好的是,您可以使用 IDL 来定义接口,将其编译为类型库,并从您的 VBA 项目中引用类型库。我不会在这里进一步讨论,但如果您想跟进,请提出另一个问题。

然后您的客户应该创建一个类(VBA 类模块),以他们选择的任何方式实现此接口,例如通过创建一个类模块ClientCallback

Class module name: ClientCallback

Option Explicit

Implements IMyCallback

Private Sub IMyCallback_MyMethod()
    ' Client adds his implementation here
End Sub

然后,您公开一个类型的参数,IMyCallback您的客户可以传递他的类的一个实例。

你的方法:

Public Sub RegisterCallback(Callback as IMyCallback)
    ...
End Sub

客户端代码:

Dim objCallback as New ClientCallback
RegisterCallback Callback
…

然后,您可以实现自己的从 Timer 调用的回调函数,并通过接口安全地调用客户端代码。

于 2019-05-21T19:02:45.707 回答
4

其中一些 Windows API 调用可能很危险。如果您想将 Windows API 功能作为库功能提供,那么最好不要让您的客户面临这种危险。所以,你最好实现自己的接口层。

下面的代码将 Windows Timer API 作为库功能提供,可以安全使用,因为它传递回调代码的字符串名称而不是指针。

此代码首次发布在我的博客上。同样在该博客文章中,如果您需要选项,我将讨论 Application.Run 的替代方案。

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
于 2019-05-27T09:39:34.650 回答