在.NET与原生COM对象交互时,可能会遇到一些棘手的问题,如挂起问题。本文将介绍这类问题产生的原因,并提供相应的解决方案。
挂起问题通常是由于垃圾回收器未能释放原生COM接口(RCW),而原生COM对象却在等待所有引用被释放。Paresh Suthar的博客提供了一种解决方案,即在.NET代码中调用Marshal.ReleaseComObject
来释放RCW,从而避免问题。但若无法修改.NET代码,则可以尝试调用CoEEShutDownCom
来释放所有RCW引用。Adam Nathan在其著作《.NET与COM:完全互操作性指南》中提到了这种可能性,但没有给出示例。特别是在.NET 4.0中,全局函数CoEEShutDownCom
已被弃用,需要一些额外的工作来实现解决方案。
RCW(Runtime Callable Wrapper)是.NET为原生COM对象提供的包装器。在本文的案例中,RCW对象是原生接收器对象的包装器。
如果有一个MFC应用程序(作为COM客户端)调用一个用C#实现的COM服务器,并且COM服务器支持连接点,通过C#属性ComSourceInterfaces
,可以在退出时重现这个问题。Paresh Suthar在其博客中很好地解释了使用ComSourceInterfaces
属性时会发生什么。当原生应用程序是MFC时,问题变得更加严重。应用程序在尝试退出时可能会挂起。(实际上,如果制造一个低内存事件,这将导致垃圾回收操作,应用程序将退出。但制造低内存事件并不是一个稳定的工作。)
退出挂起是由MFC框架的AfxOleCanExitApp()
检查引起的。在MFC世界中,通常在COM对象的构造函数中调用AfxOleLockApp
,在析构函数中调用AfxOleUnlockApp
。在案例中,AfxOleUnlockApp
没有被调用,因为接收器对象没有被释放,接收器对象的最新引用在RCW中。
在实际案例中,无法采纳Paresh Suthar的建议去修改.NET代码(它们是客户的源代码),只能修改原生接收器对象。经过深入的Google搜索和阅读代码,相信Adam Nathan的建议,使用CoEEShutDownCom
来释放所有RCW的引用应该是解决方案。
"调用CoEEShutDownCOM
会强制CLR释放它在RCWs中持有的所有接口指针。通常不需要调用此方法,但如果需要在进程终止前释放所有接口指针,或者运行泄漏检测代码,可能需要调用它。"
然后,查看了MSDN,全局CoEEShutDownCOM
已被弃用。需要使用以下代码在原生世界中实现一个全局函数。
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();
}
在AfxOleCanExitApp()
检查之前调用这个函数,世界得救了。
即使问题可以通过这个函数解决,但并不完美。在退出时,强制释放所有RCW对象引用。也许,一些引用仍然需要。在应用这个解决方案之前,需要考虑可能性。
在想,如果有一种方法(在原生世界中)来释放一个特定的RCW对象,知道它是接收器的包装器,那将是完美的解决方案。但没有找到任何解决方案,即使它是可能的还是不可能。欢迎提供对此方法的评论。或者任何其他建议?
在本文中,提供了重现由连接点技术引起的挂起问题的示例代码。并提供了解决方案代码。