Introduction
In this article, I would like to discuss a solution to solve a hang issue caused by connection point between .NET and native COM object.
In short, the hang issue is caused by garbage collection do not release native COM interface (RCW), but the native COM object is waiting for all references to be released.
Paresh Suthar's blog provides one solution, which could call Marshal.ReleaseComObject
in .NET code, to release the RCW to avoid the problem, for details, see this link.
But, if we could not change .NET code, we may call CoEEShutDownCom
to release all RCW references. Adam Nathan only talks about the possibility of this solution (.NET and COM: The Complete Interoperability Guide), but does not give out sample. Especially, in .NET 4.0, the global function CoEEShutDownCom
is deprecated. We need some work to get the solution.
Background - RCW
RCW definition could be found here.
In our case, the RCW object is the wrapper of native sink object.
How to Reproduce the Problem
If you have an MFC application (as COM client) calling a COM server implemented in C#, the COM server supports connection point, by C# propery ComSourceInterfaces
, you may reproduce the problem while exiting.
Paresh Suthar did a very good job of explaining what will be held if you use ComSourceInterfaces
property, see the blog url in the Introduction section. When the native application is MFC, the issue becomes more serious. The application may hang when attempting to exit. (In fact, if you make a low memory event, which will cause a gather collection operation, the application will exit. But making a low memory event is not a stable work.)
The exit hang is caused by the AfxOleCanExitApp()
check by MFC framework. In the MFC world, it is common to call AfxOleLockApp
in COM object's constructor and AfxOleUnlockApp
in destructor. In our case, the AfxOleUnlockApp
is not called because the sink object is not released, the latest reference of sink object is in RCW.
The following sample code could show the exit hang issue,
class Sink : public EventInterfac
{
public:
Sink()
{
AfxOleLockApp();
}
~Sink()
{
AfxOleUnlockApp();
}
public:
HRESULT STDMETHODCALLTYPE QueryInterface(const IID &iid, void **ppvObj)
{
if (iid == __uuidof(IUnknown) || iid == __uuidof(IDispatch) || iid == __uuidof(EventInterfac))
{
*ppvObj = this;
this->AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef(void)
{
m_dwRef++;
return m_dwRef;
}
ULONG STDMETHODCALLTYPE Release(void)
{
m_dwRef--;
if (m_dwRef == 0)
{
delete this;
return 0;
}
return m_dwRef;
}
HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT *){ return E_NOTIMPL; }
HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT, LCID, ITypeInfo **){ return E_NOTIMPL; }
HRESULT STDMETHODCALLTYPE GetIDsOfNames
(const IID &, LPOLESTR *, UINT, LCID, DISPID *){ return E_NOTIMPL; }
HRESULT STDMETHODCALLTYPE Invoke
(DISPID, const IID &, LCID, WORD, DISPPARAMS *, VARIANT *, EXCEPINFO *, UINT *)
{
return S_OK;
}
public:
DWORD m_dwRef = 1;
};
int _tmain(int argc, _TCHAR* argv[])
{
Sink* pSink = new Sink;
CoInitialize(NULL);
IManagedInterface* cpi;
HRESULT hResult = CoCreateInstance(__uuidof(ComServer),
NULL, CLSCTX_INPROC_SERVER,
__uuidof(IManagedInterface), reinterpret_cast<void**>(&cpi));
DWORD dwCookie = -1;
IConnectionPointContainerPtr pCPContainer;
hResult = cpi->QueryInterface(IID_IConnectionPointContainer,
(void**)&pCPContainer);
if (SUCCEEDED(hResult))
{
IConnectionPointPtr pEventCP = NULL;
hResult = pCPContainer->FindConnectionPoint(__uuidof(EventInterfac),
&pEventCP);
if (SUCCEEDED(hResult))
{
pEventCP->Advise(pSink, &dwCookie);
}
pEventCP->Release();
pEventCP = NULL;
}
for (int i = 0; i < 100; i++)
{
CComBSTR bstrTest(L"Test");
cpi->PrintHi((BSTR)bstrTest);
}
IConnectionPointPtr pEventCP = NULL;
hResult = pCPContainer->FindConnectionPoint(__uuidof(EventInterfac),
&pEventCP);
if (SUCCEEDED(hResult))
{
pEventCP->Unadvise(dwCookie);
}
pEventCP->Release();
pEventCP = NULL;
pSink->Release();
while (!AfxOleCanExitApp())
{
Sleep(1000);
}
CoUninitialize();
return 0;
}
The C# COM server object source code is as follows:
namespace CSharpComwithCP
{
[ComVisible(true)]
[Guid("901EE2A0-C47C-43ec-B433-985C02004321")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface EventInterfac
{
[DispId(1)]
void Hello();
}
[ComVisible(true)]
[Guid("DBE0E8C4-1C61-41f3-B6A4-4E2F35354321")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IManagedInterface
{
int PrintHi(string name);
}
[ComVisible(true),
ComSourceInterfaces(typeof(EventInterfac)),
ClassInterface(ClassInterfaceType.None),
Guid("80B59B58-98EA-303C-BE83-D26E5D8D4321")]
public class ComServer : IManagedInterface
{
public void FireHelloEvent()
{
if (null != _Hello)
_Hello();
}
public int PrintHi(string name)
{
Func1();
return name.Length;
}
[ComVisible(false)]
public delegate void HelloEventHandler();
public event HelloEventHandler Hello
{
add { _Hello += value; }
remove
{
_Hello -= value;
}
}
private event HelloEventHandler _Hello = null;
}
}
These are only samples, in the real world, if you are implementing ActiveX container by MFC and implementing ActiveX by C#, you will encounter the same problem.
Solution
In my real case, I could not take Paresh Suthar's suggestion to change .NET code (they are my customers' source), I could only change native sink object. After deep search on Google and read code, I believe Adam Nathan's suggestion, use CoEEShutDownCom
to release all RCW's reference should be the solution.
"Calling CoEEShutDownCOM forces the CLR to release all interface pointers, it holds onto inside RCWs. This method usually doesn’t need to be called, but can be necessary if you’re running leak-detection code or somehow depend on all interface pointers being released before process tremination."
-- .NET and COM - The Complete Interoperability Guide by Adam Nathan.
Then, I checked MSDN, the global CoEEShutDownCOM
has deprecated. We need to use the following code to implement a global function in the natvie world.
void CoEEShutdownCOM()
{
ULONG aFetched = 1;
ICLRMetaHost *meta = NULL;
ICLRRuntimeInfo *info = NULL;
HRESULT hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (void **)&meta);
IEnumUnknown * pRtEnum = NULL;
DWORD Aid = 0;
ULONGLONG bytes = 0;
ULONG fetched = 0;
hr = meta->EnumerateLoadedRuntimes(GetCurrentProcess(), &pRtEnum);
while ((hr = pRtEnum->Next(1, (IUnknown **)&info, &fetched)) == S_OK && fetched > 0)
{
void (*pfunc)();
info->GetProcAddress("CoEEShutDownCOM", (void**)&pfunc);
if (pfunc)
{
pfunc();
}
info->Release();
}
pRtEnum->Release();
meta->Release();
}
Call this function before AfxOleCanExitApp()
check, world saved.
Discussion
Even the problem could be solved by this function, but it is not perfect. When exiting, we are forcing all RCW object references to be released. Maybe, some references are still needed by someone. Before applying this solution, we need to consider the possibilities.
I am wondering, if we have a method (in the native world) to release one specific RCW object, which we know is our sink's wrapper, will be perfect solution. But I don't find any solution on this, even it is possible or not. You are very welcome to provide comments on this method. Or any other suggestions?
Conclusion
In this article, I gave out the sample code to reproduce the hang problem caused by connection point technology betwen .NET and native sink object. And provided solution codes.
.NET solution could be found in Paresh Suthar's blog.
Native solution is first mentioned by Adam Nathan, I only provide the sample code, in .NET Framework 4.0.
History
- April 23, 2016 - First version