Сообщений 10    Оценка 70        Оценить  
Система Orphus

HOWTO: Глобальный COM-синглтон в DLL

Авторы: Егор Синькевич
Сергей Холодилов
The RSDN Group

Источник: RSDN Magazine #4-2004
Опубликовано: 16.02.2005
Исправлено: 10.12.2016
Версия текста: 1.0
Идея
Реализация
Пример использования
Проблемы
Маршалинг
Преждевременная смерть

Я один, но это не значит, что я одинок...

Виктор Цой

Демонстрационный проект

Синглтоном (от singleton, одиночка) называется объект, который в любой момент работы системы существует не более чем в одном экземпляре. Часто вводится также дополнительное требование: после своего создания синглтон должен существовать ровно в одном экземпляре, то есть он не должен уничтожаться до окончания работы системы. Обычно такие объекты используются для упорядоченного доступа к каким-то глобальным ресурсам - например к лог-файлу, сетевому соединению, принтеру, пользователю. :)

В зависимости от того, насколько глобален контролируемый синглтоном ресурс и того, что подразумевается под «системой», меняются пределы уникальности синглтона. Синглтон может быть уникален в рамках:

В принципе, все три варианта несложно реализуются стандартными средствами, причём как с использованием COM, так и без.

ПРИМЕЧАНИЕ

Насколько «несложно», сильно зависит от того, что вам надо получить, и что можно использовать. Хотя концептуальных трудностей и нет, всегда могут возникнуть практические.

Но вот с COM-синглтонами, уникальными в рамках компьютера есть небольшой нюанс: обычно для их реализации предлагается только один подход – оформить COM-сервер в виде exe-файла. В частности, стандартный ATL-синглтон в dll будет уникален только в рамках процесса.

Глобальные в пределах машины синглтоны легко получить с помощью СОМ+. Нужно создать обыкновенный синглтон и зарегистрировать его в COM+-приложении. – прим.ред.

Статья посвящена красивому способу обхода этого ограничения.

Идея

Идея заключается в реализации собственной фабрики класса, работающей по следующему алгоритму:

  1. Фабрика класса хранит указатель на интерфейс создаваемого объекта и реально создаёт его только в первый раз, при обработке дальнейших запросов она просто возвращает сохранённый указатель.
  2. При первом создании объекта фабрика класса проверяет, не имеется ли уже в системе другой такой же фабрики класса.
  3. Если фабрика кем-то зарегистрирована, то объект уже создан, и новая фабрика должна не создавать второй экземпляр (это синглтон!), а вернуть указатель на существующий объект. Для этого ей нужно просто перенаправить вызов зарегистрированной фабрике.
  4. Если такая фабрика ещё не зарегистрирована, наша фабрика должна зарегистрировать «себя» с помощью CoRegisterClassObject и создать объект.

Но это идея «в чистом виде», использование ATL внесёт некоторые коррективы.

Реализация

В соответствии с идеей, функциональность фабрики класса разбивается на две части:

А, с учётом того, что, если фабрика класса уже зарегистрирована в другом процессе, роль «ядра» исполняет указатель на интерфейс этой фабрики, вырисовывается архитектура:

При этом писать «ядро» самостоятельно совсем не обязательно, можно воспользоваться стандартной ATL-фабрикой класса для синглтонов. Но, если она вас почему-то не устраивает (одна возможная причина описана ниже в разделе «Проблемы»), всегда можно написать свою. Ниже приведен код обертки.

      // Первый параметр шаблона – создаваемый класс. От него нам нужен
      // только CLSID, для регистрации.
      // Второй параметр шаблона – класс, статический метод CreateInstance которого
      // умеет создавать «ядро». Звучит страшно, но для ATL вполне стандартно.
      template <class T, class RealCFCreator>
class CComClassFactoryDllSingleton : 
    public IClassFactory,
    public CComObjectRootEx<CComGlobalsThreadModel>
{
public:
    BEGIN_COM_MAP(CComClassFactoryDllSingleton)
        COM_INTERFACE_ENTRY(IClassFactory)
    END_COM_MAP()

    HRESULT FinalConstruct()
    {
        m_dwRegister = 0;
        return S_OK;
    }

    HRESULT FinalRelease()
    {
        if (m_dwRegister != 0)
        {
            // Надо разрегистрировать фабрику класса 
            CoRevokeClassObject(m_dwRegister);
        }

        return S_OK;
    }

    //// Реализация интерфейса IClassFactory//

    STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid, void** ppvObj)
    {
        if (ppvObj == 0)
        {
            return E_POINTER;
        }
    
        if (pUnkOuter != NULL)
        {
            // Синглтоны не поддерживают агрегациюreturn CLASS_E_NOAGGREGATION;
        }

        // Создаём/получаем фабрику класса
        HRESULT hr = GetOrRegisterCF();

        if (hr == S_OK)
        {
            // Пытаемся её использовать
            hr = m_pRealClassFactory->CreateInstance(pUnkOuter, riid, ppvObj);
        }

        return hr;
    }

    STDMETHOD(LockServer)(BOOL fLock)
    {
        // Возможно, что до вызова LockServer не было ни одного// вызова CreateInstance, для начала мы должны получить фабрику.
        HRESULT hr = GetOrRegisterCF();

        if (FAILED(hr))
        {
            // Не вышлоreturn hr;
        }

        // Данный вызов идёт либо через "нас" либо, через фабрику в// удалённом процессе.
        hr = m_pRealClassFactory->LockServer(fLock);

        if (FAILED(hr))
        {
            // Не вышлоreturn hr;
        }

        // Чужой модуль – хорошо, но о своём тоже забывать не следуетif (fLock)
        {
            //_Module.Lock(); // для ATL 3
            _pAtlModule->Lock(); // для ATL 7
        }
        else
        {
            //_Module.Unlock(); // для ATL 3
            _pAtlModule->Unlock(); // для ATL 7
        }
        return S_OK;
    }

private:

    // Создаёт и регистрирует новую фабрику класса,// либо получает уже зарегистрированную фабрику.// Результат сохраняется в m_pRealClassFactory.
    HRESULT GetOrRegisterCF()
    {
        if (m_pRealClassFactory != 0)
        {
             // фабрика уже создана/получена, второй раз не требуетсяreturn S_OK;
        }

        HRESULT hr = S_OK;
        HANDLE hMutex = 0;

        __try
        {
            // Синхронизируем создание фабрики между процессами
            hMutex = CreateMutex(0, FALSE, _T("DllSingletonMutex"));
            WaitForSingleObject(hMutex, INFINITE);

            CLSID clsid = T::GetObjectCLSID();

            // Попытаемся получить уже зарегистрированную фабрику класса.
            hr = CoGetClassObject(
                    clsid,
                    CLSCTX_LOCAL_SERVER,
                    0,
                    IID_IClassFactory,
                    (void**) &m_pRealClassFactory);

            if (FAILED(hr))
            {
                // Фабрика класса ещё не зарегистрирована. Мы - первый процесс// и должны создать и зарегистрировать фабрику, для её // использования другими процессами.// Создаём фабрику класса
                hr = RealCFCreator::CreateInstance(
                           0, 
                           IID_IClassFactory, 
                          (void**)&m_pRealClassFactory);

                if (hr == S_OK)
                {
                     // Регистрируем её
                     hr = CoRegisterClassObject(
                            clsid,
                            m_pRealClassFactory,
                            CLSCTX_LOCAL_SERVER | CLSCTX_INPROC_SERVER,
                            REGCLS_MULTIPLEUSE,
                            &m_dwRegister);
                }
            }
        
        }
        __finally
        {
            // Освобождение мьютекса. По уму это надо делать// через деструктор объекта CmyMutex, но в ATL такого// нет, а писать самостоятельно – лень...if (hMutex != 0)
            {
                ReleaseMutex(hMutex);
                CloseHandle(hMutex);
            }
        }

        return hr;
    }

private:

    DWORD                  m_dwRegister; 
    CComPtr<IClassFactory> m_pRealClassFactory;
};

Для облегчения использования этого класса предназначен следующий макрос:

      #define MAKE_MACRO_PARAM(x, y) x, y
#define DECLARE_CLASSFACTORY_DLL_SINGLETON(obj)    \
          DECLARE_CLASSFACTORY_EX(                 \
              MAKE_MACRO_PARAM(                    \
                  CComClassFactoryDllSingleton<    \
                      obj,                         \
                      ATL::CComCreator<            \
                          ATL::CComObjectCached<   \
                              CComClassFactorySingleton< obj > > > > ))
ПРИМЕЧАНИЕ

Макрос MAKE_MACRO_PARAM предназначен для того, чтобы препроцессор истолковал CComClassFactoryDllSingleton< .., ..> как один параметр, а не как два параметра, разделённые запятой. За предложенное решение большое спасибо Андрею Солодовникову (Andrew S). Сергей Азаркевич (Sergey J.A.) предложил ввести макрос COMMA

#define COMMA ,

и использовать его в выражении вместо запятых, не разделяющих параметры макроса. Это более общее решение, позволяющее беспрепятственно обойти любое количество «лишних» запятых, но в частном случае вариант Андрея смотрится понятнее и симпатичнее.

В качестве «ядра» он использует стандартную ATL-фабрику для создания синглтонов.

Пример использования

Используется приведенная выше реализация фабрики классов элементарно, точно так же как стандартные макросы DECLARE_CLASSFACTORY. Достаточно добавить в тело класса, реализующего COM-объект, макрос DECLARE_CLASSFACTORY_DLL_SINGLETON.

      class ATL_NO_VTABLE CTestObj : 
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CTestObj, &CLSID_TestObj>
{
public:
    DECLARE_CLASSFACTORY_DLL_SINGLETON(CTestObj)
    ...
};

Проблемы

Описанная выше реализация работает, но у нее есть две серьёзные проблемы.

Маршалинг

Проблема проявляется, если одновременно выполняются все следующие условия:

В этом случае мы имеем следующую картину:

ПРЕДУПРЕЖДЕНИЕ

Естественно, точно такая же проблема свойственна и стандартной ATL-реализации синглтона. И сама проблема, и класс, который её решает, описаны в «Q201321 HOWTO: Alternative Implementation of ATL Singleton».

Есть два пути решения этой проблемы:

Преждевременная смерть

Поскольку описываемое поведение крайне нетипично для COM-объектов, скорее всего, процесс, загрузивший DLL, даже и не подозревает, что в нём находится синглтон. Соответственно, перед завершением он не будет заботиться о возможных внешних ссылках на синглтон, в результате чего все эти ссылки будут указывать в никуда. Или, выражаясь чуть более точно, возвращать одну из ошибок RPC_E_xxx (при проведении опытов было получено несколько разных значений).

Помимо очевидных организационных мер борьбы (написать специальный процесс, который будет создавать объект первым, и не будет выгружаться), можно попытаться пересмотреть архитектуру, сделав систему более распределённой. Идея заключается в следующем:

В результате, пока синглтон кому-то нужен, он будет практически бессмертен, как феникс, заново возрождающийся из пепла. И даже выключение питания компьютера не сможет прервать цепочку воплощений :)

Идея интересна, но её реализация выходит далеко за рамки статьи и оставляется читателю в качестве нетривиального развлечения, за которым можно провести не один долгий зимний вечер.


Эта статья опубликована в журнале RSDN Magazine #4-2004. Информацию о журнале можно найти здесь
    Сообщений 10    Оценка 70        Оценить