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

Показаны сообщения с ярлыком COM. Показать все сообщения
Показаны сообщения с ярлыком COM. Показать все сообщения

Marshall function call to another thread

Short tutorial on how to make cross apartment (cross thread) COM call without interfaces, proxies, stubs and tlbs.

Each apartment (thread) has its own accesible context object. It can be obtained via CoGetObjectContext API function:

CComPtr<IContext> pContext;   
CComPtr<IContextCallback> pContextCallback;
hr = CoGetObjectContext(IID_PPV_ARGS(&pContext));
hr = CoGetObjectContext(IID_PPV_ARGS(&pContextCallback));

One of interfaces you can get is IContextCallback with only one method - ContextCallback. As you might already guessed, we just need to get IContextCallback in target execution context and invoke ContextCallback method in any other context:

pContextCallback.p->AddRef();
_beginthread(&myThread, 65536, pContextCallback.p);
HANDLE h[] = { CreateEvent(NULL, TRUE, FALSE, NULL) };
DWORD dwIndex;
CoWaitForMultipleHandles(0, INFINITE, _countof(h), h, &dwIndex);
[...]
HRESULT _stdcall contextCall(ComCallData *pParam)
{
   OutputDebugString(_T("contextCall\n"));
   return S_OK;
}

void __cdecl myThread(LPVOID p)
{
   CoInitialize(NULL);

   CComPtr<IContextCallback> pContextCallback;
   pContextCallback.Attach(reinterpret_cast<IContextCallback *>(p));
   
   ComCallData cd = { 0, 0, NULL };
   pContextCallback->ContextCallback(&contextCall, &cd, IID_NULL, 0, NULL);

   CoUninitialize();
}

Here I'm starting a separate thread to make cross context call and using CoWaitForMultipleHandles OLE32 function to trigger message loop to accept cross thread COM calls (regular message loop is surely enough, I'm just reluctant to write one). Here are resulting call stacks:

Main thread:
> Win32ConsoleApplication.exe!contextCall(tagComCallData * pParam) Line 227 C++
  ole32.dll!CRemoteUnknown::DoCallback(struct tagXAptCallback *) Unknown
  rpcrt4.dll!_Invoke@12 () Unknown
  rpcrt4.dll!_NdrStubCall2@16 () Unknown
  ole32.dll!_CStdStubBuffer_Invoke@12 () Unknown
  ole32.dll!SyncStubInvoke(struct tagRPCOLEMESSAGE *,struct _GUID const &,class CIDObject *,void *,struct IRpcChannelBuffer *,struct IRpcStubBuffer *,unsigned long *) Unknown
  ole32.dll!StubInvoke(struct tagRPCOLEMESSAGE *,class CStdIdentity *,struct IRpcStubBuffer *,struct IRpcChannelBuffer *,struct tagIPIDEntry *,unsigned long *) Unknown
  ole32.dll!CCtxComChnl::ContextInvoke(struct tagRPCOLEMESSAGE *,struct IRpcStubBuffer *,struct tagIPIDEntry *,unsigned long *) Unknown
  ole32.dll!MTAInvoke(struct tagRPCOLEMESSAGE *,unsigned long,struct IRpcStubBuffer *,class IInternalChannelBuffer *,struct tagIPIDEntry *,unsigned long *) Unknown
  ole32.dll!STAInvoke(struct tagRPCOLEMESSAGE *,unsigned long,struct IRpcStubBuffer *,class IInternalChannelBuffer *,void *,struct tagIPIDEntry *,unsigned long *) Unknown
  ole32.dll!AppInvoke(class CMessageCall *,class CRpcChannelBuffer *,struct IRpcStubBuffer *,void *,void *,struct tagIPIDEntry *,struct LocalThis *) Unknown
  ole32.dll!ComInvokeWithLockAndIPID(class CMessageCall *,struct tagIPIDEntry *) Unknown
  ole32.dll!ComInvoke(class CMessageCall *) Unknown
  ole32.dll!ThreadDispatch(void *) Unknown
  ole32.dll!ThreadWndProc(struct HWND__ *,unsigned int,unsigned int,long) Unknown
  user32.dll!_InternalCallWinProc@20 () Unknown
  user32.dll!_UserCallWinProcCheckWow@32 () Unknown
  user32.dll!_DispatchMessageWorker@8 () Unknown
  user32.dll!_DispatchMessageW@4 () Unknown
  ole32.dll!CCliModalLoop::PeekRPCAndDDEMessage(void) Unknown
  ole32.dll!CCliModalLoop::FindMessage(unsigned long) Unknown
  ole32.dll!CCliModalLoop::HandleWakeForMsg(void) Unknown
  ole32.dll!CCliModalLoop::BlockFn(void * *,unsigned long,unsigned long *) Unknown
  ole32.dll!_CoWaitForMultipleHandles@20 () Unknown
  Win32ConsoleApplication.exe!wmain(int argc, wchar_t * * argv) Line 284 C++
  Win32ConsoleApplication.exe!__tmainCRTStartup() Line 533 C
  Win32ConsoleApplication.exe!wmainCRTStartup() Line 377 C
  kernel32.dll!@BaseThreadInitThunk@12 () Unknown
  ntdll.dll!___RtlUserThreadStart@8 () Unknown
  ntdll.dll!__RtlUserThreadStart@8 () Unknown
Worker thread:
  ntdll.dll!_NtWaitForMultipleObjects@20 () Unknown
  ntdll.dll!_NtWaitForMultipleObjects@20 () Unknown
  KernelBase.dll!_WaitForMultipleObjectsEx@20 () Unknown
  kernel32.dll!_WaitForMultipleObjectsExImplementation@20 () Unknown
  user32.dll!_RealMsgWaitForMultipleObjectsEx@20 () Unknown
  ole32.dll!CCliModalLoop::BlockFn(void * *,unsigned long,unsigned long *) Unknown
  ole32.dll!ModalLoop(class CMessageCall *) Unknown
  ole32.dll!SwitchSTA(class OXIDEntry *,class CMessageCall * *) Unknown
  ole32.dll!CRpcChannelBuffer::SwitchAptAndDispatchCall(class CMessageCall * *) Unknown
  ole32.dll!CRpcChannelBuffer::SendReceive2(struct tagRPCOLEMESSAGE *,unsigned long *) Unknown
  ole32.dll!CCliModalLoop::SendReceive(struct tagRPCOLEMESSAGE *,unsigned long *,class IInternalChannelBuffer *) Unknown
  ole32.dll!CAptRpcChnl::SendReceive(struct tagRPCOLEMESSAGE *,unsigned long *) Unknown
  ole32.dll!CCtxComChnl::SendReceive(struct tagRPCOLEMESSAGE *,unsigned long *) Unknown
  ole32.dll!NdrExtpProxySendReceive(void *,struct _MIDL_STUB_MESSAGE *) Unknown
  rpcrt4.dll!@NdrpProxySendReceive@4 () Unknown
  rpcrt4.dll!_NdrClientCall2 () Unknown
  ole32.dll!_ObjectStublessClient@8 () Unknown
  ole32.dll!_ObjectStubless@0 () Unknown
  ole32.dll!CObjectContext::InternalContextCallback(long (*)(void *),void *,struct _GUID const &,int,struct IUnknown *) Unknown
  ole32.dll!CObjectContext::ContextCallback(long (*)(struct tagComCallData *),struct tagComCallData *,struct _GUID const &,int,struct IUnknown *) Unknown
> Win32ConsoleApplication.exe!myThread(void * p) Line 239 C++
  msvcr110d.dll!_callthreadstart() Line 255 C
  msvcr110d.dll!_threadstart(void * ptd) Line 239 C
  kernel32.dll!@BaseThreadInitThunk@12 () Unknown
  ntdll.dll!___RtlUserThreadStart@8 () Unknown
 ntdll.dll!__RtlUserThreadStart@8 () Unknown

Windows OS has many hidden features, used by Microsoft products. Are we any worse?

How to force raw pointer marshaling when doing automatic COM marshaling

Иногда требуется запретить выполнять стандартный маршалинг указателя на интерфейс средствами OLE32. Например, я хочу использовать некий интерфейс IFoo из .NET-а, реализация которого попала в CLR из неуправляемого кода, при этом информации в реестре по этому интерфейсу нет (proxy/stub и tlb не зарегистрированы). Как было неоднократно показано в предыдущих заметках о взаимодействии управляемого и неуправляемого кода, при попытке использовать IFoo из другого потока получим исключение.

Избежать автоматического маршалинга указателя на интерфейс можно, самостоятельно реализовав IMarshal. Но тут кроется еще одна засада - метод GetUnmarshalClass обязан возвратить CLSID класса, который будет выполнять роль proxy. И этот класс должен быть зарегистрирован, что нам явно не подходит.

На помощь приходит функция CoCreateFreeThreadedMarshaler, которая выполняет всю грязную работу по получению необходимой функциональности. Результирующий объект нужно подключить, как tear-off реализацию IMarshal к требуемому классу.

RCW vs Garbage Collector

Недавно столкнулись с проблемой - вызов GC.WaitForPendingFinalizers(); блокировал работу приложения. На мысли навела следующая часть стека:

  [In a sleep, wait, or join] 
  ntdll.dll!_KiFastSystemCallRet@0()  
  ntdll.dll!_NtWaitForMultipleObjects@20()  + 0xc bytes 
  KernelBase.dll!_WaitForMultipleObjectsEx@20()  - 0x54 bytes 
  kernel32.dll!_WaitForMultipleObjectsExImplementation@20()  + 0x8e bytes 
  user32.dll!_RealMsgWaitForMultipleObjectsEx@20()  + 0xd7 bytes 
  ole32.dll!CCliModalLoop::BlockFn()  + 0x96 bytes 
  ole32.dll!_CoWaitForMultipleHandles@20()  - 0x51b9 bytes 
  [Managed to Native Transition] 
  mscorlib.dll!System.GC.WaitForPendingFinalizers() + 0x2c bytes 

Блокировка происходит из ole32.dll. После просмотра стеков всех остальных потоков был найден следующий любопытный житель нашего процесса:

> ntdll.dll!_KiFastSystemCallRet@0()  
  ntdll.dll!_ZwWaitForSingleObject@12()  + 0xc bytes 
  KernelBase.dll!_WaitForSingleObjectEx@12()  + 0x6c bytes 
  kernel32.dll!_WaitForSingleObjectExImplementation@12()  + 0x43 bytes 
  kernel32.dll!_WaitForSingleObject@8()  + 0x12 bytes 
  ole32.dll!GetToSTA()  + 0x72 bytes 
  ole32.dll!CRpcChannelBuffer::SwitchAptAndDispatchCall()  - 0x1939 bytes 
  ole32.dll!CRpcChannelBuffer::SendReceive2()  + 0xa6 bytes 
  ole32.dll!CAptRpcChnl::SendReceive()  + 0x5b7 bytes 
  ole32.dll!CCtxComChnl::SendReceive()  - 0x14b97 bytes 
  ole32.dll!NdrExtpProxySendReceive()  + 0x43 bytes 
  rpcrt4.dll!@NdrpProxySendReceive@4()  + 0xe bytes 
  rpcrt4.dll!_NdrClientCall2()  + 0x144 bytes 
  ole32.dll!_ObjectStublessClient@8()  + 0x7a bytes 
  ole32.dll!_ObjectStubless@0()  + 0xf bytes 
  ole32.dll!CObjectContext::InternalContextCallback()  - 0x511f bytes 
  ole32.dll!CObjectContext::ContextCallback()  + 0x8f bytes 
  clr.dll!CtxEntry::EnterContext()  + 0x119 bytes 
  clr.dll!RCWCleanupList::ReleaseRCWListInCorrectCtx()  + 0x2bb bytes 
  clr.dll!RCWCleanupList::CleanupAllWrappers()  - 0x2ef04 bytes 
  clr.dll!SyncBlockCache::CleanupSyncBlocks()  + 0xa6a bytes 
  clr.dll!Thread::DoExtraWorkForFinalizer()  - 0x4c12 bytes 
  clr.dll!WKS::GCHeap::FinalizerThreadWorker()  + 0x8b bytes 
  clr.dll!Thread::DoExtraWorkForFinalizer()  + 0x3e0ff bytes 
  clr.dll!Thread::ShouldChangeAbortToUnload()  - 0x5f4 bytes 
  clr.dll!Thread::ShouldChangeAbortToUnload()  - 0x539 bytes 
  clr.dll!ManagedThreadBase_NoADTransition()  + 0x35 bytes 
  clr.dll!ManagedThreadBase::FinalizerBase()  + 0xf bytes 
  clr.dll!WKS::GCHeap::FinalizerThreadStart()  + 0xfb bytes 
  clr.dll!Thread::intermediateThreadProc()  + 0x48 bytes 
  kernel32.dll!@BaseThreadInitThunk@12()  + 0x12 bytes 
  ntdll.dll!___RtlUserThreadStart@8()  + 0x27 bytes 
  ntdll.dll!__RtlUserThreadStart@8()  + 0x1b bytes 

Перед нами стек так называемого Finalizer Thread, который выполняет работу по освобождению ресурсов. Из стека стало ясно, что этот поток пытается избавиться от ненужных RCW. Для этого он вызывает метод Release каждого COM-объекта, но пытается сделать это в другом контексте. Очевидно, что этот контекст - это апартмент из которого объект попал в CLR. В моем случае поток, соответствующий этому апартменту, не обрабатывал асинхронные сообщения, а именно ими обменивается ole32.dll для маршалинга вызовов между контекстами.

Ахтунг состоит в том, что Finalizer Thread является чужеродным контекстом для всех COM-объектов, которые попали в CLR (за исключением тех, которые были созданы в контексте самого Finalizer Thread, а это вполне реальная ситуация). Как же в результате происходит освобождение ресурсов, если вызов GC.WaitForPendingFinalizers(); блокируется до полной очистки всего накопившегося мусора? Ответ находится на вешине первого стека:

> CPPHost.exe!CNETFormControllerCallback::~CNETFormControllerCallback()  Line 13 C++
  CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::~CComObjectNoLock<CNETFormControllerCallback>()  Line 3037 + 0xf bytes C++
  CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::`scalar deleting destructor'()  + 0x2b bytes C++
  CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::Release()  Line 3049 + 0x35 bytes C++
  CPPHost.exe!ATL::_QIThunk::Release()  Line 2692 + 0x10 bytes C++
  ole32.dll!CRemoteUnknown::DoCallback()  + 0x3b 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 
  ole32.dll!CCliModalLoop::PeekRPCAndDDEMessage()  + 0x241 bytes 
  ole32.dll!CCliModalLoop::BlockFn()  - 0x5d50 bytes 
  ole32.dll!_CoWaitForMultipleHandles@20()  - 0x51b9 bytes 
  [Managed to Native Transition] 
  mscorlib.dll!System.GC.WaitForPendingFinalizers() + 0x2c bytes 
  NETLibrary.dll!NETLibrary.NETFormController.GCAndWaitForPendingFinalizers() Line 33 + 0x5 bytes C#
  [Native to Managed Transition] 
  CPPHost.exe!NETLibrary::INETFormController::GCAndWaitForPendingFinalizers()  Line 67 + 0x10 bytes C++
  CPPHost.exe!wmain(int argc=1, wchar_t * * argv=0x006832d8)  Line 107 C++

Здесь видно, что GC.WaitForPendingFinalizers(); блокирует выполнение потока вызовом CoWaitForMultipleHandles, который крутит цикл сообщений для обработки входящих COM-вызовов. Мощно, да?

Допустим, что COM-объект попал в CLR из потока, отличного от того, в котором происходит вызов GC.WaitForPendingFinalizers();, но сам поток к моменту вызова не может обработать входящий вызов (поток завершил работу, например):

> CPPHost.exe!CNETFormControllerCallback::~CNETFormControllerCallback()  Line 13 C++
  CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::~CComObjectNoLock<CNETFormControllerCallback>()  Line 3037 + 0xf bytes C++
  CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::`scalar deleting destructor'()  + 0x2b bytes C++
  CPPHost.exe!ATL::CComObjectNoLock<CNETFormControllerCallback>::Release()  Line 3049 + 0x35 bytes C++
  CPPHost.exe!ATL::_QIThunk::Release()  Line 2692 + 0x10 bytes C++
  clr.dll!ReleaseTransitionHelper()  + 0xe bytes 
  clr.dll!SafeReleaseHelper()  + 0x6b bytes 
  clr.dll!SafeRelease()  + 0x2f bytes 
  clr.dll!IUnkEntry::Free()  + 0x4e bytes 
  clr.dll!RCW::ReleaseAllInterfaces()  + 0x1a bytes 
  clr.dll!RCW::ReleaseAllInterfacesCallBack()  + 0x178b5c bytes 
  clr.dll!RCW::Cleanup()  + 0x22 bytes 
  clr.dll!RCWCleanupList::ReleaseRCWListRaw()  + 0x18 bytes 
  clr.dll!RCWCleanupList::ReleaseRCWListInCorrectCtx()  + 0x17a15e bytes 
  clr.dll!RCWCleanupList::CleanupAllWrappers()  - 0x2ef04 bytes 
  clr.dll!SyncBlockCache::CleanupSyncBlocks()  + 0xa6a bytes 
  clr.dll!Thread::DoExtraWorkForFinalizer()  - 0x4c12 bytes 
  clr.dll!WKS::GCHeap::FinalizerThreadWorker()  + 0x8b bytes 
  clr.dll!Thread::DoExtraWorkForFinalizer()  + 0x3e0ff bytes 
  clr.dll!Thread::ShouldChangeAbortToUnload()  - 0x5f4 bytes 
  clr.dll!Thread::ShouldChangeAbortToUnload()  - 0x539 bytes 
  clr.dll!ManagedThreadBase_NoADTransition()  + 0x35 bytes 
  clr.dll!ManagedThreadBase::FinalizerBase()  + 0xf bytes 
  clr.dll!WKS::GCHeap::FinalizerThreadStart()  + 0xfb bytes 
  clr.dll!Thread::intermediateThreadProc()  + 0x48 bytes 
  kernel32.dll!@BaseThreadInitThunk@12()  + 0x12 bytes 
  ntdll.dll!___RtlUserThreadStart@8()  + 0x27 bytes 
  ntdll.dll!__RtlUserThreadStart@8()  + 0x1b bytes 

В таком случае COM-объект освобождается прямо в контексте Finalizer Thread, что, понятное дело, может привести к проблемам синхронизации, а то и вовсе катастрофой, если код оъекта был выгружен из памяти!

Последний случай - поток работает, в нем инициализирован апартмент, но он не обрабатывает асинхронные сообщения, Finalizer Thread рано или поздно подвисает...

Какие выводы можно сделать из всего этого?

  • CLR пытается быть a good COM citizen и выполняет автоматический маршалинг вызовов в нужные контексты;
  • из-за специфики среды выполнения разрушение RCW происходит в выделенном потоке, поэтому во время этой операции за кадром выполняется множество действий, которые могут привести к блокировкам;
  • уничтожением RCW Finalizer Thread занимается уже после того, как были вызваны методы Finalize объектов в соответствующей очереди, поэтому во время вызова Finalize можно пользоваться RCW, но очень нежелательно (блокировки, заваленные вызовы QueryInterface если tlb не зарегистрирована и т.д.);
  • блокировать поток желательно вызовом CoWaitForMultipleHandles;
  • Избегать передачи COM-объектов из потоков, в которых нет работающего цикла сообщений!

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