Как я могу обработать ошибки DLL в VBA? - PullRequest
12 голосов
/ 21 мая 2019

Объявление 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), а в случае сбоя проверьте значение в свойстве объекта Err LastDLLError. LastDLLError всегда возвращает ноль на Macintosh. (выделение мое)

Но в этих случаях у меня нет значений для проверки на ошибки, я просто получаю сбой.

1: При условии, что он улавливает ошибку, которая может возникнуть не всегда, если, скажем, перезапись памяти действительна, но не определена. Но, конечно, запись в ограниченную память или вызов фальшивых указателей должны быть уловлены до того, как они будут выполнены, верно? Меня больше всего интересует: Что является причиной этого сбоя (как , как он срабатывает , так и , что именно за механизмом стоит - как Excel знает, что он должен аварийно завершать работу?). По какому каналу сообщений передаются эти ошибки, и можно ли перехватить их с помощью кода VBA?

Может ли сбой быть проактивным (т.е. санация входов и т. Д.) Или ретроактивным (обработка ошибок).

Я думаю (1), вероятно, пролит свет на (2) и наоборот


В любом случае, если кто-то знает, как обрабатывать подобные ошибки API без сбоев Excel, или как избежать их возникновения, или что-то, что могло бы быть потрясающим. 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) должны быть представлены в виде Long указателей на функции (сгенерированных с ключевым словом AddressOf).

Обратные вызовы поступают из кода пользователя и могут быть недействительными. Весь смысл моей упаковки - улучшить стабильность вызовов API, и это одна область, которая нуждается в улучшении.

Проблема копирования памяти, вероятно, выходит за рамки этого вопроса, она связана с созданием генераторов в VBA, но я думаю, что там будут применяться те же методы обработки ошибок, и это упрощает пример.

Я также получаю ошибки и сбои от API таймера, генерирующего слишком много необработанных сообщений для Excel.Еще раз мне интересно, как Windows сообщает Excel «Время сбоя сейчас», почему я не могу перехватить эту инструкцию и устранить ошибку самостоятельно (т.е. уничтожить все сделанные мной таймеры и очистить очередь сообщений)?

Ответы [ 2 ]

10 голосов
/ 21 мая 2019

Из комментария:

Конечно, если я сделаю ошибочный вызов 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
…

Затем можно реализовать собственную функцию обратного вызова, которая вызывается из таймера, и безопасно вызывать код клиента через интерфейс.

4 голосов
/ 27 мая 2019

Некоторые из этих вызовов Windows API могут быть опасными. Если вы хотите использовать функциональность Windows API как библиотечную функцию, было бы вежливо не подвергать своих клиентов такой опасности. Итак, вам лучше всего реализовать собственный интерфейсный уровень.

Ниже приведен код, который поставляет API-интерфейс Windows Timer как библиотечную функцию, безопасную для использования, поскольку он передает строковые имена кода обратного вызова вместо указателей.

Этот код был впервые опубликован в моем блоге . Также в этом посте я обсуждаю альтернативы 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
...