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)
当Destination
或Source
不存在时,不高兴。
我认为这些错误是内存访问冲突。我假设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“现在到崩溃的时间”,为什么我不能截取该指令并自己处理错误(即杀死我创建的所有计时器并刷新消息队列)?
答案 0 :(得分:9)
摘自评论:
当然,如果我进行了错误的API调用,则必须让Excel /我的代码知道这很糟糕
不一定。如果您要求API函数(例如RtlMoveMemory
)在您提供指针的位置覆盖内存,它将很乐意尝试这样做。然后可能会发生很多事情:
如果内存不可写(例如代码),那么您将很幸运地遇到访问冲突,该访问冲突将在该进程造成更多损害之前将其终止。
如果内存发生可写的情况,它将被覆盖并因此被破坏,之后所有赌注都将消失。
根据您的评论:
我正在设计代码以附加用户提供的回调函数
一种替代方法是设计一个带有您的客户端代码可以实现的方法的接口。然后要求客户端传递实现该接口的类的实例。
如果您的客户端是VBA,则定义接口的一种简单方法是使用一个或多个空方法创建公共VBA类模块。按照惯例,您应使用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
…
然后,您可以实现自己的从计时器调用的回调函数,并通过该接口安全地调用客户端代码。
答案 1 :(得分:4)
其中一些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