Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

C++ and .NET interop

Речь пойдет о механизме и нюансах исполнения .NET кода из C++ и, соответственно, тонкостях взаимодействия неуправляемого кода и управляемого (С++ с CLR).

Сценарий таков: С++ приложение инициализирует CLR engine в своем процессе, загружает .NET сборки и использует .NET объекты. Обращение к методам и свойствам объектов происходит через COM интерфейсы, в нашем варианте другого механизма нет.

Конкретный случай - нужно показать форму .NET и взаимодействовать с ней (вызывать методы и получать уведомления).

Рассмотрим диаграмму классов .NET библиотеки:
NETLibrary class diagram

Главное действующее лицо - NETForm, остальные классы нужны для взаимодействия через COM (их можно вообще вынести в отдельную сборку, а NETForm создавать так, будто это просто .NET форма).

C++ будет использовать интерфейс INETFormController для взаимодействия с классом NETFormController, который в свою очередь будет адаптером для NETForm.

using System;
using System.Runtime.InteropServices;
 
namespace NETLibrary
{
   [ComVisible(true)]
   [Guid("AB185BF6-B576-46F8-880B-16484A2893FD")]
   [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
   public interface INETFormController
   {
      void AddCallback(INETFormControllerCallback callback);
      void Show();
      void Close();
      void RequestNotification();
   }
}

INETFormControllerCallback - это COM source интерфейс:

using System;
using System.Runtime.InteropServices;
 
namespace NETLibrary
{
   [ComVisible(true)]
   [Guid("466444F6-2206-4E61-BE39-D3D781E8D8DB")]
   [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
   public interface INETFormControllerCallback
   {
      void Shown();
      void ButtonClicked();
      void Notification();
   }
}

Использование INETFormControllerCallback в качестве source интерфейса позволяет задействовать унифицированный механизм подписки и отписки от уведомлений COM объекта (подробнее - Using IConnectionPointContainer). Кстати, RCW поддерживает этот интерфейс, если к классу прикреплен атрибут ComSourceInterfaces.

На форме есть поле ввода и кнопка, для начала попытаемся показать форму и получить уведомление о том, что кнопку нажали:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
 
namespace NETLibrary
{
   [ComVisible(false)]
   [ClassInterface(ClassInterfaceType.None)]
   [ComSourceInterfaces(typeof(INETFormControllerCallback))]
   class NETFormController : INETFormController
   {
      public NETFormController() {
         InitializeForm();
      }
 
      public void AddCallback(INETFormControllerCallback callback) {
         Shown += callback.Shown;
         ButtonClicked += callback.ButtonClicked;
         Notification += callback.Notification;
      }
 
      public void Show() {
         _netForm.Show();
      }
 
      public void Close() {
         _netForm.Close();
      }
 
      public void RequestNotification() {
         FireNotification(this, EventArgs.Empty);
      }
 
      public event Action Shown;
      public event Action ButtonClicked;
      public event Action Notification;
 
      private void InitializeForm() {
         _netForm = new NETForm();
         _netForm.Shown += FireShown;
         _netForm.ButtonClicked += FireButtonClicked;
      }
 
      private void FireShown(Object sender, EventArgs args) {
         if (Shown != null) {
            Shown();
         }
      }
 
      private void FireButtonClicked(Object sender, EventArgs args) {
         if (ButtonClicked != null) {
            ButtonClicked();
         }
      }
 
      private void FireNotification(Object sender, EventArgs args) {
         if (Notification != null) {
            Notification();
         }
      }
 
      NETForm _netForm;
   }
}

Далее - C++ (код инициализации CLR я взял из (All-In-One Code Framework.zip\Visual Studio 2010\CppHostCLR):

#include "stdafx.h"
 
NETLibrary::INETFormControllerPtr g_pNETObj;
 
class ATL_NO_VTABLE CNETFormControllerCallback : public ATL::CComObjectRootEx<CComSingleThreadModel>,
                                                 public NETLibrary::INETFormControllerCallback
{
 
protected:
   DECLARE_PROTECT_FINAL_CONSTRUCT()
   BEGIN_COM_MAP(CNETFormControllerCallback)
      COM_INTERFACE_ENTRY(NETLibrary::INETFormControllerCallback)
   END_COM_MAP()
 
public:
   STDMETHOD(raw_Shown)() {
      OutputDebugString(_T("Shown\n"));
      return(S_OK);
   }
 
   STDMETHOD(raw_ButtonClicked)() {
      OutputDebugString(_T("ButtonClicked\n"));
      g_pNETObj->RequestNotification();
      return(S_OK);
   }
 
   STDMETHOD(raw_Notification)() {
      OutputDebugString(_T("Notification\n"));
      return(S_OK);
   }
 
};
 
int _tmain(int argc, _TCHAR* argv[]) {
   HRESULT hr;
 
   CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
 
   CComPtr<ICLRMetaHost> spMetaHost;
   CComPtr<ICLRRuntimeInfo> spRuntimeInfo;
   CComPtr<ICorRuntimeHost> spCorRuntimeHost;
   IUnknownPtr spAppDomainThunk;
   _AppDomainPtr spDefaultAppDomain;
 
   hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&spMetaHost)); if (FAILED(hr)) return(hr);
   hr = spMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&spRuntimeInfo)); if (FAILED(hr)) return(hr);
   BOOL fLoadable;
   hr = spRuntimeInfo->IsLoadable(&fLoadable); if (FAILED(hr)) return(hr);
   hr = spRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_PPV_ARGS(&spCorRuntimeHost)); if (FAILED(hr)) return(hr);
   hr = spCorRuntimeHost->Start(); if (FAILED(hr)) return(hr);
   hr = spCorRuntimeHost->GetDefaultDomain(&spAppDomainThunk); if (FAILED(hr)) return(hr);
   hr = spAppDomainThunk->QueryInterface(IID_PPV_ARGS(&spDefaultAppDomain)); if (FAILED(hr)) return(hr);
   const _AssemblyPtr spAssembly = spDefaultAppDomain->Load_2(bstr_t(L"NETLibrary"));
   const variant_t vtObject = spAssembly->CreateInstance(bstr_t(L"NETLibrary.NETFormController"));
 
   g_pNETObj = vtObject;
 
   const NETLibrary::INETFormControllerCallbackPtr spCallback = new ATL::CComObjectNoLock<CNETFormControllerCallback>();
 
   g_pNETObj->AddCallback(spCallback);
   g_pNETObj->Show();
 
   MSG msg;
   while (GetMessage(&msg, NULL, 0, 0) > 0) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
   }
 
   return msg.wParam;
}

AddCallback я добавил чтобы упростить код (правильно - использовать connection point).

Совсем забыл - в проекте NETLibrary настроен post-build event для создания библиотеки типов:

"c:\Program Files\Microsoft SDKs\Windows\v7.0A\bin\NETFX 4.0 Tools\TlbExp.exe" $(TargetPath) /out:$(TargetDir)\$(TargetName).tlb

C++ использует директиву #import для генерации кода:

#import "../Debug/NETLibrary.tlb" rename_namespace("NETLibrary")

Итог запуска приложения - форма на экране, в окне отладки строчка Shown, а каждое нажатие на кнопку добавляет строчку ButtonClicked. Есть только одно маленькое но - не работает фокус. Вообще. Подробнее можно прочитать в Windows Forms and Unmanaged Applications Overview: the form may behave unexpectedly. For example, when you press the TAB key, the focus does not change from one control to another control. When you press the ENTER key while a command button has focus, the button's Click event is not raised. You may also experience unexpected behavior for keystrokes or mouse activity.

Там же предлагают решения - 1) использовать метод ShowDialog и 2) показывать форму в отдельном потоке с запуском цикла сообщений WinForms. Модальный диалог нам не подходит, поэтому идем в сторону создания потока.

Вторая версия NETFormController:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
 
namespace NETLibrary
{
   [ComVisible(false)]
   [ClassInterface(ClassInterfaceType.None)]
   [ComSourceInterfaces(typeof(INETFormControllerCallback))]
   class NETFormController : INETFormController
   {
      public NETFormController() {
         _thread = new Thread(ThreadProc);
         _thread.SetApartmentState(ApartmentState.STA);
         _thread.Start();
         _threadInitialized.WaitOne();
      }
 
      public void AddCallback(INETFormControllerCallback callback) {
         Shown += callback.Shown;
         ButtonClicked += callback.ButtonClicked;
         Notification += callback.Notification;
      }
 
      public void Show() {
         _remoteSync.Invoke((MethodInvoker)delegate { _netForm.Show(); });
      }
 
      public void Close() {
         _remoteSync.Invoke((MethodInvoker)delegate { _netForm.Close(); });
      }
 
      public void RequestNotification() {
         _remoteSync.Invoke((MethodInvoker)delegate { FireNotification(this, EventArgs.Empty); });
      }
 
      public event Action Shown;
      public event Action ButtonClicked;
      public event Action Notification;
 
      private void InitializeForm() {
         _netForm = new NETForm();
         _netForm.Shown += FireShown;
         _netForm.ButtonClicked += FireButtonClicked;
      }
 
      private void ThreadProc() {
         _remoteSync = new Control();
         _remoteSync.CreateControl();
         InitializeForm();
         _threadInitialized.Set();
         Application.Run();
      }
 
      private void FireShown(Object sender, EventArgs args) {
         if (Shown != null) {
            Shown();
         }
      }
 
      private void FireButtonClicked(Object sender, EventArgs args) {
         if (ButtonClicked != null) {
            ButtonClicked();
         }
      }
 
      private void FireNotification(Object sender, EventArgs args) {
         if (Notification != null) {
            Notification();
         }
      }
 
      Thread _thread;
      AutoResetEvent _threadInitialized = new AutoResetEvent(false);
      Control _remoteSync;
      NETForm _netForm;
   }
}

Запуск приложения приводит в недоумение:

A first chance exception of type 'System.InvalidCastException' occurred in NETLibrary.dll

Additional information: Unable to cast COM object of type 'System.__ComObject' to interface type 'NETLibrary.INETFormControllerCallback'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{466444F6-2206-4E61-BE39-D3D781E8D8DB}' failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).

Происходит это вот здесь:

private void FireShown(Object sender, EventArgs args) {
   if (Shown != null) {
      Shown();
   }
}

CLR пытается нам сообщить, что переданный CNETFormControllerCallback не поддерживает интерфейс INETFormControllerCallback! Недавно поддерживал, а теперь вдруг перестал. Не имеет значения, что там вызов делегата - фактически выполняется следующий код:

callback.Shown();

Что же произошло? Тоесть WTF? Вернем прежнюю реализацию NETFormController и посмотрим как используется CNETFormControllerCallback. Для этого я добавил точки останова в CComObjectNoLock.

> CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::QueryInterface(const _GUID & iid={...}, void * * 
  ppvObject=0x003ded84) Line 3057 C++
  clr.dll!SafeQueryInterfaceHelper() + 0x4f bytes
  clr.dll!SafeQueryInterface() + 0x45 bytes
  clr.dll!RCW::SafeQueryInterfaceRemoteAware() + 0x3d bytes
  clr.dll!RCW::GetComIPForMethodTableFromCache() + 0x6b bytes
  clr.dll!RCW::GetComIPFromRCW() + 0x31 bytes
  clr.dll!ComObject::GetComIPFromRCW() + 0x40 bytes
  clr.dll!ComObject::GetComIPFromRCWEx() + 0x64 bytes
  clr.dll!StubHelpers::ProcessByrefValidationList() + 0x231b2 bytes
  clr.dll!StubHelpers::GetCOMIPFromRCW() + 0x7c bytes
  012306b4()
  System.Windows.Forms.ni.dll!5d67efc3()

Здесь нас просят {466444F6-2206-4E61-BE39-D3D781E8D8DB}, что есть INETFormControllerCallback. Вывод первый - вызов COM метода намного сложнее, чем кажется на первый взгляд. RCW выполняет множество операций прежде, чем произойдет фактический переход по адресу обработчика из таблицы виртуальных функций. Вывод второй - вызов происходит динамически. Нажатие на кнопку не приводит к срабатыванию точки останова! Вывод третий - RCW использует кеш для сохранения результатов своей деятельности.

Посмотрим, что происходит в случае показа формы из другого потока. По логике - обратный вызов должен происходить в потоке формы, т.е. С++ обработчик должен исполняться в нем же.

> ole32.dll!_CoUnmarshalInterface@12()
  clr.dll!IUnkEntry::UnmarshalIUnknownForCurrContext() + 0x153 bytes
  clr.dll!IUnkEntry::GetIUnknownForCurrContext() + 0x1d6a22 bytes
  clr.dll!RCW::SafeQueryInterfaceRemoteAware() + 0x16 bytes
  clr.dll!RCW::GetComIPForMethodTableFromCache() + 0x6b bytes
  clr.dll!RCW::GetComIPFromRCW() + 0x31 bytes
  clr.dll!ComObject::GetComIPFromRCW() + 0x40 bytes
  clr.dll!ComObject::GetComIPFromRCWEx() + 0x64 bytes
  clr.dll!StubHelpers::ProcessByrefValidationList() + 0x231b2 bytes
  clr.dll!StubHelpers::GetCOMIPFromRCW() + 0x7c bytes
  013807c4()
  user32.dll!___fnDWORD@4() + 0x24 bytes
  System.Windows.Forms.ni.dll!5cd54ae8()

Как видно - вызов уходит в ole32.dll. А она в свою очередь захочет библиотеку типа для маршаллинга вызовов(?). А ни библиотека, ни вообще любая информация о интерфейсах в реестре не числится! Далее:

  user32.dll!_PeekMessageW@20()  + 0xf4 bytes
  ole32.dll!CCliModalLoop::MyPeekMessage()  + 0x30 bytes
  ole32.dll!CCliModalLoop::PeekRPCAndDDEMessage()  + 0x30 bytes
  ole32.dll!CCliModalLoop::BlockFn()  - 0x5cf0 bytes
  ole32.dll!ModalLoop()  + 0x52 bytes
  ole32.dll!SwitchSTA()  + 0x21 bytes
  ole32.dll!CRpcChannelBuffer::SwitchAptAndDispatchCall()  - 0x1837 bytes
  ole32.dll!CRpcChannelBuffer::SendReceive2()  + 0xa6 bytes
  ole32.dll!CCliModalLoop::SendReceive()  + 0x1e bytes
  ole32.dll!CAptRpcChnl::SendReceive()  + 0x72 bytes
  ole32.dll!CCtxComChnl::SendReceive()  + 0x47 bytes
  ole32.dll!NdrExtpProxySendReceive()  + 0x43 bytes
  rpcrt4.dll!NdrFixedArrayBufferSize()  + 0x76a bytes
  ole32.dll!_ObjectStublessClient@8()  + 0x7a bytes
  ole32.dll!_ObjectStubless@0()  + 0xf bytes
  ole32.dll!CStdMarshal::RemoteAddRef()  + 0xb9 bytes
  ole32.dll!CStdMarshal::MarshalClientIPID()  + 0x4e bytes
  ole32.dll!CStdMarshal::MarshalIPID()  - 0xcf27 bytes
  ole32.dll!CStdMarshal::MarshalObjRef()  + 0x92 bytes
  ole32.dll!CStdMarshal::MarshalInterface()  + 0x76 bytes
  ole32.dll!CDestObjectWrapper::MarshalInterface()  + 0x19d1 bytes
  ole32.dll!_CoMarshalInterface@24()  + 0x83 bytes

Верите или нет - по дороге выполняется PostMessage в основной поток с целью выполнить вызов именно там! Доказательство - полученное сообщение WM_USER для скрытого окна синхронизации вызовов:

> kernel32.dll!_FindActCtxSectionGuid@20()  + 0x56 bytes 
  ole32.dll!CRIFTable::GetPSClsidHelper()  + 0x2ed4 bytes 
  ole32.dll!CRIFTable::GetPSClsid()  + 0x34 bytes 
  ole32.dll!CStdMarshal::GetPSFactory()  + 0x3d bytes 
  ole32.dll!CStdMarshal::CreateStub()  + 0x5401 bytes 
  ole32.dll!CStdMarshal::ConnectSrvIPIDEntry()  + 0x26 bytes 
  ole32.dll!CStdMarshal::MarshalServerIPID()  + 0x74 bytes 
  ole32.dll!CRemoteUnknown::RemQueryInterface()  + 0x128 bytes 
  rpcrt4.dll!_Invoke@12()  + 0x2a bytes 
  rpcrt4.dll!_NdrStubCall2@16()  + 0x22f bytes 
  ole32.dll!_CStdStubBuffer_Invoke@12()  + 0x70 bytes 
  ole32.dll!SyncStubInvoke()  + 0x34 bytes 
  ole32.dll!StubInvoke()  + 0x7b bytes 
  ole32.dll!CCtxComChnl::ContextInvoke()  + 0xe6 bytes 
  ole32.dll!MTAInvoke()  + 0x1a bytes 
  ole32.dll!STAInvoke()  + 0x4a bytes 
  ole32.dll!AppInvoke()  + 0x92 bytes 
  ole32.dll!ComInvokeWithLockAndIPID()  + 0x27c bytes 
  ole32.dll!ComInvoke()  + 0x71 bytes 
  ole32.dll!ThreadDispatch()  + 0x1a bytes 
  ole32.dll!ThreadWndProc()  + 0xa0 bytes 
  user32.dll!_InternalCallWinProc@20()  + 0x23 bytes 
  user32.dll!_UserCallWinProcCheckWow@32()  + 0xb3 bytes 
  user32.dll!_DispatchMessageWorker@8()  + 0xe6 bytes 
  user32.dll!_DispatchMessageW@4()  + 0xf bytes 
  CPPHost.exe!wmain(int argc=0x00000001, wchar_t * * argv=0x015e1480)  Line 58 + 0xf bytes C++
  CPPHost.exe!__tmainCRTStartup()  Line 552 + 0x19 bytes C
  CPPHost.exe!wmainCRTStartup()  Line 371 C
  kernel32.dll!@BaseThreadInitThunk@12()  + 0x12 bytes 
  ntdll.dll!___RtlUserThreadStart@8()  + 0x27 bytes 
  ntdll.dll!__RtlUserThreadStart@8()  + 0x1b bytes 

Получается, что облом происходит на стороне С++ - вызов GetPSFactory завершается с ошибкой, stub не создается. Предварительно на стороне CLR вероятней всего происходит создание proxy, причем проходит успешно (не проверял - под вопросом).

Полагаю, CLR запоминает контекст каждого объекта (когда он приходит извне), а также тесно взаимодействует с ole32.dll если текущий контекст выполнения отличается от оригинального. Кстати, это касается лишь объектов, созданных не CLR, - при получении указателя на один из интерфейсов реализованных RCW, CLR запрашивает у него IManagedObject (кроме всего прочего) и находит оригинальный .NET объект! Я различными способами пытался получить RCW (System.__ComObject) внутри управляемого кода и у меня ничего не получилось. Также примечательно, что все RCW являются Free Threaded (сам RCW может использоваться разными потоками, а вот оборачиваемый им объект должен сам заботиться о синхронизации), поэтому надо быть осторожней с потоками.

Я нашел 3 способа решения этой проблемы (хотя идеально было бы разделить NETFormController на две части и заставить CLR связать одну со второй через COM - чтобы внутри самого CLR использовался OLE маршаллинг, но это что-то из области сферических коней в вакууме):

  1. Регистрация сборки утилитой regasm.exe
  2. Динамическая регистрация фабрики ProxyStub во время исполнения программы
  3. Маршаллинг обратных вызовов вручную

Первый способ - это тягомотина с инсталляцией приложения, версионностью, мусором в реестре и т.д. Второй - много геморроя, использование недокументированных функций и т.д. Третий - большое количество ручного кода и сложность отладки, но все же лучше, чем 1 и 2. Рассмотрим его более детально.

Необходимо пробрасывать обратные вызовы в поток C++ (и уже в его контексте выполнять). Единственно возможный вменяемый способ - через Control.Invoke. Хитрость состоит в том, что можно создать объект Control в потоке С++ и использовать его:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
 
namespace NETLibrary
{
   [ComVisible(false)]
   [ClassInterface(ClassInterfaceType.None)]
   [ComSourceInterfaces(typeof(INETFormControllerCallback))]
   class NETFormController : INETFormController
   {
      public NETFormController() {
         _localSync = new Control();
         _localSync.CreateControl();
 
         _thread = new Thread(ThreadProc);
         _thread.SetApartmentState(ApartmentState.STA);
         _thread.Start();
         _threadInitialized.WaitOne();
      }
 
      public void AddCallback(INETFormControllerCallback callback) {
         Shown += callback.Shown;
         ButtonClicked += callback.ButtonClicked;
         Notification += callback.Notification;
      }
 
      public void Show() {
         _remoteSync.Invoke((MethodInvoker)delegate { _netForm.Show(); });
      }
 
      public void Close() {
         _remoteSync.Invoke((MethodInvoker)delegate { _netForm.Close(); });
      }
 
      public void RequestNotification() {
         _remoteSync.Invoke((MethodInvoker)delegate { FireNotification(this, EventArgs.Empty); });
      }
 
      public event Action Shown;
      public event Action ButtonClicked;
      public event Action Notification;
 
      private void InitializeForm() {
         _netForm = new NETForm();
         _netForm.Shown += FireShown;
         _netForm.ButtonClicked += FireButtonClicked;
      }
 
      private void ThreadProc() {
         _remoteSync = new Control();
         _remoteSync.CreateControl();
         InitializeForm();
         _threadInitialized.Set();
         Application.Run();
      }
 
      private void FireShown(Object sender, EventArgs args) {
         if (Shown != null) {
            _localSync.Invoke(Shown);
         }
      }
 
      private void FireButtonClicked(Object sender, EventArgs args) {
         if (ButtonClicked != null) {
            _localSync.Invoke(ButtonClicked);
         }
      }
 
      private void FireNotification(Object sender, EventArgs args) {
         if (Notification != null) {
            _localSync.Invoke(Notification);
         }
      }
 
      Thread _thread;
      AutoResetEvent _threadInitialized = new AutoResetEvent(false);
      Control _localSync;
      Control _remoteSync;      
      NETForm _netForm;
   }
}

Теперь все якобы работает - вызовы пробрасываются в поток С++, в окне отладки должные сообщения о показе формы и нажатиях на кнопку. Но стоит усложнить взаимодействие и возникнет проблема - синхронные вызовы Control.Invoke не реентрабельны. Это означает, что вызор любого метода INETFormController из CNETFormControllerCallback приведет к блокировке приложения. Ровно как и попытка послать уведомление внутри синхронного вызова к INETFormController.

Можно посылать уведомления асинхронно - использовать Control.BeginInvoke, но это добавляет кода и усложняет механизм, если результат выполнения важен (например используется output параметр).

Следующий класс решает проблему реентрабельности:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using Microsoft.Win32.SafeHandles;
 
namespace NETLibrary
{
   internal class UIExecutionContext : ISynchronizeInvoke
   {
      private readonly Thread _thread;
      private AutoResetEvent _contextInitialized;
      private AutoResetEvent _contextInitializationFailed;
      private Exception _contextException;
 
      private readonly ISynchronizeInvoke _hostInvoke;
      private ISynchronizeInvoke _contextInvoke;
 
      private readonly Stack<ContextCall> _hostInvokes = new Stack<ContextCall>(2);
      private readonly Stack<ContextCall> _contextInvokes = new Stack<ContextCall>(2);
 
      private readonly AutoResetEvent _performInvoke = new AutoResetEvent(false);
      private readonly AutoResetEvent _performCallback = new AutoResetEvent(false);
 
 
      public UIExecutionContext() {
         _hostInvoke = new SyncControl("Host");
 
         _thread = new Thread(ExecutionContextRoutine);
         _thread.SetApartmentState(ApartmentState.STA);
         _thread.Name = "UIExecutionContext";
         using (_contextInitialized = new AutoResetEvent(false))
         using (_contextInitializationFailed = new AutoResetEvent(false)) {
            _thread.Start();
            WaitHandle[] waitHandles = new WaitHandle[] { _contextInitializationFailed, _contextInitialized };
            if (WaitHandle.WaitAny(waitHandles) == 0) {
               throw new InvalidOperationException("Failed to create UI execution context.", _contextException);
            }
         }
      }
 
      private bool IsContext {
         get { return (Thread.CurrentThread.ManagedThreadId == _thread.ManagedThreadId); }
      }
 
      private ISynchronizeInvoke TargetInvoke {
         get { return (IsContext ? _hostInvoke : _contextInvoke); }
      }
 
      private AutoResetEvent PerformInvoke {
         get { return (IsContext ? _performInvoke : _performCallback); }
      }
 
      private AutoResetEvent PerformCallback {
         get { return (IsContext ? _performCallback : _performInvoke); }
      }
 
      private Stack<ContextCall> Callbacks {
         get { return (IsContext ? _contextInvokes : _hostInvokes); }
      }
 
      private Stack<ContextCall> Invokes {
         get { return (IsContext ? _hostInvokes : _contextInvokes); }
      }
 
      #region Implementation of ISynchronizeInvoke
 
      /// <summary>
      /// Asynchronously executes the delegate on the thread that created this Object.
      /// </summary>
      /// <returns>
      /// An <see cref="T:System.IAsyncResult"/> interface that represents the asynchronous operation started by calling this method.
      /// </returns>
      /// <param name="method">A <see cref="T:System.Delegate"/> to a method that takes parameters of the same number and type that are contained in <paramref name="args"/>. </param><param name="args">An array of type <see cref="T:System.Object"/> to pass as arguments to the given method. This can be null if no arguments are needed. </param>
      public IAsyncResult BeginInvoke(Delegate method, Object[] args) {
         return (TargetInvoke.BeginInvoke(method, args));
      }
 
      /// <summary>
      /// Waits until the process started by calling <see cref="M:System.ComponentModel.ISynchronizeInvoke.BeginInvoke(System.Delegate,System.Object[])"/> completes, and then returns the value generated by the process.
      /// </summary>
      /// <returns>
      /// An <see cref="T:System.Object"/> that represents the return value generated by the asynchronous operation.
      /// </returns>
      /// <param name="result">An <see cref="T:System.IAsyncResult"/> interface that represents the asynchronous operation started by calling <see cref="M:System.ComponentModel.ISynchronizeInvoke.BeginInvoke(System.Delegate,System.Object[])"/>. </param>
      public Object EndInvoke(IAsyncResult result) {
         return (TargetInvoke.EndInvoke(result));
      }
 
      /// <summary>
      /// Synchronously executes the delegate on the thread that created this Object and marshals the call to the creating thread.
      /// </summary>
      /// <returns>
      /// An <see cref="T:System.Object"/> that represents the return value from the delegate being invoked, or null if the delegate has no return value.
      /// </returns>
      /// <param name="method">A <see cref="T:System.Delegate"/> that contains a method to call, in the context of the thread for the control. </param><param name="args">An array of type <see cref="T:System.Object"/> that represents the arguments to pass to the given method. This can be null if no arguments are needed. </param>
      public Object Invoke(Delegate method, Object[] args) {
         IAsyncResult asyncResult = null;
         bool doBeginInvoke;
         ContextCall contextCall;
 
         lock (_thread) {
            doBeginInvoke = Callbacks.Count == 0;
 
            if (doBeginInvoke)
               asyncResult = TargetInvoke.BeginInvoke(method, args);
 
            contextCall = new ContextCall(method, args, asyncResult);
            Invokes.Push(contextCall);
            if (!doBeginInvoke) {
               PerformInvoke.Set();
            }
         }
 
         ContextCall callback;
         
         var waitHandles = new[] { PerformCallback, contextCall.AsyncWaitHandle };
 
         for (int wh = WaitHandle.WaitAny(waitHandles); wh == 0; wh = WaitHandle.WaitAny(waitHandles)) {
            // No need to lock here - another thread is doing the same loop
            callback = Callbacks.Peek();
            callback.PerformNoThrow();
            Callbacks.Pop();
         }
         
         lock (_thread) {
            if (doBeginInvoke) {
               Invokes.Pop();
            }
            // Got one more callback            
            callback = PerformCallback.WaitOne(0) ? Callbacks.Pop() : null;
         }
 
         if (callback != null) {
            callback.PerformNoThrow();            
         }
 
         return (doBeginInvoke ? TargetInvoke.EndInvoke(contextCall.AsyncResult) : contextCall.Result);
      }
 
      /// <summary>
      /// Gets a value indicating whether the caller must call <see cref="M:System.ComponentModel.ISynchronizeInvoke.Invoke(System.Delegate,System.Object[])"/> when calling an Object that implements this interface.
      /// </summary>
      /// <returns>
      /// true if the caller must call <see cref="M:System.ComponentModel.ISynchronizeInvoke.Invoke(System.Delegate,System.Object[])"/>; otherwise, false.
      /// </returns>
      public bool InvokeRequired {
         get { return (TargetInvoke.InvokeRequired); }
      }
 
      #endregion
 
      private void ExecutionContextRoutine() {
         try {
            _contextInvoke = new SyncControl("Context");
            _contextInitialized.Set();
            Application.Run();
         }
         catch (Exception ex) {
            _contextException = ex;
         }
      }
 
      #region Nested type: ContextCall
 
      private class ContextCall : IAsyncResult
      {
         public readonly IAsyncResult AsyncResult;
         private readonly Object[] _args;
         private readonly Delegate _method;
         private AutoResetEvent _asyncWaitHandle;
         private bool _isCompleted;
         private Object _result;
         private Exception _exception;
 
         public ContextCall(Delegate method, Object[] args, IAsyncResult asyncResult) {
            _method = method;
            _args = args;
            AsyncResult = asyncResult;
         }
 
         public Object Result {
            get {
               if (_exception != null)
                  throw _exception;
               return (_result);
            }
         }
 
         #region Implementation of IAsyncResult
 
         /// <summary>
         /// Gets a value that indicates whether the asynchronous operation has completed.
         /// </summary>
         /// <returns>
         /// true if the operation is complete; otherwise, false.
         /// </returns>
         public bool IsCompleted {
            get { return (AsyncResult.IsCompleted); }
         }
 
         /// <summary>
         /// Gets a <see cref="T:System.Threading.WaitHandle"/> that is used to wait for an asynchronous operation to complete.
         /// </summary>
         /// <returns>
         /// A <see cref="T:System.Threading.WaitHandle"/> that is used to wait for an asynchronous operation to complete.
         /// </returns>
         public WaitHandle AsyncWaitHandle {
            get {
               lock (this) {
                  if (AsyncResult != null)
                     return (AsyncResult.AsyncWaitHandle);
                  if (_asyncWaitHandle == null)
                     _asyncWaitHandle = new AutoResetEvent(_isCompleted);
                  return (_asyncWaitHandle);
               }
            }
         }
 
         /// <summary>
         /// Gets a user-defined object that qualifies or contains information about an asynchronous operation.
         /// </summary>
         /// <returns>
         /// A user-defined object that qualifies or contains information about an asynchronous operation.
         /// </returns>
         public Object AsyncState {
            get { return (AsyncResult.AsyncState); }
         }
 
         /// <summary>
         /// Gets a value that indicates whether the asynchronous operation completed synchronously.
         /// </summary>
         /// <returns>
         /// true if the asynchronous operation completed synchronously; otherwise, false.
         /// </returns>
         public bool CompletedSynchronously {
            get { return (AsyncResult.CompletedSynchronously); }
         }
 
         #endregion
 
         public void PerformNoThrow() {
            try {
               _result = _method.DynamicInvoke(_args);
            }
            catch (Exception ex) {
               _exception = ex;
            }
 
            _isCompleted = true;
 
            if (AsyncResult == null && _asyncWaitHandle != null) {
               _asyncWaitHandle.Set();
            }
         }
      }
 
      #endregion
 
      #region Nested type: SyncControl
 
      [DebuggerDisplay("SyncType = {SyncType}")]
      private class SyncControl : Control
      {
         public readonly String SyncType;
 
         public SyncControl(String syncType) {
            SyncType = syncType;
            CreateHandle();
         }
      }
 
      #endregion
   }
}

Он создает выделенный поток, окна для пробрасывания вызовов, а также является контекстно-зависимой реализацией ISynchronizeInvoke. Во время выполнения синхронной операции происходит ожидание синхронных обратных вызовов и их рекурсивное выполнение.

Класс NETFormController теперь выглядит очень просто:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
 
namespace NETLibrary
{
   [ComVisible(false)]
   [ClassInterface(ClassInterfaceType.None)]
   [ComSourceInterfaces(typeof(INETFormControllerCallback))]
   class NETFormController : UIExecutionContext, INETFormController
   {
      public NETFormController() {
         Invoke((MethodInvoker)InitializeForm, null);
      }
 
      public void AddCallback(INETFormControllerCallback callback) {
         Shown += callback.Shown;
         ButtonClicked += callback.ButtonClicked;
         Notification += callback.Notification;
      }
 
      public void Show() {
         Invoke((MethodInvoker)delegate { _netForm.Show(); }, null);
      }
 
      public void Close() {
         Invoke((MethodInvoker)delegate { _netForm.Close(); }, null);
      }
 
      public void RequestNotification() {
         Invoke((MethodInvoker)delegate { FireNotification(this, EventArgs.Empty); }, null);
      }
 
      public event Action Shown;
      public event Action ButtonClicked;
      public event Action Notification;
 
      private void InitializeForm() {
         _netForm = new NETForm();
         _netForm.Shown += FireShown;
         _netForm.ButtonClicked += FireButtonClicked;
      }
 
      private void FireShown(Object sender, EventArgs args) {
         if (Shown != null) {
            Invoke(Shown, null);
         }
      }
 
      private void FireButtonClicked(Object sender, EventArgs args) {
         if (ButtonClicked != null) {
            Invoke(ButtonClicked, null);
         }
      }
 
      private void FireNotification(Object sender, EventArgs args) {
         if (Notification != null) {
            Invoke(Notification, null);
         }
      }
 
      NETForm _netForm;
   }
}

Я слегка изменил код чтобы продемонстрировать работающую реентрабельность:

class ATL_NO_VTABLE CNETFormControllerCallback : public ATL::CComObjectRootEx<CComSingleThreadModel>,
                                                 public NETLibrary::INETFormControllerCallback
{
 
public:
   CNETFormControllerCallback() : _zNesting(0) { }
 
protected:
   DECLARE_PROTECT_FINAL_CONSTRUCT()
   BEGIN_COM_MAP(CNETFormControllerCallback)
      COM_INTERFACE_ENTRY(NETLibrary::INETFormControllerCallback)
   END_COM_MAP()
 
public:
   STDMETHOD(raw_Shown)() {
      OutputDebugString(_T("Shown\n"));
      return(S_OK);
   }
 
   STDMETHOD(raw_ButtonClicked)() {
      OutputDebugString(_T("ButtonClicked\n"));
      g_pNETObj->RequestNotification();
      return(S_OK);
   }
 
   STDMETHOD(raw_Notification)() {
      OutputDebugString(_T("Notification\n"));
      if (++_zNesting <= 10)
         g_pNETObj->RequestNotification();
      --_zNesting;
      return(S_OK);
   }
 
private:
   size_t _zNesting;
 
};

При нажатии на кнопку происходит десять рекурсивных вызовов и, соответственно, появляется 10 сообщений в окне отладки.

Окончательный вариант исходно когда можно скачать здесь.

Copyright 2007-2011 Chabster