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