如何在VBA中处理DLL错误?

12

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 会告诉调用者它在做一些无法完成的事情-可能是发送消息给 Excel,但没有处理程序吗?MSDN对此有解释

调用 Windows 动态链接库 (DLL) 或 Macintosh 代码资源时的系统错误不会导致异常,并且不能通过 Visual Basic 错误跟踪来捕获。 在调用 DLL 函数时,您应该检查每个返回值的成功或失败(根据 API 规范),并在发生故障时,检查 Err 对象的 LastDLLError 属性中的值。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 timers。这些需要使用回调函数进行注册,而(由于VBA的限制)必须采用Long函数指针的形式(使用AddressOf关键字生成)。

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

内存复制问题可能超出了这个问题的范围,这与在VBA中生成器有关,但我认为相同的错误处理技术也适用于那里,并且更容易举例说明。

我还从Timer API生成太多未处理消息而导致错误和崩溃。再一次地,我想知道,Windows如何告诉Excel“现在该崩溃了”,为什么我无法拦截该指令并自行处理错误(即杀死我做的所有定时器并刷新消息队列)?


5
如何避免它们发生:避免这种情况的唯一真正方法是传递有效的指针。访问冲突(access violation)并不等同于由Win32API调用返回的系统错误(System Error)。 - Joe
2
没错。你的问题不是需要想办法在错误发生后如何处理它们,而是需要修复代码中导致错误的缺陷。一旦你修复了这些缺陷,错误就不会再出现了。 - David Heffernan
5
我没有误解。问题在于你的期望不可行。你不能检查任意地址是否可以安全地作为“lpPrevWndFunc”传递。你必须要求提供函数指针的人正确地提供它。同样,你也不能期望接收任意地址并覆盖该地址处的内存。再次强调,责任在于提供指针的人,确保它是有效的。 - David Heffernan
4
简短回答是,没有可靠的方法来清除原始指针。访问冲突通过结构化异常 (SEH) 报告(请参阅 如何在 Visual Basic .NET 或 Visual Basic 2005 中使用结构化异常处理),但据我所知,VBA 不直接支持 SEH。为什么要允许外部代码向您发送原始指针?这里的用例是什么? - Remy Lebeau
2
@DavidHeffernan 说实话,这可能是大部分答案的内容。虽然我仍然不完全相信为什么‘出了问题’时我的代码无能为力?如果我进行错误的API调用,肯定会有某些东西让Excel/我的代码知道错误,然后Excel在未处理的异常上崩溃(可能可观察/可处理)。或者崩溃机制不同(例如,Excel挂起等待从未到来的API返回,然后Windows将其杀死,因为它无响应-更难观察/捕捉,我真的不知道。 - Greedo
显示剩余7条评论
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
…

您可以编写自己的回调函数,在计时器中调用它,并通过接口安全地调用客户端代码。


2
只是想澄清为什么我发布了一个悬赏而不是接受这个答案:虽然这确实提供了一个好的解决方法(我的意思是,我也点了+1,它肯定会是下一步),但它仅针对问题的特定方面提供了解决方案,并绕过了我最感兴趣的方面(考虑到我只在设定悬赏后才明确范围,这是可以理解的,在此之后,这个答案已经被发布了)。但正如我所说,这并没有解决与API一起工作的开发方面,这可能是我现在最关心的问题,也不能适用于其他DLL错误。 - Greedo

4

一些 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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接