Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Введение в COM.docx
Скачиваний:
8
Добавлен:
24.11.2019
Размер:
587.01 Кб
Скачать

Howto: Глобальный com-синглтон в dll

Авторы: Егор Синькевич Сергей Холодилов The RSDN Group Источник: RSDN Magazine #4-2004

Опубликовано: 16.02.2005 Исправлено: 13.03.2005 Версия текста: 1.0

Идея Реализация Пример использования Проблемы

Маршалинг Преждевременная смерть

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

Виктор Цой

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

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

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

  • Процесса.

  • Компьютера.

  • Сети.

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

ПРИМЕЧАНИЕ

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

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

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

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

Идея

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

  1. Фабрика класса хранит указатель на интерфейс создаваемого объекта и реально создаёт его только в первый раз, при обработке дальнейших запросов она просто возвращает сохранённый указатель.

  2. При первом создании объекта фабрика класса проверяет, не имеется ли уже в системе другой такой же фабрики класса.

  3. Если фабрика кем-то зарегистрирована, то объект уже создан, и новая фабрика должна не создавать второй экземпляр (это синглтон!), а вернуть указатель на существующий объект. Для этого ей нужно просто перенаправить вызов зарегистрированной фабрике.

  4. Если такая фабрика ещё не зарегистрирована, наша фабрика должна зарегистрировать «себя» с помощью CoRegisterClassObject и создать объект.

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

Реализация

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

  • «Ядро». Собственно создаёт объект.

  • «Обёртка». Занимается регистрацией и получением указателя на зарегистрированную фабрику.

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

  • «Обёртка» содержит указатель на интерфейс IClassFactory, реализуемый «ядром».

  • Если фабрика класса уже кем-то зарегистрирована, он указывает на интерфейс внешней фабрики класса.

  • Если фабрику класса надо создавать, объект создаётся, регистрируется, указатель на его интерфейс IClassFactory сохраняется

  • Вызовы методов интерфейса IClassFactory делегируются этому указателю.

При этом писать «ядро» самостоятельно совсем не обязательно, можно воспользоваться стандартной 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)

...

};

Проблемы

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

Маршалинг

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

  • Вызов CoCreateInstance происходит из того же процесса, в котором был создан объект.

  • Поточная модель объекта – STA

  • Поточная модель потока, вызывающего CoCreateInstance – STA

  • Это не тот же самый поток, который создал объект.

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

  • «Настоящая» фабрика класса («ядро») находится в этом процессе.

  • Так как поточная модель объекта и потока, вызывающего CoCreateInstance совпадают, DllGetClassObject, а, следовательно, и методы фабрики класса, вызываются напрямую.

  • В результате стандартная ATL-фабрика для создания синглтона вместо указателя на proxy вернёт прямой указатель на интерфейс.

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

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

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

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

  • Отказаться от стандартной ATL-фабрики для создания синглтонов. Например, фабрика классов, вместо того, чтобы хранить прямой указатель на созданный объект, может поместить его в GIT и заново «доставать» оттуда при каждом новом «создании».

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

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

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

  • Клиенты синглтона из других процессов не используют прямой указателем на proxy, вместо этого у них есть указатель на некоторый дополнительный объект, который перенаправляет все вызовы proxy, а уже тот – синглтону.

  • Когда «дополнительный объект» обнаруживает, что proxy вернул RPC_E_xxx, он понимает, что синглтон скоропостижно выгрузился, и нужно пересоздать его заново.

  • Для «пересоздания», естественно, вызывается фабрика классов синглтона, которая очень быстро обнаруживает, что хранящаяся у неё ссылка на внешнюю фабрику классов тоже мертва. В этой ситуации она либо получает ссылку на новую фабрику класса в каком-то другом процессе, либо, если новой фабрики ещё нет, создаёт её самостоятельно.

  • При пересоздании синглтон каким-то образом «подхватывает» состояние своей предыдущей «инкарнации». Например, оно может храниться в файле или реестре.

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

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

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