- •Введение в com
- •Часть 1. Общие сведения и немного истории.
- •Часть 3. Com-серверы, какие они бывают и о том, что дал нам Microsoft, чтобы управлять ими.
- •Часть 4. «Говорящий не знает, знающий не говорит. Лао Цзы» - от теории к практике!
- •Часть 5. Реализация проекта сом-сервера на atl.
- •Часть 1. Общие сведения и немного истории.
- •Часть 3. Com-серверы, какие они бывают и о том, что дал нам Microsoft, чтобы управлять ими.
- •Часть 4. «Говорящий не знает, знающий не говорит. Лао Цзы» - от теории к практике!
- •Часть 5. Реализация проекта сом-сервера на atl.
- •Howto: Глобальный com-синглтон в dll
- •Пример расширения возможностей tWebBrowser
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+-приложении. – прим.ред. |
Статья посвящена красивому способу обхода этого ограничения.
Идея
Идея заключается в реализации собственной фабрики класса, работающей по следующему алгоритму:
Фабрика класса хранит указатель на интерфейс создаваемого объекта и реально создаёт его только в первый раз, при обработке дальнейших запросов она просто возвращает сохранённый указатель.
При первом создании объекта фабрика класса проверяет, не имеется ли уже в системе другой такой же фабрики класса.
Если фабрика кем-то зарегистрирована, то объект уже создан, и новая фабрика должна не создавать второй экземпляр (это синглтон!), а вернуть указатель на существующий объект. Для этого ей нужно просто перенаправить вызов зарегистрированной фабрике.
Если такая фабрика ещё не зарегистрирована, наша фабрика должна зарегистрировать «себя» с помощью 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. Информацию о журнале можно найти здесь