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

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-объектов из потоков, в которых нет работающего цикла сообщений!
Copyright 2007-2011 Chabster