提问人:Greedo 提问时间:5/21/2019 最后编辑:Ryan WildryGreedo 更新时间:5/28/2019 访问量:1844
如何处理VBA中的DLL错误?
How can I handle DLL errors in VBA?
问:
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)
当或不存在时不快乐。Destination
Source
我认为这些错误是内存访问冲突。我假设 Windows 告诉调用者它正在做一些它无法做的事情1 - 也许它向 Excel 发送了一条消息并且没有处理程序?MSDN对此有这样的说法:
调用 Windows 动态链接库 (DLL) 期间出现系统错误,或者 Macintosh 代码资源不会引发异常,也无法被捕获 使用 Visual Basic 错误捕获。调用 DLL 函数时,你 应检查每个返回值是否成功或失败(根据 API 规范),如果发生故障,请检查 值。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 计时器。这些需要向它们注册回调函数,这些回调函数(鉴于 VBA 的限制)必须以函数指针的形式出现(使用关键字生成)。Long
AddressOf
回调来自用户代码,可能无效。我的包装的全部意义在于提高 API 调用的稳定性,这是一个需要改进的领域。
内存复制问题可能超出了这个问题的范围,它与在 VBA 中制作生成器有关,但我认为同样的错误处理技术也适用于那里,它提供了一个更简单的例子。
我还从 Timer API 中收到错误和崩溃,为 Excel 生成了太多未经处理的消息。我再次想知道,Windows如何告诉Excel“现在是时候崩溃了”,为什么我不能拦截该指令并自己处理错误(即杀死我制作的所有计时器并刷新消息队列)?
答:
来自评论:
当然,如果我进行了错误的 API 调用,则必须让 Excel/我的代码知道它很糟糕
不一定。如果您要求 API 函数(例如)覆盖您为其提供指针的位置的内存,它会愉快地尝试这样做。然后可能会发生一些事情:RtlMoveMemory
如果内存不可写(例如代码),那么您将很幸运地获得访问冲突,这将在进程造成更多损害之前终止该过程。
如果内存是可写的,它将被覆盖并因此损坏,之后所有赌注都将关闭。
从您的评论:
我正在设计代码来附加用户提供的回调函数
另一种方法是使用客户端代码可以实现的方法设计接口。然后要求客户端传递实现该接口的类的实例。
如果您的客户端是 VBA,则定义接口的一种简单方法是使用一个或多个空方法创建公共 VBA 类模块。按照惯例,您应该使用(接口)前缀来命名此类 - 例如 .空方法(Subs 或 Functions)可以具有您想要的任何签名,但我会保持简单:I
IMyCallback
例:
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 调用,并通过接口安全地调用客户端代码。
评论
其中一些 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
评论
how to avoid them happening
:避免这种情况的唯一真正方法是传递有效的指针。访问冲突与 Win32API 调用返回的系统错误不同。lpPrevWndFunc