使用靜態儲存持續時間銷毀本機對象
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 和高級版本進行測試,很遺憾我無法訪問這台機器上的早期版本)。它應該在命令行上以相同的方式工作(如上所述建構),但這裡是如何從 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
首先解決簡單的問題:
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 困住了。