Greedquest / vbInvoke

vbInvoke
MIT License
9 stars 1 forks source link

Active error handler is ignored #11

Open Ik-12 opened 1 year ago

Ik-12 commented 1 year ago

Thanks for this great utility. I hoped I could use it to fix the issue that Application.Run ignores the active error handler. However, calling a function with this method suffers from the same problem. Please see below for a minimal reproducible example.

Any ideas if this could be made to work?

(Tested with MS Access 2019 16.0.10397.20021 64-bit)

Public Sub Test()

On Error GoTo ErrorHandler

Dim exampleModuleAccessor As Object
Set exampleModuleAccessor = GetStandardModuleAccessor("SomeModule", Application.VBE.VBProjects("SomeProject"))

' Brings up the Debug Dialog; expected behavior is to handle the error with the error handler
' (as happens if "TestCallback" is method of a real class module).
CallByName exampleModuleAccessor, "TestCallback", VbMethod

ErrorHandler:
If Err.Number <> 0 Then
    Debug.Print "Error handled."
    Err.clear
End If

End Sub

Public Function TestCallback()

Debug.Print "TestCallback called."

' Throw error
Debug.Print 1 / 0

End Function
Greedquest commented 1 year ago

Known limitation I'm afraid. I imagine Application.Run uses this exact same mechanism under the hood. I believe the IDispatch::Invoke of the VBComponent creates an independent debugger session. So would need to manually disable it and that would likely require some assembly instruction hacking. I think IIRC Rubberduck itself struggles in this area - if you have an unhandled error in a unit test RD cannot suppress it.

Workaround is of course to add error handling inside the TestCallback function, similar to how RD inserts automatic error handling code in the default test method template:

'@TestMethod("Uncategorized")
Private Sub TestMethod1()
    On Error GoTo TestFail

    'Arrange:

    'Act:

    'Assert:
    Assert.Succeed

TestExit:
    Exit Sub
TestFail:
    Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
    Resume TestExit
End Sub
Greedquest commented 1 year ago

Sidenote:

Dim exampleModuleAccessor As Object
Set exampleModuleAccessor = GetStandardModuleAccessor("SomeModule", Application.VBE.VBProjects("SomeProject"))

' Brings up the Debug Dialog; expected behavior is to handle the error with the error handler
' (as happens if "TestCallback" is method of a real class module).
CallByName exampleModuleAccessor, "TestCallback", VbMethod

Could be

Dim exampleModuleAccessor As Object
Set exampleModuleAccessor = GetExtendedModuleAccessor("SomeModule", Application.VBE.VBProjects("SomeProject"))

' Brings up the Debug Dialog; expected behavior is to handle the error with the error handler
' (as happens if "TestCallback" is method of a real class module).
exampleModuleAccessor.TestCallback

although that won't help with the error trapping, it is just a neater syntax

Ik-12 commented 1 year ago

Thanks for the prompt response and the suggestions! It seems that this is indeed a VBA language limitation.

Ik-12 commented 1 year ago

I investigated the error handling issue a bit further:

Greedquest commented 1 year ago

Thanks for the research!

Does InvokeHook enable error handling/trapping? Or is it the same as CallByName - untrappable errors but at least the correct error message.

The vtable trick is actually my post:) Thinking aloud - I don't believe just because the method is called from a class, that is what is enabling the error handling, I think vba's AddressOf operator points to a wrapper function that contains the debugger context because otherwise you couldn't put breakpoints in window subclassing procs, which IIRC you can.

Does dispcallfunc AddressOf Foo with the error raising .bas method trap errors?

Ik-12 commented 1 year ago

I didn't test InvokeHook because it requires distributing/installing TLBINF32.DLL, which isn't possible in our case. However, I assume it works as it's the official workaround provided by Microsoft.

Error trapping from caller doesn't work with dispcallfunc (tested).

Yet another undocumented detail about CallByName: If the error number raised by the called method is in range 0-65535, it's returned correctly as is. But if error number is < 0 (as it often should be, see 6.1.3.2.1.2 in MS-VBAL), it's replace by generic error number 440.

Greedquest commented 1 year ago

InvokeHook most likely calls ITypeInfo::Invoke under the hood - this is possible to do without TLBINF32.dll (I left a comment on that qu you linked pointing to the VBA version of this repo on codereview. In this repo's original VBA implementation, I use QueryInterface and various vtable calls to navigate the typelibs manually to avoid a dependency on TLBINF32).

However I understood from that linked microsoft article that it only fixed the Err.Number, and didn't make mention of whether it fixed the problem with Error handlers being ignored., which is what you care about?

So worth checking if TLBINF32.InvokeHook on the StandardModuleAccessor's ITypeInfo allows invocation with error handling, or even gets the error numbers right (unlike callbyname). I would be surprised if it did help since CreateStdDispatch makes IDispatch::Invoke forward calls to ITypeInfo::Invoke so I don't see these as behaving differently. But perhaps VBA uses a custom IDispatch implementation or something...