25

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 的快速版本中不可用,因此我在本文中省略了它们)。
  • 动态加载(CLRCreateInstancemscoree.dll在未安装兼容的 CLR 时提供更好的错误消息)。
  • 将命令行从主机传递到Main程序集 DLL 中的指定函数。

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


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

我正在使用带有 .NET 4 的 Visual Studio 2012 开发混合模式 C++/CLI 应用程序,并且惊讶地发现一些本机全局对象的析构函数没有被调用。调查问题后发现它们的行为类似于本文中解释的托管对象。

我对这种行为感到非常惊讶(我对托管对象理解它)并且在任何地方都找不到它,无论是在C++/CLI 标准中还是在destructors 和 finalizers的描述中。

按照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 

不受欢迎的行为:

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 场景中“正常”运行(定义为我所期望的行为)的原因是允许在本机函数上使用LoadLibraryGetProcAddress。因此,我希望在可预见的将来依靠它不会改变是相对安全的,但无论哪种方式,都希望得到官方来源/文档的某种确认/否认。

更新 2

在 Visual Studio 2012 中(使用 Express 和 Premium 版本进行测试,遗憾的是我无法访问这台机器上的早期版本)。它应该在命令行上以相同的方式工作(如上所述构建),但这里是如何从 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    

作为一个独立的可执行文件运行,我得到一个与 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    
4

2 回答 2

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();

我选择了一分钟作为合理的时间,必要时进行调整。请注意,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 启动以卸载应用程序域。您可以在 vc/crt/src/mstartup.cpp 源代码文件中查看其源代码。


第二种情况发生在 C++ 类是源代码文件的一部分时,该文件在 /clr 没有生效的情况下编译并链接到混合模式程序集中。然后编译器使用普通的 atexit() 处理程序来调用析构函数,就像它通常在非托管可执行文件中所做的那样。在这种情况下,当 Windows 在程序终止时卸载 DLL 并且 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 困住了。

于 2012-11-29T21:00:12.437 回答
1

回答“这是在哪里记录的/我如何才能对这个主题进行更多的教育?” 问题:如果您从此处下载并查看共享源公共语言基础结构(又名 SSCLI),您可以了解它是如何工作的(或至少在框架 2 中曾经工作过) http://www.microsoft.com/en-我们/下载/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;
    }

这将告诉您执行引擎将遵守与EClrOperation EnumerationOPR_FinalizerRun中的值相对应的策略(如果已定义)。GetEEPolicy 在&中定义。eePolicy.heePolicy.cpp

于 2012-12-03T09:43:51.587 回答