The exception unknown software exception (0xc00200001) occured in the application at location 0x7c812a5b.
Ошибка возникает периодически при завершении работы приложения и регулярно при вызове некоторых юнит-тестов.
Стал разбираться в чем дело. Проект включает две сборки: интерфейсная часть написана на C#, реализация движка - на CLI/C++. Поискав ошибку в интернете обнаружил, что подобные ошибки встречаются в случае наличия в C++-ной сборке статических объектов с нетривиальными деструкторами. Как раз мой случай - один из классов в моей сборке включал реализацию синглетона Мейерса.
class Kernel { Kernel(void); ~Kernel(void) { /*удаление unmanaged объектов*/ } public: static Kernel GetInstance() { static Kernel k; //Синглетон Мейерса return k; } ... }
Никаких вызовов управляемого кода в моем статическом объекте вообще нет - Kernel, это чистый native-класс.
Поскольку без синглетонов при разработке приложений обойтись трудно, решил разобраться, как можно реализовать синглетон Мейерса в сборке, написанной на CLI/C++, в которой управляемый и неуправляемый коды смешаны.
Написал простой тестовый проект, включающий две сборки. Первая сборка Test написана на CLI/С++ и содержит управляемый класс Managed и неуправляемый Unmanaged, причем неуправляемый вызывается из управляемого. Вторая сборка написана на C# и вызывает класс Managed из первой. Управляемая сборка включает тест NUnit, через который производится запуск приложения (можно, конечно, запускать приложение и напрямую, но через NUnit проблем вылазит больше).
Код на C# тривиален:
class Class1 { public void Test1() { Test.ManagedClass m = new Test.ManagedClass(); m.Dispose(); } } class Program { /*Функция Main для запуска приложения напрямую, без NUnit*/ static void Main(string[] args) { Class1 c = new Class1(); for (int i = 0; i < 10; ++i) c.Test1(); } } [TestFixture] public class test { [Test] /*Тест NUnit для запуска приложения*/ public void Execute() { Class1 c = new Class1(); for (int i = 0; i < 10; ++i) c.Test1(); } }Код на CLI/C++ взятый за основу:
class Unmanaged1 { int *m_p; public: Unmanaged1(void) : m_p(new int()) {} ~Unmanaged1(void) { delete m_p; /*non trivial destructor*/ } static Unmanaged1& GetInstanceRef() { static Unmanaged1 u; return u; } int* GetPtr() { return m_p; } };
Вариант 1. Использование #pragma managed
Первое, что приходит в голову - это явно указать, что класс Unmanaged является неуправляемым.#pragma managed(push, off) class Unmanaged { ...... }; #pragma managed(pop)Не работает. При завершении работы приложения возникает исключение
0xc0020001: The string binding is invalid
. Вызов деструктора класса производится при выгрузке DLL, когда деинициализация CLR уже фактически проведена. Если в деструкторе вызывается управляемый код, то возникает ошибка. Явно у меня управляемый код нигде не вызвается. Есть какой-то неявный вызов (?)
Вариант 2a. Разделение кода на cpp и h-файлы
Вынесем функцию GetInstanceRef() в cpp-файл:/*unmanaged.cpp*/ Unmanaged& Unmanaged::GetInstanceRef() { static Unmanaged2 u; return u; }Тоже не работает. При вызове возникает ошибка: This function must be called in the default domain.
Вариант 2b. Вынос статической переменной за пределы класса
Вынесем переменнуюu
из GetInstanceRef
.
/*unmanaged.cpp*/ static Unmanaged2 u; Unmanaged2& Unmanaged2::GetInstanceRef() { return u; }Такой вариант срабатывает.Почему он срабатывает объяснено в книге Expert Visual C++/CLI: .NET for Visual C++ Programmers. Дело в том, что инициализация статических и глобальных переменных, объявленных в cpp-модулях, компилируемых с ключем /clr, производится в специальном конструкторе модуля. Конструктор модуля, по сути, представляет из себя инициализатор сборки и является первой управляемой функцией, вызываемой CLR. Соответственно, деинициализация переменных производится не при выгрузке DLL, а непосредственно перед завершением работы CLR. В итоге - все работает. Кстати, порядок инициализации переменных в смешанной сборке таков. Вначале производится инициализация всех глобальных и статических объектов, объявленных в cpp-модулях, компилируемых без ключа /clr. Затем производится инициализация всех глобальных и статических объектов, объявленных в cpp-модулях, компилируемых с ключем /clr.
Вариант 2c. Инициализация синглетона из неуправляемого кода.
Возможно проблема варианта 2а в том, что синглетон инициализируется вызовом управляемого кода. А что если его инициализировать заранее?/*unmanaged.cpp*/ Unmanaged2& dummy = Unmanaged2::GetInstanceRef(); Unmanaged2& Unmanaged2::GetInstanceRef() { static Unmanaged2 u; return u; }Такой код тоже срабатывает.
Вариант 3. Компиляция без ключа /clr
В моем случае класс Unmanaged не вызывает никакого управляемого кода. Теоретически, статические переменные этого класса можно было бы инициализировать при загрузке DLL, уничтожать при выгрузке DLL и проблем быть не должно. Что если этот класс откомпилировать без ключа /clr? В книге Хиджа приведен детальный алгоритм, как сделать обычный неуправляемый проект DLL (который будет компилироваться без ключа /clr) и включить в него несколько исходных файлов с управляемым кодом (которые будут компилироваться с ключем /clr). Для этого нужно создать второй precompiled header - stdafx_clr.h, и задать отдельные свойства компиляции (с ключом /clr) для файла stdafx_clr.cpp и всех cpp-файлов, содержащих управляемый код. Попробовал. Вариант 2a заработал. Вариант 1 не работает и в этом случае. Если вызывать текстовый класс через unit test, то получаем ошибкуManaged Debugging Assistant 'LoaderLock' has detected a problem in 'C:\Program Files\NUnit 2.4.8\bin\nunit.exe'.
Additional Information: Attempting managed execution inside OS Loader lock. Do not attempt to run managed code inside a DllMain or image initialization function since doing so can cause the application to hang.
Если вызывать приложение напрямую, то ошибка "0xC0020001: The string binding is invalid.".
Спасибо, очень помогло, сначала обрамил прагмой класс со статическими полями - поначалу обрадовался, падать перестало.
ОтветитьУдалитьПотом заметил, что падает в деструкторе, вынес в глобальные, все работает.