如何处理 VBA 中的 DLL 错误?

Posted

技术标签:

【中文标题】如何处理 VBA 中的 DLL 错误?【英文标题】:How can I handle DLL errors in VBA? 【发布时间】:2019-10-07 18:43:47 【问题描述】:

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

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

同样,

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

DestinationSource 不存在时不高兴。

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

调用 Windows 动态链接库 (DLL) 或 Macintosh 代码资源不会引发异常且无法被捕获 使用 Visual Basic 错误捕获。 在调用 DLL 函数时,您 应该检查每个返回值是成功还是失败(根据 API 规范),如果发生故障,请检查 Err 对象的 LastDLLError 属性中的值。 LastDLLError 总是 在 Macintosh 上返回零。 (强调我自己的)

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

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

我最感兴趣的是:

    导致此崩溃的原因(如何触发,以及其背后的机制究竟是什么 - Excel 如何知道它需要崩溃? )。传达这些错误的消息通道是什么,我可以用 VBA 代码拦截它们吗?

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

我认为 (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

动机

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

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

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

    李>

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

回调来自用户代码,可能无效。我包装的重点是提高 API 调用的稳定性,这是需要改进的一个方面。

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

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

【问题讨论】:

how to avoid them happening:避免这种情况的唯一真正方法是传递有效的指针。访问冲突与 Win32API 调用返回的系统错误不同。 没错。您的问题不在于您需要在错误发生后解决如何处理错误。您需要首先修复代码中导致错误的缺陷。一旦你修复了缺陷,错误就不会发生。 我没有误会。问题是你的期望是不可行的。您无法检查任意地址是否可以安全地作为lpPrevWndFunc 传递。您必须要求提供函数指针的人正确地执行此操作。同样,您不能期望接收任意地址并覆盖该地址处的内存。再一次,责任在于提供指针的人,以确保它是有效的。 @Greedo 简短的回答是,没有可靠的方法来清理原始指针。通过Structured Exception (SEH) 报告访问冲突(参见How to use structured exception handling in Visual Basic .NET or in Visual Basic 2005),但是VBA 不直接支持SEH、IIRC。为什么您允许外部代码向您发送原始指针?这里的用例是什么? @DavidHeffernan 坦率地说,这可能是大部分答案。尽管我仍然不完全相信我理解为什么'当坏事发生时'我的代码对此无能为力?当然,如果我进行了错误的 API 调用,必须让 Excel/我的代码知道它很糟糕,然后 Excel 在未处理的异常(可能是可观察的/可处理的)上崩溃。或者崩溃机制可能不同(例如,Excel 挂起等待从永远不会到来的 API 返回,然后 Windows 终止该进程,因为它没有响应——我认为更难观察/捕捉)。我真的不知道。 【参考方案1】:

来自评论:

如果我进行了错误的 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 调用的回调函数,并通过接口安全地调用客户端代码。

【讨论】:

只是为了澄清为什么我发布了赏金而不是接受这个答案:虽然这确实提供了一个很好的解决方法(我的意思是,我也 +1d,而且它肯定会成为下一步),它只为我的问题的特定方面提供解决方案,并绕过我最感兴趣的方面(考虑到我只是在设置赏金时澄清了范围,之后这个答案是发布)。但正如我所说,这并没有解决使用 API 的开发方面,这可能是我现在最关心的事情,也不适用于其他 DLL 错误。【参考方案2】:

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

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

此代码是first published on my blog。同样在那篇博客文章中,我讨论了 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

【讨论】:

以上是关于如何处理 VBA 中的 DLL 错误?的主要内容,如果未能解决你的问题,请参考以下文章

如何处理 VBA 中的复选框值?

如何处理缺少的 dll,如 msvcp100.dll、msvcp100.dll?

处理 Mongoose 验证错误——在哪里以及如何处理?

处理包/模块中的错误时如何处理Python异常

如何处理 C++ 加载的 C# DLL 中的异常

如果 DLL 的 app.config 应该在“主配置”中……我们如何处理 DLL 中的 WCF 引用?