1
votes

I've created a simple c# COM component that is meant to be called by a C++ MFC application. It works but has an undesired side-effect.

The C++ client calls the c# component using it's main GUI thread, so I can't block it because the C# code is making Database operations that may take longer. That's why i need async or a thread for this...

This is the C# code, simplified:

    public async void ShowOverviewDialogAsync()
    {
        var w = new Window();
        var dbOperationOk = await LongDbOperaionAsync();
        w.ShowDialog();
    }

Well, the C++ code goes in, and returns after the call to async which is expected. Here is the C++ calling code:

HRESULT hr = cSharpCom.CoCreateInstance(__uuidof(CSharpCom));
if (SUCCEEDED(hr))
{
   cSharpCom->ShowOverviewDialogAsync();
}
// continues without waiting for the dialog close...

The thing is, after this call, the C++ code continues to do its thing and the cSharpCom object goes out of scope. So the only way to call a Release is to make the c# object a member and on destructing or creating a new one, do a release call:

if (_cSharpCom != NULL) _cSharpCom->Release();
HRESULT hr = _cSharpCom.CoCreateInstance(__uuidof(CSharpCom));
if (SUCCEEDED(hr))
{
   _cSharpCom->ShowOverviewDialogAsync();
}

This is the first Drawback. The second drawback is that the modal C# window is not a real Modal window as the message pooling of the MFC(C++) continues as normal, which is actually undesired (Right?)

These are the current ideas:

  1. return a HWND to the calling code and somehow make the modal call using the C++ Cwnd:RunModalLoop.
  2. return some kind of event to the calling code, that can be waited upon without blocking the GUI thread.
  3. set a callback that will make sure the COM object will be released.
  4. override the Task class so that it returns a COM aware Interface that allows the C++ code to wait on something (see 2. and 3.)

Number 4 is what I will probably try out. The COM signature would look like:

    HRESULT _stdcall ShowOverviewDialogAsync([out, retval] IUnknown** pRetVal);

Just for the lack of completeness, if the method has no async, the MFC C++ GUI hread works as expected and shows the Window in a modal way:

    public void ShowOverviewDialogAsync()
    {
        var w = new Window();
        var dbOperationOk = LongDbOperaion();
        w.ShowDialog(); // C++ will wait here - non-blocking
    }

If you are asking why my async method is returning void. Well, it's defined in an interface and I can't use Task as a return value, unless I go for the solution 4 above.

[ComVisible(true)]
[Guid("XXXXXX-xxxx-xxx-xxx-XXXXX")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICSharpCom
{
    /// <summary>
    /// Displays the dialog in async mode
    /// </summary>
    [DispId(0)]
    void ShowOverviewDialogAsync(); 
}

Besides, COM interop does not accept generics like Task.

How to test it. Create a MFC application with one button that calls the c# com as defined.

Any ideas appreciated.

cheers, Marco

2
About the 1st drawback, well this is COM, you must release objects you've created (or use smart pointers, etc.). For the rest, mixing different UI technologies seems unnecessary. Also the async/await syntactic candy should be kept on the C# side IMHO, especially if it's "just" for long-running tasks. So, I would try to keep all UI related things (windows, dialogs, etc.) in the MFC app, and non-UI things in C#. You can use threads in C# or in MFC. It's easier to make sure you act nice with threading in UI only if it's only a central place (MFC so).Simon Mourier
Yep, i also think I should keep the UI on the MFC. The thing is that we have different platforms running in c++, Delphi and other stuff and with COM i can serve all of them in one shot. The releasing is not really a problem, just design ugliness. I'm using smart pointer already. It's more like a challenge to get this working,Marco
There is yet another drawback. Any excption thrown within the asyn method will be shown on the Win32 application, in my case on the MFC, as a KernellException without details whatsoever. - the last entry on the managed code is mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.ThrowAsync.AnonymousMethod__6_0(object state) Unknown Symbols loaded. and then it jumps to KernelBase.dll!_RaiseException@16() Unknown Symbols loaded. That's it. No details of the managed exception is passed to the COM client.Marco

2 Answers

1
votes

I've managed to get it working, although I'm not very happy with the spaghetti code needed in c++.

This is how I've managed until now:

    [ComVisible(true)]
    [Guid("XXXXXX-xxxx-xxx-xxx-XXXXX")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface ICSharpCom
    {
        /// <summary>
        /// Displays the dialog in async mode
        /// </summary>
        [DispId(0)]
        void ShowOverviewDialogAsync(IntPtr eventHandle); 
    }

    //Implementation:        
    ...
    _resetEvent = new ManualResetEvent(false);
                m.SafeWaitHandle = new Microsoft.Win32.SafeHandles.SafeWaitHandle(eventHandle, ownsHandle);


    // ... at some later point in the method, when the window closes

    _resetEvent .Set()

Until now, looks ok. On the C++ side it gets complicated:

            auto handle = CreateEvent(NULL, TRUE, FALSE, CString("MyEvent"));

            if (handle != NULL)
            {
                HANDLE handles[] = { handle };
                hr = prescOverview->ShowOverviewDialog((long)handle);
                auto handleCount = _countof(handles);
                if (SUCCEEDED(hr))
                {
                    BOOL running = TRUE;
                    do {
                        DWORD const res = ::MsgWaitForMultipleObjectsEx(
                            handleCount,
                            handles,
                            INFINITE,
                            QS_ALLINPUT,
                            MWMO_INPUTAVAILABLE | MWMO_ALERTABLE);

                        if (res == WAIT_FAILED)
                        {
                            running = FALSE;
                            hr = GetLastError();
                            break;
                        }
                        else if (res == WAIT_OBJECT_0 + 0)
                        {
                            CComBSTR err;
                            prescOverview->GetError(&err);
                            CString errContainer(err);
                            if (errContainer.GetLength() > 0)
                            {
                                // log
                                hr = E_FAIL;
                            }
                            else {
                                hr = S_OK;
                            }
                            running = FALSE;
                            break;
                        }
                        else if (res >= WAIT_OBJECT_0 && res <= WAIT_OBJECT_0 + handleCount)
                        {
                            // process messages.
                            MSG msg;
                            while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
                            {
                                if (msg.message == WM_QUIT)
                                {
                                    PostQuitMessage(static_cast<int>(msg.wParam));
                                    hr = ERROR_CANCELLED;
                                    running = FALSE;
                                    break;
                                }
                                else {
                                    TranslateMessage(&msg);
                                    DispatchMessage(&msg);
                                }
                            }

                        }
                    } while (running);
                }
                CloseHandle(handle);
            }
        }

It's working. However The calling client must know how to do it -> read the docs and it's not intuitive.

I'm still working on it though.

Cheers, Marco

0
votes

I've just noticed i've never updated this. Well, the easiest way to achieve this is to use events. If you are using ATL this will help you. I've managed this by first declaring the C# COM like this:

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(ICSharpCom))]
[ComSourceInterfaces(typeof(ICSharpComEventHandler))]
[Guid("XXXXXX-xxxx-xxx-xxx-XXXXX")]
public class CSharpCom : ICSharpCom
{
    [ComVisible(false)]
    public delegate void WorkCompleted(string result);

    public event WorkCompleted OnWorkCompleted;

    public int DoWork(string input)
    {
       Task t = ....
       // do some hard work aync by usin
       OnWorkCompleted?.Invoke(t.Result);
    } 
}
[ComVisible(true)]
[Guid("XXXXXX-xxxx-yyy-xxx-XXXXX")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ICSharpComEventHandler 
{
    [DispId(1)]
    void OnWorkCompleted(string result);
}

On The C++ side, it's not that easy

_ATL_FUNC_INFO OnWorkCompletedInfo = {CC_STDCALL, VT_EMPTY, 1, {VT_BSTR}};
    class CEventSink : public IDispEventSimpleImpl<ONE_GOOD_ID, CEventSink, &DIID_ICSharpComEventHandler>
    {
        public:
            BEGIN_SINK_MAP(CEventSink)
                SINK_ENTRY_INFO(ONE_GOOD_ID, DIID_ICSharpComEventHandler, 1, OnWorkCompleted, &OnWorkCompletedInfo)
            END_SINK_MAP()

            const void __stdcall OnWorkCompleted(_bstr_t result)
            {
                // do something
            }

            CEventSink(CComPtr<ICSharpCom> psharp)
            {
                if (!pEps)
                {
                    throw std::invalid_argument("psharp was null");
                }

                m_pSharp = psharp;
                DispEventAdvise(m_pSharp);
            }

            void __stdcall StartListening()
            {
                DispEventAdvise(m_pSharp);
            }

            void __stdcall StopListening()
            {
                DispEventUnadvise(m_pSharp);
                m_pSharp = nullptr;
            }


        private:
            CComPtr<ICSharpCom> m_pSharp;
    };

That's it guys. Sorry for not posting before.

Cheers, Marco