Dot-Net

使用靜態儲存持續時間銷毀本機對象

  • November 29, 2012

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 和高級版本進行測試,很遺憾我無法訪問這台機器上的早期版本)。它應該在命令行上以相同的方式工作(如上所述建構),但這裡是如何從 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    

首先解決簡單的問題:

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 困住了。

引用自:https://stackoverflow.com/questions/13632187