среда, 14 января 2009 г.

Синглетон Мейерса в смешанных сборках CLI/C++

В текущем проекте, разрабатываемым под Microsoft .Net 2.0, столкнулся со следующей ошибкой:

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.".

Итоги

Итак, как можно реализовать синглетон Мейерса в неуправляемом коде в смешанной сборке? Реализовать синглетон в заголовочном файле C++ (вариант 1) не удается - такой синглетон нельзя вызвать из управляемого кода без появления ошибки при закрытии приложения. Если вынести реализацию в cpp-шный файл (вариант 2), то ситуация меняется. В случае, когда весь проект компилируется с ключом /clr, статическую переменную необходимо вынести за пределы класса и объявить глобально в теле cpp-файла (вариант 2b). Альтернативный вариант - можно предварительно инициализировать синглетон из неуправялемого кода, до его первого вызова из управляемого кода (вариант 2c). Если проект компилируется без ключа /clr, а ключ /clr включен лишь для файлов с управляемым кодом, то статическую переменную можно оставлять локальной (вариант 2a). Исходники тестового проекта можно взять здесь. Тестирование велось под VS 2005. Update: недавно налетел на столь же неприятную проблему с синглетонами на Android. Реализация синглетона - вещь непростая..

1 комментарий:

  1. Спасибо, очень помогло, сначала обрамил прагмой класс со статическими полями - поначалу обрадовался, падать перестало.
    Потом заметил, что падает в деструкторе, вынес в глобальные, все работает.

    ОтветитьУдалить