25

2012-12-09 まとめ:

  • 通常の混合モード アプリケーションでは、グローバル ネイティブ C++ デストラクタがファイナライザとして実行されます。その動作または関連するタイムアウトを変更することはできません。
  • 混合モードのアセンブリ DLL は、DLL のロード/アンロード中に、ネイティブ DLL とまったく同じように、C++ コンストラクター/デストラクタを実行します。
  • COM インターフェイスを使用してネイティブ実行可能ファイルで CLR をホストすると、両方のデコンストラクターがネイティブ DLL のように動作し (私が望む動作)、ファイナライザーのタイムアウトを設定することができます (追加ボーナス)。
  • 私が知る限り、上記は少なくとも Visual Studio 2008、2010、および 2012 に適用されます。 (.NET 4 でのみテスト済み)

私が使用する予定の実際の CLR ホスティング実行可能ファイルは、いくつかの小さな変更を除いて、この質問で概説されているものと非常によく似ています。

  • OPR_FinalizerRunHans Passant の提案に従って、ある値 (現在は 60 秒ですが、変更される可能性があります) に設定します。
  • ATL COM スマート ポインター クラスを使用します (これらは Visual Studio のエクスプレス エディションでは使用できないため、この投稿では省略しました)。
  • CLRCreateInstance動的に読み込みmscoree.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 

望ましくない動作:

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 ファイルでは、グローバル デストラクタは、ネイティブ コードまたは CLR コードのどちらに配置されているかに関係なく、 DomainUnload 中にファイナライザとして実行されます。これは、Visual Studio 2008、2010、および 2012 の場合です。
  • ネイティブ アプリケーションによってホストされる混合モード DLL では、マネージ メソッドが実行され、他のすべてのクリーンアップが発生した後、グローバル ネイティブ オブジェクトのデストラクタが DLL_PROCESS_DETACH 中に実行されます。それらはコンストラクターと同じスレッドで実行され、それらに関連付けられたタイムアウトはありません (望ましい動作)。予想どおり、グローバルマネージド オブジェクト( でコンパイルされたファイルに配置された非参照クラス/clr) の時間デストラクタは、 を使用して制御できますICLRPolicyManager->SetTimeout(OPR_ProcessExit, <timeout>)

推測を危険にさらすと、DLL シナリオでグローバル ネイティブ コンストラクター/デストラクタが "正常に" (期待どおりに動作するように定義されている) 機能する理由は、ネイティブ関数の使用LoadLibraryと使用を許可するためだと思いGetProcAddressます。したがって、予見可能な将来に変更されないことに依存することは比較的安全であると予想しますが、いずれにしても、公式の情報源/ドキュメントから何らかの確認/拒否があることを感謝します.

更新 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    

スタンドアロンの実行可能ファイルとして実行すると、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 の著書「Customizing the Microsoft .NET Framework Common Language Runtime」です。.NET 4.0 ではホスティング インターフェイスが変更されているため、古いことに注意してください。MSDN はそれについて多くを語っていませんが、ホスティング インターフェイスは十分に文書化されています。

デバッガーの設定を変更し、タイプを「自動」から「管理」または「混合」に変更することで、デバッグを簡単にすることができます。

3000 ミリ秒のスリープはちょうど端にあることに注意してください。5000 ミリ秒でテストする必要があります。/clr を有効にしてコンパイルされたコードに C++ クラスが表示される場合は、#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();

妥当な時間として 1 分を選びましたが、必要に応じて微調整します。MSDN ドキュメントにはバグがあることに注意してください。OPR_FinalizerRun が許可された値として示されていませんが、実際には正しく機能します。また、ファイナライザー スレッドのタイムアウトを設定すると、マネージ ファイナライザーがアンマネージ C++ クラスを間接的に破棄する場合 (非常に一般的なシナリオ) にタイムアウトが発生しなくなります。

/clr でコンパイルされた CLRHost でこのコードを実行すると、GetCLRManager() の呼び出しが HOST_E_INVALIDOPERATION リターン コードで失敗することがわかります。CLRHost.exe を実行するために読み込まれた既定の CLR ホストでは、ポリシーをオーバーライドできません。そのため、CLR をホストするための専用の EXE を用意することにかなりこだわっています。

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 ソース コード ファイルで確認できます。


2 番目のシナリオは、C++ クラスが、/clr を有効にせずにコンパイルされ、混合モード アセンブリにリンクされたソース コード ファイルの一部である場合に発生します。その後、コンパイラは通常の atexit() ハンドラを使用して、アンマネージ実行可能ファイルで通常行われるのと同じように、デストラクタを呼び出します。この場合、プログラムの終了時に DLL が 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 が管理されるとすぐに、デストラクタが /clr なしでコンパイルされたコードに表示される場合でも、AppDomain.Unload でデストラクタを実行します。したがって、まだタイムアウトの問題があります。アンマネージ EXE を持つことは非常に珍しいことではありません。これは、[ComVisible] マネージ コードをロードする場合などに発生します。しかし、それはあなたのシナリオのようには聞こえません.CLRHostに行き詰まっています.

于 2012-11-29T21:00:12.437 に答える
1

「これはどこに文書化されていますか / このトピックについてもっと学ぶにはどうすればよいですか?」質問: ここから共有ソース共通言語インフラストラクチャ(別名 SSCLI)をダウンロードしてチェックアウトすると、これがどのように機能するか (または少なくともフレームワーク 2 で機能していた) を理解できますhttp://www.microsoft.com/en- us/download/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 に答える