使用静态存储持续时间破坏本机对象

use*_*653 24 .net windows clr mixed-mode c++-cli

2012-12-09摘要:

  • 在普通的混合模式应用程序中,全局本机C++析构函数作为终结器运行.无法更改该行为或关联的超时.
  • 混合模式程序集DLL在DLL加载/卸载期间运行C++构造函数/析构函数 - 与本机DLL完全相同.
  • 使用COM接口在本机可执行文件中托管CLR 允许解构器的行为与本机DLL(我想要的行为)一样,并为终结器设置超时(额外的奖励).
  • 据我所知,上述内容至少适用于Visual Studio 2008,2010和2012.(仅用.NET 4测试过)

我计划使用的实际CLR托管可执行文件与此问题中概述的非常类似,除了一些小的更改:

  • OPR_FinalizerRun根据Hans Passant的建议设置为某个值(当前为60秒,但可能会发生变化).
  • 使用ATL COM智能指针类(这些在Visual Studio的快速版本中不可用,所以我在这篇文章中省略了它们).
  • Lodaing CLRCreateInstancemscoree.dll动态(以允许更好的错误消息没有安装兼容CLR时).
  • 将命令行从主机传递到Main程序集DLL中的指定函数.

感谢所有花时间阅读问题和/或评论的人.


2012-12-02帖子底部更新.

我正在使用Visual Studio 2012和.NET 4进行混合模式C++/CLI应用程序,并且惊讶地发现某些本机全局对象的析构函数没有被调用.调查的问题,它原来,他们表现得像管理对象在解释这个职位.

我对此行为感到非常惊讶(我理解它对于托管对象)并且无法在任何地方找到它,无论是在C++/CLI标准中还是在析构函数和终结器的描述中.

根据Hans Passant的评论中的建议,我将程序编译为程序集DLL并将其托管在一个小的本机可执行文件中,这确实给了我所需的行为(析构函数有足够的时间来完成和运行在同一个线程中建造)!

我的问题:

  1. 我可以在独立的可执行文件中获得相同的行为吗?
  2. 如果(1)不可行,是否可以ICLRPolicyManager->SetTimeout(OPR_ProcessExit, INFINITE)为可执行文件配置进程超时策略(即基本上调用)?这将是一个可接受的解决方法.
  3. 在哪里记录/我如何更多地了解这个主题?我宁愿不依赖易于改变的行为.

要重现如下编译以下文件:

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 
Run Code Online (Sandbox Code Playgroud)

不需要的行为:

C:\Temp\clrhost>clr.exe
[1210] Global::Global()
[d10] Global::~Global()

C:\Temp\clrhost>
Run Code Online (Sandbox Code Playgroud)

运行托管:

C:\Temp\clrhost>CLRHost.exe clr.dll
[1298] Global::Global()
2a returned.
[1298] Global::~Global()
[1298] Global::~Global() - Done!

C:\Temp\clrhost>
Run Code Online (Sandbox Code Playgroud)

使用的文件:

// 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;
}
Run Code Online (Sandbox Code Playgroud)

2012-12-02:
据我所知,行为似乎如下:

  • 在混合模式EXE文件中,全局析构函数在DomainUnload期间作为终结器运行,无论它们是放在本机代码还是CLR代码中.Visual Studio 2008,2010和2012就是这种情况.
  • 在由本机应用程序托管的混合模式DLL中,在运行托管方法并且发生所有其他清理之后, DLL_PROCESS_DETACH期间将运行全局本机对象的析构函数.它们在与构造函数相同的线程中运行,并且没有与它们相关联的超时(所需行为).正如预期的那样,可以使用控制全局托管对象的时间析构函数(放置在编译的文件中的非ref类/clr)ICLRPolicyManager->SetTimeout(OPR_ProcessExit, <timeout>).

令人猜测的是,我认为全局本机构造函数/析构函数在DLL场景中"正常"运行(定义为我期望的行为)的原因是允许使用LoadLibraryGetProcAddress使用本机函数.因此,我希望在可预见的未来依赖它不会发生变化是相对安全的,但我希望能够从官方消息/文件中得到某种确认/否认.

更新2:

在Visual Studio 2012中(使用快速版和高级版测试,遗憾的是我无法访问此计算机上的早期版本).它应该在命令行上以相同的方式工作(如上所述构建),但这里是如何从IDE中重现的.

构建CLRHost.exe:

  1. 文件 - >新项目
  2. Visual C++ - > Win32 - > Win32控制台应用程序(将项目命名为"CLRHost")
  3. 应用程序设置 - >其他选项 - >清空项目
  4. 按"完成"
  5. 右键单击解决方案资源管理器中的源文件.添加 - >新建项目 - > Visual C++ - > C++文件.将其命名为CLRHost.cpp并从帖子中粘贴CLRHost.cpp的内容.
  6. 项目 - >属性.配置属性 - > C/C++ - >代码生成 - >将"启用C++异常"更改为"是的SEH异常(/ EHa)"和"基本运行时检查"更改为"默认"
  7. 建立.

构建CLR.DLL:

  1. 文件 - >新项目
  2. Visual C++ - > CLR - >类库(将项目命名为"CLR")
  3. 删除所有自动生成的文件
  4. 项目 - >属性.配置属性 - > C/C++ - >预编译头 - >预编译头.更改为"不使用预编译标题".
  5. 右键单击解决方案资源管理器中的源文件.添加 - >新建项目 - > Visual C++ - > C++文件.将其命名为CLR.cpp并从帖子中粘贴CLR.cpp的内容.
  6. 添加一个名为Native.cpp的新C++文件并粘贴帖子中的代码.
  7. 右键单击解决方案资源管理器中的"Native.cpp"并选择属性.将C/C++ - >常规 - >公共语言运行时支持更改为"无公共语言运行时支持"
  8. 项目 - >属性 - >调试.将"Command"更改为指向CLRhost.exe,将"Command Arguments"更改为"$(TargetPath)",包括引号,"Debugger Type"更改为"Mixed"
  9. 构建和调试.

在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    
Run Code Online (Sandbox Code Playgroud)

作为独立的可执行文件运行,我获得的堆栈跟踪与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    
Run Code Online (Sandbox Code Playgroud)

Han*_*ant 9

首先解决简单的问题:

一个很好的CLR定制资源是Steven Pratschner的书 "自定义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();
Run Code Online (Sandbox Code Playgroud)

我选择了一分钟作为合理的时间,根据需要进行调整.请注意,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
Run Code Online (Sandbox Code Playgroud)

请注意,这与您在问题中的观察结果不同.代码由受管版本的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..
Run Code Online (Sandbox Code Playgroud)

然而,这只是在启动EXE未受管理时才会出现的极端情况.一旦EXE被管理,它将在AppDomain.Unload上运行析构函数,即使它们出现在没有/ clr编译的代码中.所以你仍然有超时问题.拥有非托管EXE并不是很常见,例如当您加载[ComVisible]托管代码时会发生这种情况.但这听起来不像你的情景,你被CLRHost困住了.