2012-12-09总结:
我打算使用的实际CLR托pipe可执行文件与此问题中列出的可执行文件非常相似,只是进行了一些小的更改:
OPR_FinalizerRun
设置为某个值(目前为60秒,但可能会更改)。 mscoree.dll
dynamicCLRCreateInstance
(以便在安装兼容CLR时允许更好的错误消息)。 Main
函数。 感谢所有花时间阅读问题和/或评论的人。
2012-12-02更新在post底部。
我正在使用Visual Studio 2012与.NET 4混合模式C ++ / CLI应用程序,惊奇地发现一些本地全局对象的析构函数没有被调用。 调查这个问题,事实certificate,他们的行为像本文所解释的托pipe对象。
我对这种行为感到非常惊讶(我理解它是用于pipe理对象的),无论是在C ++ / CLI标准还是在析构函数和终结器的描述中,都找不到任何地方。
在Hans Passant的评论中,我把这些程序编译成一个程序集DLL,并把它存放在一个小的本地可执行文件中,它给了我所期望的行为(析构函数有足够的时间在同一个线程中完成并运行build)!
我的问题:
ICLRPolicyManager->SetTimeout(OPR_ProcessExit, INFINITE)
)为可执行文件? 这将是一个可接受的解决方法。 重新编译下面的文件如下:
cl /EHa /MDd CLRHost.cpp cl /EHa /MDd /c Native.cpp cl /EHa /MDd /c /clr CLR.cpp link /out:CLR.exe Native.obj CLR.obj link /out:CLR.dll /DLL Native.obj CLR.obj
不想要的行为:
C:\Temp\clrhost>clr.exe [1210] Global::Global() [d10] Global::~Global() C:\Temp\clrhost>
运行托pipe:
C:\Temp\clrhost>CLRHost.exe clr.dll [1298] Global::Global() 2a returned. [1298] Global::~Global() [1298] Global::~Global() - Done! C:\Temp\clrhost>
使用的文件:
// CLR.cpp public ref class T { static int M(System::String^ arg) { return 42; } }; int main() {} // Native.cpp #include <windows.h> #include <iostream> #include <iomanip> using namespace std; struct Global { Global() { wcout << L"[" << hex << GetCurrentThreadId() << L"] Global::Global()" << endl; } ~Global() { wcout << L"[" << hex << GetCurrentThreadId() << L"] Global::~Global()" << endl; Sleep(3000); wcout << L"[" << hex << GetCurrentThreadId() << L"] Global::~Global() - Done!" << endl; } } g; // CLRHost.cpp #include <windows.h> #include <metahost.h> #pragma comment(lib, "mscoree.lib") #include <iostream> #include <iomanip> using namespace std; int wmain(int argc, const wchar_t* argv[]) { HRESULT hr = S_OK; ICLRMetaHost* pMetaHost = 0; ICLRRuntimeInfo* pRuntimeInfo = 0; ICLRRuntimeHost* pRuntimeHost = 0; wchar_t version[MAX_PATH]; DWORD versionSize = _countof(version); if (argc < 2) { wcout << L"Usage: " << argv[0] << L" <assembly.dll>" << endl; return 0; } if (FAILED(hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost)))) { goto out; } if (FAILED(hr = pMetaHost->GetVersionFromFile(argv[1], version, &versionSize))) { goto out; } if (FAILED(hr = pMetaHost->GetRuntime(version, IID_PPV_ARGS(&pRuntimeInfo)))) { goto out; } if (FAILED(hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pRuntimeHost)))) { goto out; } if (FAILED(hr = pRuntimeHost->Start())) { goto out; } DWORD dwRetVal = E_NOTIMPL; if (FAILED(hr = pRuntimeHost->ExecuteInDefaultAppDomain(argv[1], L"T", L"M", L"", &dwRetVal))) { wcerr << hex << hr << endl; goto out; } wcout << dwRetVal << " returned." << endl; if (FAILED(hr = pRuntimeHost->Stop())) { goto out; } out: if (pRuntimeHost) pRuntimeHost->Release(); if (pRuntimeInfo) pRuntimeInfo->Release(); if (pMetaHost) pMetaHost->Release(); return hr; }
2012-12-02 :
据我所知,行为似乎如下:
/clr
编译的文件中的非引用类)的时间析构函数可以使用ICLRPolicyManager->SetTimeout(OPR_ProcessExit, <timeout>)
。 有一个猜测,我认为全局本地构造函数/析构函数在DLL场景中“正常”(定义为行为与我所期望的一样)的原因是允许在本机函数上使用LoadLibrary
和GetProcAddress
。 因此,我希望在可以预见的将来,依靠它是不会改变的,是相对安全的,但是希望能以任何方式从官方来源/文件中得到某种确认/否认。
更新2 :
在Visual Studio 2012中(使用快速和高级版本进行testing,我不幸无法访问此机器上的早期版本)。 它应该以相同的方式在命令行(build立如上所述),但是这里是如何从IDE内重现。
build立CLRHost.exe:
build立CLR.DLL:
在Global的析构函数中放置一个断点给出下面的堆栈跟踪:
> clr.dll!Global::~Global() Line 11 C++ clr.dll!`dynamic atexit destructor for 'g''() + 0xd bytes C++ clr.dll!_CRT_INIT(void * hDllHandle, unsigned long dwReason, void * lpreserved) Line 416 C clr.dll!__DllMainCRTStartup(void * hDllHandle, unsigned long dwReason, void * lpreserved) Line 522 + 0x11 bytes C clr.dll!_DllMainCRTStartup(void * hDllHandle, unsigned long dwReason, void * lpreserved) Line 472 + 0x11 bytes C mscoreei.dll!__CorDllMain@12() + 0x136 bytes mscoree.dll!_ShellShim__CorDllMain@12() + 0xad bytes ntdll.dll!_LdrpCallInitRoutine@16() + 0x14 bytes ntdll.dll!_LdrShutdownProcess@0() + 0x141 bytes ntdll.dll!_RtlExitUserProcess@4() + 0x74 bytes kernel32.dll!74e37a0d() mscoreei.dll!RuntimeDesc::ShutdownAllActiveRuntimes() + 0x10e bytes mscoreei.dll!_CorExitProcess@4() + 0x27 bytes mscoree.dll!_ShellShim_CorExitProcess@4() + 0x94 bytes msvcr110d.dll!___crtCorExitProcess() + 0x3a bytes msvcr110d.dll!___crtExitProcess() + 0xc bytes msvcr110d.dll!__unlockexit() + 0x27b bytes msvcr110d.dll!_exit() + 0x10 bytes CLRHost.exe!__tmainCRTStartup() Line 549 C CLRHost.exe!wmainCRTStartup() Line 377 C kernel32.dll!@BaseThreadInitThunk@12() + 0x12 bytes ntdll.dll!___RtlUserThreadStart@8() + 0x27 bytes ntdll.dll!__RtlUserThreadStart@8() + 0x1b bytes
作为一个独立的可执行文件运行,我得到一个与Hans Passant观察到的非常相似的堆栈跟踪(虽然它没有使用CRT的可pipe理版本):
> clrexe.exe!Global::~Global() Line 10 C++ clrexe.exe!`dynamic atexit destructor for 'g''() + 0xd bytes C++ msvcr110d.dll!__unlockexit() + 0x1d3 bytes msvcr110d.dll!__cexit() + 0xe bytes [Managed to Native Transition] clrexe.exe!<CrtImplementationDetails>::LanguageSupport::_UninitializeDefaultDomain(void* cookie) Line 577 C++ clrexe.exe!<CrtImplementationDetails>::LanguageSupport::UninitializeDefaultDomain() Line 594 + 0x8 bytes C++ clrexe.exe!<CrtImplementationDetails>::LanguageSupport::DomainUnload(System::Object^ source, System::EventArgs^ arguments) Line 628 C++ clrexe.exe!<CrtImplementationDetails>::ModuleUninitializer::SingletonDomainUnload(System::Object^ source, System::EventArgs^ arguments) Line 273 + 0x6e bytes C++ kernel32.dll!@BaseThreadInitThunk@12() + 0x12 bytes ntdll.dll!___RtlUserThreadStart@8() + 0x27 bytes ntdll.dll!__RtlUserThreadStart@8() + 0x1b bytes
首先获得简单的问题:
CLR定制的一个很好的资源是Steven Pratschner的书 “自定义Microsoft .NET Framework公共语言运行时”。 注意它已经过时了,.NET 4.0中的主机接口已经改变了。 MSDN并没有多说这个,但是托管接口已经有很好的文档了。
您可以通过更改调试器设置使调试更简单,将Type从“Auto”更改为“Managed”或“Mixed”。
请注意,您的3000毫秒睡眠只是在边缘,你应该测试5000毫秒。 如果C ++类出现在使用/ clr生效的代码中, 即使#pragma是非托管的,你也需要重载终结器线程超时。 在.NET 3.5 SP1 CLR版本上测试,下面的代码运行良好,给析构函数足够的时间运行完成:
ICLRControl* pControl; if (FAILED(hr = pRuntimeHost->GetCLRControl(&pControl))) { goto out; } ICLRPolicyManager* pPolicy; if (FAILED(hr = pControl->GetCLRManager(__uuidof(ICLRPolicyManager), (void**)&pPolicy))) { goto out; } hr = pPolicy->SetTimeout(OPR_FinalizerRun, 60000); pPolicy->Release(); pControl->Release();
我在合理的时间选了一分钟,根据需要调整。 请注意MSDN文档有一个错误,它不显示OPR_FinalizerRun作为允许的值,但它确实工作正常。 设置终结器线程超时还可以确保托管终结器在间接破坏非托管C ++类(一种非常常见的情况)时不会超时。
当你用CLRHost编译/ clr运行这个代码时,你会看到一个事情,就是对GetCLRManager()的调用将失败,并返回一个HOST_E_INVALIDOPERATION返回码。 被装载来执行CLRHost.exe的默认CLR主机不会让你重写策略。 所以你坚持有一个专门的EXE来托管CLR。
当我通过使用CLRHost加载混合模式程序集来测试这个时,调用堆栈在析构函数上设置断点时看起来像这样:
CLRClient.dll!Global::~Global() Line 24 C++ [Managed to Native Transition] CLRClient.dll!<modulee>.?A0x789967ab.??__Fg@@YMXXZ() + 0x1b bytes CLRClient.dll!_exit_callback() Line 449 C++ CLRClient.dll!<CrtImplementationDetails>::LanguageSupport::_UninitializeDefaultDomain(void* cookie = <undefined value>) Line 753 C++ CLRClient.dll!<CrtImplementationDetails>::LanguageSupport::UninitializeDefaultDomain() Line 775 + 0x8 bytes C++ CLRClient.dll!<CrtImplementationDetails>::LanguageSupport::DomainUnload(System::Object^ source = 0x027e1274, System::EventArgs^ arguments = <undefined value>) Line 808 C++ msvcm90d.dll!<CrtImplementationDetails>.moduleeUninitializer.SingletonDomainUnload(object source = {System.AppDomain}, System.EventArgs arguments = null) + 0xa1 bytes // Rest omitted
请注意,这不像你的问题中的意见。 该代码由CRT的管理版本(msvcm90.dll)触发。 而这段代码运行在一个专用的线程上,由CLR启动来卸载一个appdomain。 你可以在vc / crt / src / mstartup.cpp源代码文件中看到这个源代码。
第二种情况发生在C ++类是编译时没有/ clr的源代码文件的一部分,并被链接到混合模式程序集中。 编译器然后使用正常的atexit()处理程序来调用析构函数,就像通常在非托管可执行文件中所做的那样。 在这种情况下,DLL在程序终止时被Windows卸载,并且管理的CRT版本关闭。
值得注意的是,这发生在 CLR关闭之后 ,并且析构函数在程序的启动线程上运行。 因此,CLR超时在图片之外,并且析构函数可以花费尽可能长的时间。 现在堆栈跟踪的本质是:
CLRClient.dll!Global::~Global() Line 12 C++ CLRClient.dll!`dynamic atexit destructor for 'g''() + 0xd bytes C++ // Confusingly named functions elided //... CLRHost.exe!__crtExitProcess(int status=0x00000000) Line 732 C CLRHost.exe!doexit(int code=0x00000000, int quick=0x00000000, int retcaller=0x00000000) Line 644 + 0x9 bytes C CLRHost.exe!exit(int code=0x00000000) Line 412 + 0xd bytes C // etc..
然而,这只是在启动EXE不受管理的情况下才会出现的情况。 一旦EXE被管理,它将在AppDomain.Unload上运行析构函数,即使它们出现在编译时没有/ clr的代码中。 所以你仍然有超时问题。 拥有一个非托管的EXE并不是很少见,例如,当你加载[ComVisible]托管代码时会发生这种情况。 但是,这听起来不像你的情况,你坚持与CLRHost。
为了回答“这个记录在哪里/我怎样才能更多地了解这个话题?” 问题:如果您从这里下载并查看共享源公共语言基础结构 (又名SSCLI),则可以理解这是如何工作的(或者至少用于框架2的工作) http://www.microsoft.com/zh-cn/ us / download / details.aspx?id = 4917 。
一旦你提取了这些文件,你会在gcEE.ccp
(“垃圾回收执行引擎”)中找到这个:
#define FINALIZER_TOTAL_WAIT 2000
它定义了这个着名的默认值2秒。 你也将在同一个文件中看到这个:
BOOL GCHeap::FinalizerThreadWatchDogHelper() { // code removed for brevity ... DWORD totalWaitTimeout; totalWaitTimeout = GetEEPolicy()->GetTimeout(OPR_FinalizerRun); if (totalWaitTimeout == (DWORD)-1) { totalWaitTimeout = FINALIZER_TOTAL_WAIT; }
这将告诉您执行引擎将遵循OPR_FinalizerRun
策略(如果已定义),它们对应于EClrOperation枚举中的值。 GetEEPolicy在eePolicy.h
& eePolicy.cpp
定义。