简体   繁体   English

如何在VBA中处理DLL错误?

[英]How can I handle DLL errors in VBA?

The API declaration: API声明:

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. 当为lpPrevWndFunc参数提供不存在的函数指针时,将崩溃Excel。

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. DestinationSource不存在时不满意。

I think these errors are memory access violations. 我认为这些错误是内存访问违规。 I assume Windows tells the caller that it's doing something it can't 1 - perhaps it sends a message to Excel and there's no handler for it? 我假设Windows告诉调用者它正在做的事情它不能1 - 也许它向Excel发送消息并且它没有处理程序? MSDN has this to say about it: MSDN对此有这样的说法:

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. 调用Windows动态链接库(DLL)或Macintosh代码资源期间的系统错误不会引发异常,也不会使用Visual Basic错误捕获捕获。 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. 调用DLL函数时,应检查每个返回值的成功或失败 (根据API规范),如果发生故障,请检查Err对象的LastDLLError属性中的值。 LastDLLError always returns zero on the Macintosh. LastDLLError始终在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. 1:如果它捕获了错误,它可能并不总是,如果说内存重写有效但未定义。 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?). 导致崩溃的原因( 它是如何被触发的 ,以及它背后的机制到底是什么 - Excel如何知道它需要崩溃?)。 What is the message channel over which these errors are being communicated, and can I intercept them with VBA code? 传递这些错误的消息通道是什么,我可以用VBA代码拦截它们吗?

  2. Whether the crash can be pro-actively (ie sanitising inputs etc.) or retro-actively (handling errors) prevented. 是否可以主动(即消毒输入等)或反复主动(处理错误)防止崩溃。

I think (1) will likely shed light on (2) and vice-versa 我认为(1)可能会阐明(2),反之亦然


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. 无论如何,如果有人知道如何在没有Excel崩溃的情况下处理像这样的API错误,或者如何避免它们发生,或者任何可能发生的事情。 On Error Resume Next doesn't seem to work... On Error Resume Next似乎不起作用......

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. 当每次出错时Excel崩溃时,开发代码(调试过程等)会变得更加困难。 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! 这不是可以通过简单地自己解决(并将不同的接口暴露给客户端代码,使用我现有正确的API调用实现)来解决的,因为我很少第一次就做对了!

  2. I would like to create robust code which is able to handle errors in user input (eg 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.) 这可以通过例如将函数指针抽象为可调用类来解决,但这不是其他类型的DLL错误的通用解决方案(并且仍然不处理1)。


Specifically, I'm trying to develop a friendly interface to wrap WinAPI timers . 具体来说,我正在尝试开发一个友好的界面来包装WinAPI计时器 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). 这些需要在它们中注册回调函数,这些函数(鉴于VBA的限制)必须以Long函数指针的形式出现(使用AddressOf关键字生成)。

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. 我的包装的重点是提高API调用的稳定性,这是一个需要改进的领域。

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. 内存复制问题可能超出了这个问题的范围,它与在VBA中生成生成器有关,但我认为相同的错误处理技术也适用于那里,并且它提供了一个更简单的示例。

I also get errors and crashes from the Timer API generating too many unhandled messages for Excel. 我也从Timer API中获取错误和崩溃,为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 (ie Kill all the Timers I made & flush the message queue)? 我再次想知道,Windows如何告诉Excel“现在崩溃的时间”,为什么我不能拦截该指令并自己处理错误(即杀死我制作的所有计时器并刷新消息队列)?

From a comment: 来自评论:

Surely if I make an erroneous API call, something's gotta let Excel/my code know that it was bad 当然,如果我进行了错误的API调用,那么我必须让Excel /我的代码知道它很糟糕

Not necessarily. 不必要。 If you ask an API function (eg RtlMoveMemory ) to overwrite memory at a location for which you supply a pointer, it will cheerfully attempt to do so. 如果您要求API函数(例如RtlMoveMemory )覆盖您提供指针的位置的内存,它将高兴地尝试这样做。 Then a number of things could happen: 然后会发生很多事情:

  • If the memory is not writable (eg 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. 如果您的客户端是VBA,那么定义接口的简单方法是使用一个或多个空方法创建公共VBA类模块。 By convention, you should name this class with an I (for interface) prefix - eg IMyCallback . 按照惯例,您应该使用I (用于接口)前缀命名此类 - 例如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. 或者,如果客户端使用VBA以外的语言,则可以使用IDL定义接口,将其编译为类型库,并从VBA项目中引用类型库。 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, eg by creating a class module ClientCallback : 然后您的客户端应该创建一个类(VBA类模块),以他们选择的任何方式实现此接口,例如通过创建类模块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. 然后,您公开一个类型为IMyCallback的参数,您的客户端可以传递其类的实例。

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. 然后,您可以实现从Timer调用的自己的回调函数,并通过该接口安全地调用客户端代码。

Some of these Windows API calls can be dangerous. 其中一些Windows API调用可能很危险。 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. 如果您希望将Windows API功能作为库功能发布,那么不要让客户面临此类危险是有礼貌的。 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. 下面是将Windows Timer API作为库特性提供的代码,可以安全使用,因为它传递回调代码的字符串名称而不是指针。

This code was first published on my blog . 此代码首次发布在我的博客上 Also on that blog post I discuss alternatives to Application.Run if you want options. 同样在该博客文章中,如果您需要选项,我将讨论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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM