C++ and .NET interop
Речь пойдет о механизме и нюансах исполнения .NET кода из C++ и, соответственно, тонкостях взаимодействия неуправляемого кода и управляемого (С++ с CLR).
Сценарий таков: С++ приложение инициализирует CLR engine в своем процессе, загружает .NET сборки и использует .NET объекты. Обращение к методам и свойствам объектов происходит через COM интерфейсы, в нашем варианте другого механизма нет.
Конкретный случай - нужно показать форму .NET и взаимодействовать с ней (вызывать методы и получать уведомления).
Рассмотрим диаграмму классов .NET библиотеки:
Главное действующее лицо - 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 маршаллинг, но это что-то из области сферических коней в вакууме):
- Регистрация сборки утилитой
regasm.exe
- Динамическая регистрация фабрики ProxyStub во время исполнения программы
- Маршаллинг обратных вызовов вручную
Первый способ - это тягомотина с инсталляцией приложения, версионностью, мусором в реестре и т.д. Второй - много геморроя, использование недокументированных функций и т.д. Третий - большое количество ручного кода и сложность отладки, но все же лучше, чем 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 сообщений в окне отладки.
Окончательный вариант исходно когда можно скачать здесь.