6

We have a native C++ application which supports some VBA macros of various types over COM. One of these types, VBAExtension, registers itself with the core C++ application, resulting in an instance of (a class derived from) IConnectionPointImpl<Extension, &DIID_IExtensionEvents, CComDynamicUnkArray>. This works fine; both core and other VBA macros can access the methods on IExtensionEvents, given an appropriate VBAExtension object.

We also have a .NET assembly (written in C#) which is also loaded in to the core application at run-time. For historical reasons, the assembly is loaded in by an auto-running VBA macro; then, when the user presses a particular button, another VBA macro runs the main entry point of the assembly, which brings up a System.Windows.Forms dialog for further interaction.

That’s the setup. I’m seeing some weird behaviour accessing the VBAExtension methods from within the .NET assembly. Specifically, I am running the following code from various locations in the assembly:

foreach (VBAExtension ve in app.Extensions)
{
    System.Diagnostics.Debug.Print("Ext: " + ve.Name);
}

If I run it from the constructor of the assembly’s main object; or from the assembly’s main entry point (before the dialog is displayed), everything is fine – I get the names of the VBAExtensions printed out.

However, if I run the same code from a command kicked off by a button in the assembly’s (modal - we're calling form.ShowDialog()) WinForm, the ve.Names are all blank. The pDispatch->Invoke call made by the IConnectionPointImpl subclass succeeds (returns S_OK), but does not set any return vars.

If I change the dialog to be non-modal (invoked with form.Show()), then the names work again. The modality (modalness?) of the form appears to affect whether the IConnectionPointImpl calls succeed.

Anyone know what's going on?

Edit: Since first posting, I've demonstrated that it's not the invoking call stack that matters; instead, it's whether the call is made from a modal dialog. I have updated the main text.

Edit 2: Per Hans Passant's answer, here are the answers to his diagnostic questions:

  • As expected, in the good (modeless) case there is no error if I rename the VBA event handler. The call simply returns no data.
  • I've put a MsgBox call into the VBA handler; it displays in the modeless case, but does not in the modal case. Ergo, the handler is not executed in the modal case.
  • By use of Err, I can tell that if we hit an exception in the VBA handler we get a VBA error dialog. Once clearing that, the C++ Invoke call has 0x80020009 ("Exception occurred") as return code, and pExcepInfo filled in with generic failure values (VBA has swallowed the actual details)
  • The event does not fire on the second display of the modal dialog, either immediately following the first dialog or during a second invocation of the C# add-in.

I'll try to dig into our message loops as a next step.

Chowlett
  • 45,935
  • 20
  • 116
  • 150
  • 1
    `IConnectionPointImpl` is about events and your foreach loop is accessing extension property directly, why do you think `IConnectionPointImpl` is involved here? It would make more sense if your code snippet was C++ code running inside some event handler and that would query external Name property, but this snippet would have to be C++ then and yours in C#. – Roman R. Oct 09 '15 at 15:44
  • Because I can step through and see that it is. Note that `ve` is a .NET object in C# which exposes the `.Name` property; that property is backed in C++ by a method which finds the ICPI interface registered by the VBA macro and `Dispatch`es the relevant... uh... _thing_, which makes the VBA call which returns the Name. That is, the call is C#-(.NET)->C++-(IConnectionPointImp)->VBA. Not C#->VBA. – Chowlett Oct 09 '15 at 15:50
  • Just to make sure I got this right: `ve.Name` has C# implementation, then it calls some C++ code (COM interface method?), then C++ code does its things somehow, and as part of it it issues a COM event, which is handled by certain VBA handler and this handler would be the source of the string in question? Then you see that the string is lost (already in C++ code as early as after completion of `IDispatch::Invoke`) when all this is called in mentioned scenario. – Roman R. Oct 09 '15 at 15:56
  • Very nearly. `ve.Name` has no C# imlpementation. `VBAExtension` is a type from our C++ application's type library, so `ve.Name` is directly over COM. Otherwise, yes. – Chowlett Oct 09 '15 at 16:04
  • 2
    My guess is that it is a threading problem. Your COM object is most likely STA object. Then VBA sink interface is - cutting long story short - restricted to receive calls from the same thread where it connected to connection point. I suppose you some how calling it from another thread afterwards. This is easy to check by tracing thread IDs you have these calls on. Then if you step inside connection point code, there is a chance that you could also see that `Invoke` call gets you failure `HRESULT` which might be later ignored. It again might explain the reason (esp. `RPC_E_WRONG_THREAD`). – Roman R. Oct 09 '15 at 16:09
  • I noticed your mention that you get `S_OK` from `Invoke` though. Also, do you have this string filled by the event handler into `[in, out] BSTR*` parameter? If you left `out` out there then marshaler would not take VBA's string back to your C++ code, however with direct no-marshaling call it might work out well. – Roman R. Oct 09 '15 at 16:17
  • Checking thread IDs, the `Invoke` call is being made in the same C++ thread as the registration for both the modal and modeless cases. I am indeed getting `S_OK` from the `Invoke` - `pDispatch->Invoke(0x1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &disp, &varResult, NULL, NULL);` returns `S_OK` but `disp.rgvargs` is empty. The `.idl` does indeed have `[in, out] BSTR*` - are you suggesting it might _work_ if it were just `[in] BSTR*`? – Chowlett Oct 12 '15 at 08:29

1 Answers1

4

There are excessively few hard facts in this question to build an answer on. Could be something very simple, could be a nasty memory corruption problem or an obscure dependency inside the VBA interpreter on the thread state. The rough diagnostic is that the VBA event handler simply did not run. That is not an uncommon accident in general, the declarative style used in Basic to declare event handlers leave very few good way to ever diagnose subscription problems. Many a VBA programmer has lost clumps of hair trying to troubleshoot a "why did the event handler not run" problem like this one.

Gather some hard facts first and add them to your question:

  • First verify that your C++ code can actually see that there is no event handler at all. Use the good version, rename the event handler. Expectation is that you don't, raising an event that the sink doesn't subscribe is not an error.
  • Verify that the event handler actually executed in the bad version. Have it do something else than assign the BSTR argument, some you can easily see like a file on disk.
  • Verify that you can properly diagnose an exception in the event handler. Assign the Err object and verify that your C++ code generates a proper diagnostic. Note that your IDispatch::Invoke() call passes NULL for pExcepInfo, not a good way to generate a diagnostic.
  • Check if the event runs the second time you display a window, if it does then you have an execution order problem.

Focusing a bit on ShowDialog(). This method does have plenty of side-effects. The first thing that no longer works is the message loop in your C++ code. It is now the .NET message loop that dispatches messages. Look at yours for side-effects, doing more work than simply GetMessage/DispatchMessage(). That work no longer gets done. Also search your codebase for PostThreadMessage(), those messages fall on the floor when the .NET code pumps.

And keep in mind that your native C++ code loses control when the C# code calls ShowDialog(). It doesn't regain control until you close the window. That can trigger a simple order-of-execution problem, your C++ code should not do anything important after does whatever it does to get the C# code running.

Bahman_Aries
  • 4,658
  • 6
  • 36
  • 60
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Thanks for the pointers. As you may have guessed, I'm not a VBA / COM expert, so your diag suggestions are very welcome. I've updated my answer - the main point appears to be the in the failure case the VBA event is not being executed at all. – Chowlett Oct 19 '15 at 09:32
  • Well, you've got your smoking gun, binding the VBA event handler just never happened. An execution order problem is the best theory, you however can't set a clean breakpoint on the code that is supposed to do the binding. Focus on the code that loads the VBA module and ensure it runs before the dialog appears. Call Microsoft Support if necessary, they can set that breakpoint. – Hans Passant Oct 19 '15 at 10:03
  • Hmm, not sold on that. Given a C# assembly with two entry points, one of which displays the dialog as Modal and one as Modeless; and two VBA macros each calling one of those entry points; the event fires in the modeless case and fails in the modal case when performed in either order _even within the same application execution_. So it's definitely bound, because the modeless case can call it. – Chowlett Oct 19 '15 at 10:15