Уничтожение родных объектов со статической продолжительностью хранения

2012-12-09 Резюме:

  • В обычном смешанном режиме глобальные встроенные деструкторы С++ работают как финализаторы. Невозможно изменить это поведение или связанный с ним тайм-аут.
  • DLL с смешанным режимом запускает конструкторы/деструкторы С++ во время загрузки/выгрузки DLL - точно так же, как родная DLL.
  • Хостинг CLR в собственном исполняемом файле с использованием COM-интерфейса позволяет обеим деконструкторам вести себя как в родной DLL (по моему желанию), так и при настройке таймаута для финализаторов (дополнительный бонус).
  • Насколько я могу сказать выше, применяется, по крайней мере, к Visual Studio 2008, 2010 и 2012. (Только для тестирования с .NET 4)

Фактический исполняемый файл хоста CLR, который я планирую использовать, очень похож на тот, который изложен в этом вопросе, за исключением нескольких незначительных изменений:

  • Настройка OPR_FinalizerRun на некоторое значение (60 секунд в настоящее время, но может быть изменено), как это предложил Ханс Пассант.
  • Использование классов интеллектуального указателя класса ATL COM (они недоступны в экспресс-версиях Visual Studio, поэтому я их пропустил из этого сообщения).
  • Lodaing CLRCreateInstance из mscoree.dll динамически (чтобы обеспечить лучшее сообщение об ошибках, когда не установлена ​​совместимая среда CLR).
  • Передача командной строки от хоста к назначенной функции Main в сборке DLL.

Спасибо всем, кто нашел время, чтобы прочитать вопрос и/или комментарий.


2012-12-02 Обновление в нижней части сообщения.

Я работаю над приложением С++/CLI с смешанным режимом, используя Visual Studio 2012 с .NET 4, и был удивлен, обнаружив, что деструкторы для некоторых из нативных глобальных объектов не вызывались. Исследуя проблему, выясняется, что они ведут себя как управляемые объекты, как описано в этом сообщении.

Я был очень удивлен этим поведением (я понимаю это для управляемых объектов) и не мог найти его документально нигде, ни в С++/CLI standard или в описании деструкторов и финализаторов.

Следуя предложению в комментарии 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 после запуска управляемого метода и происходит всякая другая очистка. Они запускаются в том же потоке, что и конструктор, и нет связанного с ними тайм-аута (желаемое поведение). Как и ожидалось, деструкторы времени глобальных управляемых объектов (классы non-ref, помещенные в файлы, скомпилированные с помощью /clr), можно контролировать с помощью ICLRPolicyManager->SetTimeout(OPR_ProcessExit, <timeout>).

Угрожая предположению, я думаю, что причина, по которой глобальные нативные конструкторы/деструкторы функционируют нормально (определяется как поведение, как и следовало ожидать) в сценарии DLL, позволяет разрешать использование LoadLibrary и GetProcAddress для собственных функций. Поэтому я ожидал бы, что относительно безопасно полагаться на то, что он не изменится в обозримом будущем, но хотел бы получить какое-либо подтверждение/отказ от официальных источников/документации в любом случае.

Обновление 2:

В Visual Studio 2012 (тестируется с экспресс-и премиум-версиями, к сожалению, у меня нет доступа к более ранним версиям на этом компьютере). Он должен работать аналогично в командной строке (здание, как описано выше), но здесь, как воспроизводить из среды IDE.

Здание CLRHost.exe:

  • Файл → Новый проект
  • Visual С++ → Win32 → Консольное приложение Win32 (назовите проект "CLRHost" )
  • Настройки приложения → Дополнительные параметры → Пустой проект
  • Нажмите "Готово"
  • Щелкните правой кнопкой мыши Исходные файлы в проводнике решений. Добавить → Новый элемент → Visual С++ → Файл С++. Назовите его CLRHost.cpp и вставьте содержимое CLRHost.cpp из сообщения.
  • Проект → Свойства. Свойства конфигурации → C/С++ → Генерация кода → Изменить "Включить исключения С++" на "Да с исключениями SEH (/EHa)" и "Основные проверки выполнения" на "По умолчанию"
  • Построить.

Здание CLR.DLL:

  • Файл → Новый проект
  • Visual С++ → CLR → Библиотека классов (Назовите проект "CLR" )
  • Удалить все автогенерируемые файлы
  • Проект → Свойства. Свойства конфигурации → C/С++ → Предварительно скомпилированные заголовки → Предварительно скомпилированные заголовки. Измените на "Не использовать предварительно скомпилированные заголовки".
  • Щелкните правой кнопкой мыши Исходные файлы в проводнике решений. Добавить → Новый элемент → Visual С++ → Файл С++. Назовите его CLR.cpp и вставьте содержимое CLR.cpp из сообщения.
  • Добавьте новый файл С++ с именем Native.cpp и вставьте код из сообщения.
  • Щелкните правой кнопкой мыши на "Native.cpp" в проводнике решений и выберите свойства. Изменить C/С++ → Общие → Поддержка Common Language RunTime для "Без поддержки обычного языка RunTime"
  • Проект → Свойства → Отладка. Измените "Command", чтобы указать на CLRhost.exe "Аргументы команд" на "$ (TargetPath)", включая кавычки, "Тип отладчика" на "Смешанные"
  • Построение и отладка.

Размещение точки останова в деструкторе 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
    [email protected]()  + 0x136 bytes   
    [email protected]()  + 0xad bytes   
    [email protected]()  + 0x14 bytes   
    [email protected]()  + 0x141 bytes    
    [email protected]()  + 0x74 bytes 
    kernel32.dll!74e37a0d()     
    mscoreei.dll!RuntimeDesc::ShutdownAllActiveRuntimes()  + 0x10e bytes    
    [email protected]()  + 0x27 bytes  
    [email protected]()  + 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
    [email protected]@12()  + 0x12 bytes    
    [email protected]()  + 0x27 bytes   
    [email protected]()  + 0x1b bytes    

Запуск как автономный исполняемый файл. Я получаю трассировку стека, которая очень похожа на ту, которую наблюдает Ханс Пассант (хотя он не использует управляемую версию 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++
    [email protected]@12()  + 0x12 bytes    
    [email protected]()  + 0x27 bytes   
    [email protected]()  + 0x1b bytes    

Ответ 1

Как легко получить простые вопросы:

Хорошим ресурсом для настройки CLR является книга Стивена Пратшнера "Настройка среды выполнения Common Language Runtime Microsoft.NET Framework". Помните, что он устарел, интерфейсы хоста изменились в .NET 4.0. MSDN ничего не говорит об этом, но хостинг-интерфейсы хорошо документированы.

Вы можете упростить отладку, изменив настройку отладчика, изменив тип "Авто" на "Управляемый" или "Смешанный".

Обратите внимание, что ваш сон на 3000 мсек находится только на краю, вы должны протестировать 5000 мс. Если класс С++ появляется в коде, который скомпилирован с /clr, даже при неуправляемом эффекте #pragma, вам необходимо переопределить таймаут потока финализатора. Протестированный в версии CLR.NET 3.5 SP1, следующий код работал хорошо, чтобы дать деструктору достаточное время для завершения:

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 как допустимое значение, но на самом деле работает правильно. Установка тайм-аута потока финализатора также гарантирует, что управляемый финализатор не будет тайм-аут, когда он косвенно разрушает неуправляемый класс С++, очень распространенный сценарий.

Одна вещь, которую вы увидите при запуске этого кода с помощью CLRHost, скомпилированной с помощью /clr, заключается в том, что вызов GetCLRManager() завершится неудачно с кодом возврата HOST_E_INVALIDOPERATION. Хост CLR по умолчанию, загруженный для выполнения вашего CLRHost.exe, не позволит вам переопределить политику. Таким образом, вы довольно застряли, имея выделенный EXE для размещения CLR.

Когда я протестировал это, установив CLRHost на сборку смешанного режима, стек вызовов выглядел так, когда устанавливал точку останова на деструкторе:

CLRClient.dll!Global::~Global()  Line 24    C++
[Managed to Native Transition]  
CLRClient.dll!<Module>[email protected]@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.


Второй сценарий возникает, когда класс С++ является частью файла исходного кода, который скомпилирован без /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 будет управляться, он будет запускать деструкторы на AppDomain.Unload, даже если они появляются в коде, который был скомпилирован без /clr. Таким образом, у вас все еще есть проблема с таймаутом. Наличие неуправляемого EXE не очень необычно, это произойдет, например, при загрузке управляемого кода [ComVisible]. Но это не похоже на ваш сценарий, вы застряли в CLRHost.

Ответ 2

Чтобы ответить на вопрос "Где это документировано/как я могу больше узнать о теме?" вопрос: вы можете понять, как это работает (или используется для работы хотя бы для фреймворка 2), если вы загружаете и проверяете общую инфраструктуру общего языка (aka SSCLI) здесь 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;
    }

Это скажет вам, что Execution Engine будет подчиняться политике OPR_FinalizerRun, если она определена, которая соответствует значению в EClrOperation Enumeration. GetEEPolicy определяется в eePolicy.h и eePolicy.cpp.