如何处理VBA中的DLL错误?

How can I handle DLL errors in VBA?

提问人:Greedo 提问时间:5/21/2019 最后编辑:Ryan WildryGreedo 更新时间:5/28/2019 访问量:1844

问:

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

当为参数提供不存在的函数指针时,将导致 Excel 崩溃。lpPrevWndFunc

同样地

Private Declare Sub RtlMoveMemory Lib "kernel32" (ByRef Destination As LongPtr, _
                                                  ByRef Source As LongPtr, _
                                                  ByVal Length As Long)

当或不存在时不快乐。DestinationSource

我认为这些错误是内存访问冲突。我假设 Windows 告诉调用者它正在做一些它无法做的事情1 - 也许它向 Excel 发送了一条消息并且没有处理程序?MSDN对此有这样的说法:

调用 Windows 动态链接库 (DLL) 期间出现系统错误,或者 Macintosh 代码资源不会引发异常,也无法被捕获 使用 Visual Basic 错误捕获。调用 DLL 函数时,你 应检查每个返回值是否成功或失败(根据 API 规范),如果发生故障,请检查 值。LastDLLError 总是 在 Macintosh 上返回零。(强调我自己的)

但是在这些情况下,我没有值来检查错误,我只是崩溃了。

1:如果它捕获了可能并不总是的错误,如果说内存重写有效但未定义。但是,在执行之前,肯定应该捕获写入受限内存或调用虚假指针,对吗?

我最感兴趣的是:

  1. 是什么导致了这种崩溃(它是如何触发的,以及它背后的机制是什么 - Excel 如何知道它需要崩溃?通过什么消息通道传达这些错误,我可以用VBA代码拦截它们吗?

  2. 是否可以主动(即清理输入等)或追溯(处理错误)防止崩溃。

我认为 (1) 可能会阐明 (2),反之亦然


无论如何,如果有人知道如何在不发生 Excel 崩溃的情况下处理此类 API 错误,或者如何避免它们发生,或者任何会很棒的事情。 似乎不起作用......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

赋予动机

我问这个问题有两个主要原因:

  1. 每次我犯错时,当 Excel 崩溃时,开发代码(调试过程等)变得更加困难。这不是通过简单地自己把它弄好(并向客户端代码公开一个不同的接口,它使用我现有的正确 API 调用实现)来解决的问题,因为我很少第一次就把它做对!

  2. 我想创建能够处理用户输入错误(例如无效的函数指针或内存写入位置)的健壮代码。这在一定程度上可以通过将函数指针抽象为可调用的类来处理,但这不是其他类型的 dll 错误的通用解决方案(并且仍然不处理 1)。


具体来说,我正在尝试开发一个友好的界面来包装 WinAPI 计时器。这些需要向它们注册回调函数,这些回调函数(鉴于 VBA 的限制)必须以函数指针的形式出现(使用关键字生成)。LongAddressOf

回调来自用户代码,可能无效。我的包装的全部意义在于提高 API 调用的稳定性,这是一个需要改进的领域。

内存复制问题可能超出了这个问题的范围,它与在 VBA 中制作生成器有关,但我认为同样的错误处理技术也适用于那里,它提供了一个更简单的例子。

我还从 Timer API 中收到错误和崩溃,为 Excel 生成了太多未经处理的消息。我再次想知道,Windows如何告诉Excel“现在是时候崩溃了”,为什么我不能拦截该指令并自己处理错误(即杀死我制作的所有计时器并刷新消息队列)?

Excel VBA WinAPI 错误处理 内存访问

评论

5赞 Joe 5/21/2019
how to avoid them happening:避免这种情况的唯一真正方法是传递有效的指针。访问冲突与 Win32API 调用返回的系统错误不同。
2赞 David Heffernan 5/21/2019
完全。你的问题不在于你需要弄清楚在错误发生后如何处理它们。首先,您需要修复代码中导致错误的缺陷。修复缺陷后,不会发生错误。
5赞 David Heffernan 5/22/2019
我没有误会。问题是你的期望是不可行的。您无法检查是否可以安全地将任意地址作为 传递。您必须要求提供函数指针的人正确地执行此操作。同样,您不能期望接收任意地址并覆盖该地址的内存。再一次,提供指针的人有责任确保它有效。lpPrevWndFunc
4赞 Remy Lebeau 5/22/2019
@Greedo简短的回答是,没有可靠的方法来清理原始指针。访问冲突是通过结构化异常 (SEH) 报告的(请参见如何在 Visual Basic.net 或 Visual Basic 2005 中使用结构化异常处理),但 VBA 不直接支持 SEH 和 IIRC。为什么一开始就允许外部代码向你发送原始指针?这里的用例是什么?
2赞 Greedo 5/22/2019
@DavidHeffernan坦率地说,这可能是大部分的答案。虽然我仍然不完全相信,但我理解为什么“当坏事发生时”,我的代码对此无能为力?当然,如果我进行了错误的 API 调用,则必须让 Excel/我的代码知道它很糟糕,然后 Excel 会在未处理的异常(可能是可观察/可处理)时崩溃。或者崩溃机制可能不同(例如,Excel 挂起等待从未到来的 API 返回,然后 Windows 终止进程,因为它没有响应 - 我认为更难观察/捕获)。我真的不知道。

答:

10赞 Joe 5/22/2019 #1

来自评论:

当然,如果我进行了错误的 API 调用,则必须让 Excel/我的代码知道它很糟糕

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

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

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

从您的评论:

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

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

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

例:

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 调用,并通过接口安全地调用客户端代码。

评论

2赞 Greedo 6/1/2019
只是为了澄清为什么我发布了赏金而不是接受这个答案:虽然这确实提供了一个很好的解决方法(我的意思是,我也+1d,这肯定会是下一步),它只为我的问题的特定方面提供了解决方案,并绕过了我最感兴趣的方面(考虑到我只是在设置赏金时澄清了范围,这很公平, 发布此答案)。但正如我所说,这并没有解决使用 API 的开发方面,这可能是我现在最关心的工作方式,也不适用于其他 DLL 错误。
4赞 S Meaden 5/27/2019 #2

其中一些 Windows API 调用可能很危险。如果要将 Windows API 功能作为库功能提供,则最好不要让客户端面临此类危险。因此,最好实现自己的接口层。

下面是将 Windows 计时器 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