2012-12-09 总结:
- 在普通的混合模式应用程序中,全局本机 C++ 析构函数作为终结器运行。无法更改该行为或相关的超时。
- 混合模式程序集 DLL 在 DLL 加载/卸载期间运行 C++ 构造函数/析构函数 - 与本机 DLL 完全一样。
- 使用 COM 接口将 CLR 托管在本机可执行文件中,可以让解构器像在本机 DLL 中一样运行(我想要的行为)并为终结器设置超时(额外的好处)。
- 据我所知,上述内容至少适用于 Visual Studio 2008、2010 和 2012。(仅使用 .NET 4 测试)
我计划使用的实际 CLR 托管可执行文件与此问题中概述的非常相似,除了一些小的更改:
- 按照 Hans Passant 的建议,设置
OPR_FinalizerRun
为某个值(目前为 60 秒,但可能会发生变化)。 - 使用 ATL COM 智能指针类(这些在 Visual Studio 的快速版本中不可用,因此我在本文中省略了它们)。
- 动态加载(
CLRCreateInstance
以mscoree.dll
在未安装兼容的 CLR 时提供更好的错误消息)。 - 将命令行从主机传递到
Main
程序集 DLL 中的指定函数。
感谢所有花时间阅读问题和/或评论的人。
2012-12-02 在帖子底部更新。
我正在使用带有 .NET 4 的 Visual Studio 2012 开发混合模式 C++/CLI 应用程序,并且惊讶地发现一些本机全局对象的析构函数没有被调用。调查问题后发现它们的行为类似于本文中解释的托管对象。
我对这种行为感到非常惊讶(我对托管对象理解它)并且在任何地方都找不到它,无论是在C++/CLI 标准中还是在destructors 和 finalizers的描述中。
按照Hans Passant评论中的建议,我将程序编译为程序集 DLL 并将其托管在一个小的本机可执行文件中,这确实给了我所需的行为(析构函数有足够的时间来完成并在同一个线程中运行)建造)!
我的问题:
- 我可以在独立的可执行文件中获得相同的行为吗?
- 如果(1)不可行,是否可以
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-02:
据我所知,行为似乎如下:
- 在混合模式的 EXE 文件中,全局析构函数在 DomainUnload 期间作为终结器运行,无论它们是放置在本机代码还是 CLR 代码中。在 Visual Studio 2008、2010 和 2012 中就是这种情况。
- 在本机应用程序托管的混合模式 DLL 中,全局本机对象的析构函数在托管方法运行和所有其他清理发生后的DLL_PROCESS_DETACH 期间运行。它们与构造函数在同一个线程中运行,并且没有与它们关联的超时(所需的行为)。正如预期的那样,可以
/clr
使用ICLRPolicyManager->SetTimeout(OPR_ProcessExit, <timeout>)
.
冒险猜测,我认为全局本机构造函数/析构函数在 DLL 场景中“正常”运行(定义为我所期望的行为)的原因是允许在本机函数上使用LoadLibrary
和GetProcAddress
。因此,我希望在可预见的将来依靠它不会改变是相对安全的,但无论哪种方式,都希望得到官方来源/文档的某种确认/否认。
更新 2:
在 Visual Studio 2012 中(使用 Express 和 Premium 版本进行测试,遗憾的是我无法访问这台机器上的早期版本)。它应该在命令行上以相同的方式工作(如上所述构建),但这里是如何从 IDE 中重现。
构建 CLRHost.exe:
- 文件 -> 新项目
- Visual C++ -> Win32 -> Win32 控制台应用程序(将项目命名为“CLRHost”)
- 应用程序设置 -> 附加选项 -> 空项目
- 按“完成”
- 右键单击解决方案资源管理器中的源文件。添加 -> 新项目 -> Visual C++ -> C++ 文件。将其命名为 CLRHost.cpp 并粘贴帖子中 CLRHost.cpp 的内容。
- 项目-> 属性。配置属性 -> C/C++ -> 代码生成 -> 将“启用 C++ 异常”更改为“有 SEH 异常 (/EHa)”,将“基本运行时检查”更改为“默认”
- 建造。
构建 CLR.DLL:
- 文件 -> 新项目
- Visual C++ -> CLR -> 类库(将项目命名为“CLR”)
- 删除所有自动生成的文件
- 项目-> 属性。配置属性 -> C/C++ -> 预编译头文件 -> 预编译头文件。更改为“不使用预编译头文件”。
- 右键单击解决方案资源管理器中的源文件。添加 -> 新项目 -> Visual C++ -> C++ 文件。将其命名为 CLR.cpp 并从帖子中粘贴 CLR.cpp 的内容。
- 添加一个名为 Native.cpp 的新 C++ 文件并粘贴帖子中的代码。
- 在解决方案资源管理器中右键单击“Native.cpp”并选择属性。将 C/C++ -> 常规 -> 公共语言运行时支持更改为“无公共语言运行时支持”
- 项目-> 属性-> 调试。将“Command”更改为指向 CLRhost.exe,将“Command Arguments”更改为“$(TargetPath)”,包括引号,“Debugger Type”更改为“Mixed”
- 构建和调试。
在 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