2012-12-09摘要:
我计划使用的实际CLR托管可执行文件与此问题中概述的非常类似,除了一些小的更改:
OPR_FinalizerRun
设置为某个值(目前为60秒,但可能会有变化)。CLRCreateInstance
来自mscoree.dll
(当没有安装兼容的CLR时,允许更好的错误消息)。Main
函数。感谢所有花时间阅读问题和/或评论的人。
2012-12-02帖子底部的更新。
我正在使用带有.NET 4的Visual Studio 2012处理混合模式C ++ / CLI应用程序,并且惊讶地发现某些本地全局对象的析构函数未被调用。调查问题后发现它们的行为与托管对象相同,如this post中所述。
我对这种行为感到非常惊讶(我对托管对象理解它)并且无法在C++/CLI standard或destructors and finalizers的描述中找到任何记录。
根据Hans Passant的评论中的建议,我将程序编译为程序集DLL并将其托管在一个小的本机可执行文件中,这确实给了我所需的行为(析构函数有足够的时间来完成和运行与他们构造的线程相同)!
我的问题:
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>
运行托管:
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月2日:
据我所知,行为似乎如下:
/clr
来控制全局托管对象的时间析构函数(放置在使用ICLRPolicyManager->SetTimeout(OPR_ProcessExit, <timeout>)
编译的文件中的非ref类)。 有一种猜测,我认为全球本土构造函数/析构函数运行的原因通常是&#34;在DLL场景中(定义为我期望的行为)是允许在本机函数上使用LoadLibrary
和GetProcAddress
。因此,我希望在可预见的未来依靠它不会发生变化是相对安全的,但不管怎样,我都希望从官方消息/文件中得到某种确认/否认。
更新2 :
在Visual Studio 2012中(使用快速版和高级版测试,我很遗憾无法访问此计算机上的早期版本)。它应该在命令行上以相同的方式工作(如上所述构建),但这里是如何从IDE中重现的。
构建CLRHost.exe:
构建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的托管版本):
> 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
答案 0 :(得分:9)
首先解决简单的问题:
CLR自定义的一个很好的资源是Steven Pratschner's book“自定义Microsoft .NET Framework公共语言运行时”。请注意它已过时,主机接口在.NET 4.0中已更改。 MSDN对此并没有太多说明,但主机接口已有详细记录。
您可以通过更改调试器设置使调试更简单,将类型从“自动”更改为“管理”或“混合”。
请注意,您的3000毫秒睡眠就在边缘,您应该测试5000毫秒。如果C ++类出现在使用/ clr生效的代码中,即使#pragma unmanaged生效,那么您还需要覆盖终结器线程超时。在.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 ++类时不会超时,这是一种非常常见的情况。
使用/ clr编译的CLRHost运行此代码时,您将看到的一件事是,对GetCLRManager()的调用将失败并返回HOST_E_INVALIDOPERATION返回码。加载执行CLRHost.exe的默认CLR主机不允许您覆盖策略。所以你很难有一个专门的EXE来主持CLR。
当我通过让CLRHost加载混合模式程序集来测试它时,在析构函数上设置断点时调用堆栈看起来像这样:
CLRClient.dll!Global::~Global() Line 24 C++
[Managed to Native Transition]
CLRClient.dll!<Module>.?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>.ModuleUninitializer.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()处理程序来调用析构函数,就像它通常在非托管可执行文件中那样。在这种情况下,当程序终止时Windows被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困住。
答案 1 :(得分:1)
回答“这在哪里记录/我如何更多地了解自己的主题?”问题:如果您从Shared Source Common Language Infrastructure下载并查看http://www.microsoft.com/en-us/download/details.aspx?id=4917(又名SSCLI),您可以了解其工作原理(或至少用于框架2)。
解压缩文件后,您会在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 Enumeration中的值。 GetEEPolicy在eePolicy.h
&amp; eePolicy.cpp
。