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

时间:2012-11-29 18:21:20

标签: .net windows c++-cli clr mixed-mode

2012-12-09摘要:

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

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

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

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


2012-12-02帖子底部的更新。

我正在使用带有.NET 4的Visual Studio 2012处理混合模式C ++ / CLI应用程序,并且惊讶地发现某些本地全局对象的析构函数未被调用。调查问题后发现它们的行为与托管对象相同,如this post中所述。

我对这种行为感到非常惊讶(我对托管对象理解它)并且无法在C++/CLI standarddestructors and finalizers的描述中找到任何记录。

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

我的问题:

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

    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日
    据我所知,行为似乎如下:

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

    有一种猜测,我认为全球本土构造函数/析构函数运行的原因通常是&#34;在DLL场景中(定义为我期望的行为)是允许在本机函数上使用LoadLibraryGetProcAddress。因此,我希望在可预见的未来依靠它不会发生变化是相对安全的,但不管怎样,我都希望从官方消息/文件中得到某种确认/否认。

    更新2

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

    构建CLRHost.exe:

    1. 文件 - &gt;新项目
    2. Visual C ++ - &gt; Win32 - &gt; Win32控制台应用程序(将项目命名为&#34; CLRHost&#34;)
    3. 应用程序设置 - &gt;附加选项 - &gt;空项目
    4. 按&#34;完成&#34;
    5. 右键单击解决方案资源管理器中的源文件。添加 - &gt;新商品 - &gt; Visual C ++ - &gt; C ++文件。将其命名为CLRHost.cpp并从帖子中粘贴CLRHost.cpp的内容。
    6. 项目 - &gt;属性。配置属性 - &gt; C / C ++ - &gt;代码生成 - &gt;更改&#34;启用C ++例外&#34;与SEH例外(/ EHa)&#34;和&#34;基本运行时检查&#34;到&#34;默认&#34;
    7. 构建
    8. 构建CLR.DLL:

      1. 文件 - &gt;新项目
      2. Visual C ++ - &gt; CLR - &gt;类库(将项目命名为&#34; CLR&#34;)
      3. 删除所有自动生成的文件
      4. 项目 - &gt;属性。配置属性 - &gt; C / C ++ - &gt;预编译的标题 - &gt;预编译头文件。更改为&#34;不使用预编译标题&#34;。
      5. 右键单击解决方案资源管理器中的源文件。添加 - &gt;新商品 - &gt; Visual C ++ - &gt; C ++文件。将其命名为CLR.cpp并从帖子中粘贴CLR.cpp的内容。
      6. 添加一个名为Native.cpp的新C ++文件并粘贴帖子中的代码。
      7. 右键单击&#34; Native.cpp&#34;在解决方案资源管理器中并选择属性。改变C / C ++ - &gt;一般 - &gt;公共语言运行时支持&#34;没有公共语言运行时支持&#34;
      8. 项目 - &gt;属性 - &gt;调试。改变&#34;命令&#34;指向CLRhost.exe,&#34; Command Arguments&#34; to&#34; $(TargetPath)&#34;包括引号,&#34;调试器类型&#34;到&#34;混合&#34;
      9. 构建和调试。
      10. 在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    
        

2 个答案:

答案 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