Глава 7
Фабрика класса

Когда я был совсем маленьким и еще не собирался стать пожарным, я мечтал стать дизайнером наборов конструктора Lego. У меня были самые разные идеи относительно хитроумных новых деталей, из которых можно было бы строить потрясающие модели. Я даже послал несколько проектов в компанию (которая не стала запускать их в производство.) Тем не менее, несмотря на отсутствие у фирмы интереса к моим новациям, сейчас я мог бы производить детали Lego прямо у себя в спальне.

Уже появились машинки, которые называют трехмерными принтерами (3D-printers), — и это название очень им подходит. Они похожи на струйные принтеры, но выбрасывают тонкую струю пластика под давлением, а не чернила. Такой принтер наносит пластмассу слоями тоньше миллиметра. Повторная «печать» по одному и тому же месту позволяет создать сложные трехмерные объекты. Их можно использовать как прототипы или формы для изготовления деталей, а иногда и как готовые детали. С такой машинкой можно было бы организовать Домашнюю Фабрику Пластиковых Деталей. При помощи пакета САПР можно было бы в мгновение ока проектировать и производить новые детали. На такой домашней фабрике Вы могли бы сделать ту хитрую детальку с переходом 1х3, без которой никак не собиралась вся модель. Вообще у Вас больше не было бы недостатка в деталях - хотя компания Lego, вероятно, предпочла бы все же продавать Вам свои.

В этой главе я собираюсь рассмотреть своего рода фабрику, на которой производятся не детали Lego, а компоненты. Эта фабрика класса — просто компонент с интерфейсом для создания других компонентов, так что обойдется она нам дешевле, чем трехмерный принтер за 50000 долларов.

Но прежде чем заняться фабрикой класса, мы познакомимся с самым простым способом создания компонентов — при помощи функции CoCreateInstance. He удивительно, что этим способом пользуются чаще всего. К сожалению, он недостаточно гибок и годится не для всех компонентов. Как раз здесь, с точки зрения клиента, и появляется фабрика класса. Все компоненты создаются на этой фабрике - CoCreateInstance при создании компонента тоже пользуется ее услугами, но неявно и незаметно для вызывающей программы. Клиент получает большую свободу в создании компонентов, если он прямо использует фабрику. Точно так же, как у Вас было бы больше возможностей, если бы вы не покупали детали Lego у фирмы, а делали их сами, на Домашней Фабрике Пластиковых Деталей.

CoCreateInstance

Для создания компонентов в библиотеке СОМ служит функция CoCreateInstance, которая, получив CLSID, создает экземпляр соответствующего компонента и возвращает интерфейс этого экземпляра. В этом разделе мы рассмотрим использование CoCreateInstance и увидим, с какими ограничениями оно связано. Но сначала давайте посмотрим на саму функцию.

Прототип CoCreateInstance

Объявление CoCreateInstance приведено ниже:
HRESULT __stdcall CoCreateInstance(
const CLSID& cisid,
IDnknown* pIUnknownOuter,    // Внешний компонент
DWORD dwClsContext,          // Контекст сервера
const IID& iid,
void** ppv
);
У функции четыре входных параметра (in) и единственный выходной (out). Первый параметр — CLSID создаваемого компонента. Второй параметр используется для агрегирования компонентов и будет обсуждаться в следующей главе. Третий параметр — dwClsContext— ограничивает контекст исполнения компонента, с которым данный клиент может работать. Этот параметр мы рассмотрим позже.

Четвертый параметр, iid— это IID интерфейса, который мы хотим использовать для работы с компонентом. Указатель на этот интерфейс возвращается через последний параметр — ppv. Поскольку в CoCreateInstance передается IID, клиент может не вызывать QueryInterface для созданного компонента.

Использование CoCreafelnsfance

Используется CoCreateInstance так же просто, как и QueryInterface:
// Создать компонент
IX* рIХ = NULL;
HRESULT hr = ::CoCreateInstance(CLSID_Component1,
		NULL,
		CLSCTX_INPROC_SERVER,
		IID_IX,
		(void**)&pIX);
if (SUCCEEDED(hr)),
{
		pIX->Fx() ;
		pIX->Release() ;
}
В данном примере мы создаем компонент, задаваемый CLSID_Componentl. Мы не агрегируем его, поэтому значением второго параметра является NULL. В следующей главе мы будем передавать функции значение, отличное от NULL. Параметр CLSCTX_INPROC_SERVER заставляет CoCreateInstance загружать только те компоненты, которые содержатся в серверах в процессе или в DLL.

Значения, передаваемые CoCreateInstance ткачестве двух последних параметров, — те же самые, которые мы передавали бы Qwrylnterface. В данном примере мы передаем IID_IX, чтобы запросить интерфейс IX, который возвращается указателем р1Х. Если вызов CoCreateInstance был успешным, то интерфейс IX готов к работе. Освобождение же интерфейса IX указывает, что клиент завершил использование и этого интерфейса, и самого компонента.

Контекст класса

Третий параметр CoCreateInstance — dwClsContext — используется для управления тем, где может исполняться компонент: в том же процессе, что и клиент, в другом процессе или на другой машине. Значением параметра может быть комбинация признаков, приведенных ниже:
CLSCTX_INPROC_SERVER Клиент принимает только компоненты, которые исполняются в одном с ним процессе. Подобные компоненты должны быть реализованы в DLL.
CLSCTX_INPROC_HANDLERКлиент будет работать с обработчиками в процессе. Обработчик в процессе - это компонент внутри процесса, который реализует только часть компонента. Другие части реализуются компонентом вне процесса - локальным или удаленным сервером.
CLSCTX_LOCAL_SERVERКлиент будет работать с компонентами, которые выполняются в другом процессе, но на той же самой машине. Локальные серверы реализуются в EXE, как мы увидим в гл. 10.
CLSCTX_REMOTE_SERVERКлиент допускает компоненты, выполняющиеся на другой машине. Использование этого флага требует задействования DCOM. Мы рассмотрим его в гл. 10.
Один и тот же компонент может быть доступен во всех трех контекстах: удаленном, локальном и в процессе. В некоторых случаях клиент может пожелать использовать только компоненты в процессе, так как они работают быстрее. В других случаях он может не захотеть использовать компоненты, работающие в его собственном процессе, так они имеют доступ к любому участку памяти процесса, что не слишком безопасно. Однако в большинстве случаев клиента не интересует контекст, в котором исполняется компонент. Поэтому в OBJBASE.H определены удобные константы которые комбинируют (с помощью побитового ИЛИ) приведенные выше значения (см. табл. 7-1).

Таблица 7-1. Предопределенные комбинации признаков контекста исполнения
КонстантыЗначения
CLSCTX_INPROCCLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER
CLSCTX_ALLCLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER
CLSCTX_SERVERCLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER
Значение CLSCTX_REMOTE_SERVER добавляется к CLSCTX_ALL и CLSCTX_SERVER, только если перед включением OBJBASE.H Вы определили символ препроцессора _WIN32_WINNT большим или равным 0х0400. (Тот же эффект даст определение перед включением OBJBASE.H символа препроцессора _WIN32_DCOM.) Предупреждение: если Вы передадите функции CoCreateInstance значение CLSCTX_REMOTE_SERVER на системе, которая не поддерживает DCOM, то CoCreateInstance возвратит ошибку E_INVALIDARG. Это легко может случиться, если Вы компилируете свою программу с _WIN32_WINNT, большим или равным 0х0400, и затем запускаете ее в системе Microsoft Windows NT 3.51 или Microsoft Windows 95, которые не поддерживают DCOM. Более подробно CLSCTX_LOCAL_SERVER и CLSCTX_REMOTE_SERVER рассматриваются в гл. 10.

Листинг кода клиента

В качестве примера в этой главе мы- создадим наши первые настоящие клиент и компонент СОМ. Копии всех исходных файлов находятся на прилагающемся компакт-диске. Листинг 7-1 содержит код клиента. Единственным его существенным отличием от клиентов из гл. 5 является сояание компонента с помощью CoCreateInstance. Среди других особенности можно назвать использование CoInitialize и CoUninitialize для инициализации библиотеки СОМ (как обсуждалось в гл. 6).
//
// Client.cpp - реализация клиента
//
#include 
#include 

#include "Iface.h"

void trace(const char* msg) { cout << "Client: \t\t" << msg << endl ;}

//
// main function
//
int main()
{
	// Инициализация библиотеки COM
	CoInitialize(NULL) ;

	trace("Call CoCreateInstance to create") ;
	trace("  component and get interface IX.") ;
	IX* pIX = NULL ; 
	HRESULT hr = ::CoCreateInstance(CLSID_Component1,
	                                NULL, 
	                                CLSCTX_INPROC_SERVER,
	                                IID_IX, 
	                                (void**)&pIX) ;
	if (SUCCEEDED(hr))
	{
		trace("Succeeded getting IX.") ;
		pIX->Fx() ;          // Use interface IX.

		trace("Ask for interface IY.") ;
		IY* pIY = NULL ;
		hr = pIX->QueryInterface(IID_IY, (void**)&pIY) ;
		if (SUCCEEDED(hr))
		{
			trace("Succeeded getting IY.") ;
			pIY->Fy() ;       // Use interface IY.
			pIY->Release() ;
			trace("Release IY interface.") ;
		}
		else
		{
			trace("Could not get interface IY.") ;
		}

		trace("Ask for interface IZ.") ;

		IZ* pIZ = NULL ;
		hr = pIX->QueryInterface(IID_IZ, (void**)&pIZ) ;
		if (SUCCEEDED(hr))
		{
			trace("Succeeded in getting interface IZ.") ;
			pIZ->Fz() ;
			pIZ->Release() ;
			trace("Release IZ interface.") ;
		}
		else
		{
			trace("Could not get interface IZ.") ;
		}

		trace("Release IX interface.") ;
		pIX->Release() ;
	}
	else
	{
		cout << "Client: \t\tCould not create component. hr = "
		     << hex << hr << endl ;    
	}

	// Закрыть библиотеку COM
	CoUninitialize() ;

	return 0 ;
}

Но CoCreatelnstcince недостаточно гибка

Создание объектов — очень важная операция в любой объектно-ориентированной системе. Прежде чем использовать объекты, их необходимо создать. Если каждый объект создается по-своему, то будет трудно использовать разные объекты полиморфно. Следовательно, желательно, чтобы создание объектов было как можно более гибким — а все компоненты, в свою очередь, создавались сходным образом. Чем гибче процесс создания, тем легче его адаптировать к нуждам множества компонентов.

Выше мы видели, что CoCreateInstance получает CLSID, создает соответствующий компонент и возвращает требуемый указатель на интерфейс. В большинстве случаев CoCreateInstance вполне достаточно. Однако CoCreateInstance недостаточно гибка, чтобы предоставить клиенту способ управления процессом создания компонента. Когда CoCreateInstance возвращает управление, компонент уже создан. Уже поздно управлять тем, где он загружается в память, или проверять, есть ли вообще у клиента право создавать компонент.

Проблема состоит в том, как управлять созданием компонента. Нам не нужно беспокоиться об управлении инициализацией компонента. Компонент нетрудно инициализировать через интерфейс, который можно запросить после создания. Но Вы не можете получить интерфейс компонента до тех пор, пока тот не создан, — а тогда уже поздно задавать условия создания.

Решение состоит в том, чтобы явно использовать другой компонент, единственным назначением которого будет создание нужного нам компонента.

Фабрики класса

На самом деле CoCreateInstance не создает компоненты непосредственно. Вместо этого она создает компонент, называемый фабрикой класса (class factory) который затем и порождает нужный компонент. Фабрика класса — компонент, единственной задачей которого является создание других компонентов. Точнее, конкретная фабрика класса создает компоненты, сооответствующие только одному конкретному CLSID. Клиент использует поддерживаемые фабрикой класса интерфейсы для управления тем, как фабрика создает каждый компонент. Стандартный интерфейс создания компонентов — IClassfactory. Компоненты, создаваемые CoCreateInstance, порождаются именно при помощи этого интерфейса.

Теперь давайте посмотрим, как клиент может создать компонент, напрямую используя фабрику класса. Первый шаг — создание самой фабрики. Когда фабрика класса создана, мы используем некоторый интерфейс, подобный IClassFactory, для окончательного создания своего компонента.

Использование CoGetClassObject

CoCreateInstance получает CLSID и возвращает указатель на интерфейс компонента. Нам нужна эквивалентная функция, которая по CLSID возвращает указатель на интерфейс, принадлежащий фабрике класса данного CLSID. Такая функция в библиотеке СОМ существует и называется CoGetClassObject.

Объявление CoGetClassObject показано ниже:

HRESULT __stdcall CoGetClassObject(
const CLSID& clsid,
DWORD dwClsContext,
COSERVERINFO* pServerInfo, // Зарезервировано для DOOM
const IID& iid,
void** ppv
);
Как видите, CoGetClassObject очень похожа на CoCreateInstance. Первым параметром обеих функций является CLSID нужного компонента. Обеим также передается контекст выполнения — dwClsContext. Два последних параметра тоже совпадают. Но CoGetClassObject возвращает запрашиваемый указатель для фабрики класса, тогда как CoCreateInstance — для самого компонента. Отличаются эти функции только одним параметром. CoCreateInstance принимает указатель IUnknown, тогда как CoGetClassObject - указатель на COSERVERINFO. COSERVERINFO используется DCOM для управления доступом к удаленным компонентам. Мы рассмотрим эту структуру в гл. 10.

Повторю, существенное различие между CoGetClassObject и CoCreateInstance состоит в том, что первая возвращает указатель, относящийся к фабрике класса нужного нам компонента, а не к самому компоненту. Требуемый компонент создается при помощи интерфейса, указатель на который возвращает CoGetClassObject. Обычно это указатель IClassFactory.

IClassFactory

Стандартный интерфейс, который поддерживают фабрики класса для создания компонентов, — это IClassFactory. Объявление этого интерфейса приводится ниже:
interface IClassFactory : IUnknown
{
HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter,
const IID& iid,
void** ppv);
HRESULT __stdcall LockServer(BOOL bLock);
} ;
У IClassFactory есть две функции-члена, CreateInstance и LockServer. Обсуждение LockServer мы отложим до конца главы.
CreateInstance
IClassFactory::CreateInstance — это еще одна функция со знакомыми параметрами. Первый параметр, pUnknownOuter — указатель на интерфейс IUnknown. Это тот же самый указатель, что передается CoCreateInstance. Его мы рассмотрим в следующей главе при обсуждении агрегирования компонентор.

Два оставшихся параметра — те же, что у QueryInterface. С их помощью вызывающая функция может запросить интерфейс компонента одновременно с созданием последнего. Это экономит клиенту один вызов функции. Если компонент к тому же выполняется на удаленной машине, то так экономится и один цикл запрос — ответ по сети.

Самое интересное в CreateInstance — не те параметры, которые у нее есть, а параметр, которого у нее нет. IClassFactory::CreateInstance не получает в качестве параметра CLSID. Это означает, что данная функция может создавать компоненты, соответствующие только одному CLSID — тому, который был передан CoGetClassObject:
IClassFactory2
Microsoft уже объявила еще один интерфейс создания компонентов, дополняющий IClassFactory. IClassFactory2 добавляет к IClassFactory поддержку лицензирования или разрешения на создание. Клиент обязан передать фабрике класса при помощи IClassFactory2 корректный ключ или лицензию, прежде чем та будет создавать компоненты. С помощью IClassFactory2 фабрика класса может гарантировать, что клиент получил компонент легально и имеет право пользования. Я уверен, что это не последний интерфейс для создания компонентов.

CoCreateInstance vs. CoGetClassObject

Создание фабрики класса, получение указателя IClassFactory и затем создание компонента — это большая работа, которую приходится проделывать всякий раз при создании компонента. Вот почему в большинстве программ используется CoCreateInstance, а не CoGetClassObject. Однако, как я уже говорил, CoCreateInstance в действительности реализована при помощи СоGetClassObject. Ниже приведен код, показывающий, как это могло бы быть сделано:
HRESULT CoCreateInstance(const CLSID& cisid,
IDnknown* pUnknownOuter,
DWORD dwClsContext,
const IID& iid,
void* ppv)
{
	// Установить в NULL выходной параметр
	*ppv = NULL ;

	// Создать фабрику класса и получить
	// указатель на интерфейс IClassFactory
	IClassFactory* pIFactory = NULL ;
	HRESULT hr = CoGetClassObject(clsid, 
						dwClsContext,
						NULL,
						IID_IClassFactory,
						(void**)&pIFactory) ;
	if (SUCCEEOED(hr))
	{
		// Создать компонент
		hr = pIFactory->CreateInstance(pUnknownOuter, iid, ppv);

		// Освободить фабрику класса
		pIFactory->Release() ;
	}
	return hr;
}
CoCreateInstance вызывает CoGetClassObject и получает указатель на интерфейс IClassFactory фабрики класса. Затем с помощью полученного указателя СоCreateInstance вызывает IClassFactory::CreateInstance, результатом чего и является создание нового компонента.
Зачем нужна CoGetClassObject?
В большинстве случаев можно забыть о CoGetClassObject и создать компонент при помощи CoCreateInstance. Однако есть два случая, в которых следует использовать именно CoGetClassObject, а не CoCreateInstance. Во-первых должны использовать CoGetClassObject, если хотите создать объект с помощью интерфейса, отличного от IClassFactory. Так, если Вам нужен IClassFactory2, придется использовать CoGetClassObject. Во-вторых, если Вы хотите сразу создать много компонентов, то эффективнее будет один раз создать фабрику класса для всех компонентов, вместо того чтобы создавать я освобождать ее для каждого экземпляра компонента. CoGetClassObject дает клиенту столь необходимую возможность управления процессом создания.

Создавать компоненты вручную с помощью фабрики класса гораздо хлопотнее, чем позволить CoCreateInstance сделать все за Вас. Но если Вы запомните, что фабрика класса — это просто компонент, который создает другие компоненты, будет гораздо легче понять, как это делается.

Фабрики класса инкапсулируют создание компонентов

Мне бы хотелось отметить некоторые характеристики фабрик класса, прежде чем показать Вам, "как их реализовать. Во-первых, данный экземпляр фабрики класса создает компоненты, соответствующие только одному CLSID. Это очевидно, поскольку CoGetClassObject имеет в качестве параметра CLSID, a IClassFactory::CreateInstance нет. Во-вторых, фабрика класса для данного CLSID создается тем же разработчиком, который реализует соответствующий компонент. Компонент-фабрика класса в большинстве случаев содержится в той же DLL, что и создаваемый компонент. Конкретный экземпляр фабрики класса соответствует одному конкретному CLSID, и как фабрика, так и создаваемый ею компонент реализуются одним и тем же программистом. Поэтому фабрика класса может иметь и имеет специфические знания о создаваемом компоненте. Реализация фабрики класса с использованием специфической информации о создаваемом компоненте — отнюдь не программистская небрежность. Задача фабрики класса состоит в том, чтобы знать, как создается компонент, и инкапсулировать это знание так, чтобы максимально изолировать клиент от требований компонента.

Реализация фабрики класса

В этом разделе мы рассмотрим реализацию компонента, обращая особое внимание на реализацию фабрики класса. Но сначала посмотрим, как создаются сами фабрики класса.

Использование DIIGetClassObject

В гл. 5 функция CallCreateInstance вызывала для создания компонента функцию CreateInstance из DLL. Функции CoGetClassObject также нужна точка входа DLL для создания фабрики класса компонента, которая (фабрика — ред.) реализована в одной DLL с компонентом. Эта точка входа называется DllGetClassObject. CoGetClassObject вызывает функцию DllGetClassObject, которая в действительности и создает фабрику класса. DUGetClassObject объявлена так:
STDAPI DllGetClassObject(
const CLSID& cisid,
const IID& lid,
void** ppv
);
Три параметра этой функции Вам уже знакомы: это те же параметры, что передаются CoGetClassObject. Первый — идентификатор класса компонентов, которые будет создавать фабрика класса. Второй — идентификатор интерфейса фабрики, который желает использовать клиент. Указатель на этот интерфейс возвращается через третий параметр.

Весьма существенно, что DllGetClassObject передается CLSID. Этот параметр, позволяет одной DLL поддерживать несколько компонентов, так как по значению CLSID можно выбрать подходящую фабрику класса.

Общая картина

Набросок общей картины создания компонента представлен на рис. 7-1. Здесь Вы увидите основных «участников» процесса. Во-первых, это клиент, который инициирует запрос обращением к CoGetClassObject. Во-вторых, это библиотека СОМ, реализующая CoGetClassObject. В-третьих, это DLL. DLL содержит функцию DllGetClassObject, которая вызывается CoGetClassObject. Задача DllGetClassObject — создать запрошенную фабрику класса. Способ, которым она это делает, оставлен полностью на усмотрение разработчика, так как он скрыт от клиента.

После того как фабрика класса создана, клиент использует интерфейс IClassFactory для создания компонента. Как именно IClassFactory::CreateInstance создает компонент — дело разработчика. Как уже отмечалось, IClassFactory инкапсулирует этот процесс, поэтому при создании компонента фабрика класса может использовать специфические знания.


РИС. 7-1. Пронумерованные точки показывают последовательность создания клиентом компонента с помощью библиотеки СОМ и фабрики класса.

Теперь мы готовы рассмотреть реализацию компонента.

Листинг кода компонента

Реализация компонента и его фабрики класса показаны в листинге 7-2. Фабрика реализована классом C++ CFactory. Первое, что Вы должны заметить в CFactory, — это просто еще один компонент. Он реализует IUnknown так же, как и другие компоненты. Единственное отличие между реализациями CFactory и СА составляют наборы поддерживаемых интерфейсов.

Просматривая код, особое внимание уделите CFactory::CreateInstance и DllGetClassObject.

//
// Cmpnt.cpp
//
#include 
#include 

#include "Iface.h"      // Объявления интерфейсов
#include "Registry.h"   // Функции для работы с Реестром

// Функция трассировки
void trace(const char* msg) { cout << msg << endl ;}
///////////////////////////////////////////////////////////
//
// Глобальные переменные
//
static HMODULE g_hModule = NULL ;   // Описатель модуля DLL
static long g_cComponents = 0 ;     // Счетчик активных компонентов
static long g_cServerLocks = 0 ;    // Счетчик блокировок
// Дружественное имя компонента
const char g_szFriendlyName[] = "Inside COM, Chapter 7 Example" ;
// He зависящий от версии ProgID
const char g_szVerIndProgID[] = "InsideCOM.Chap07" ;
// ProgID
const char g_szProgID[] = "InsideCOM.Chap07.1" ;
///////////////////////////////////////////////////////////
//
// Компонент 
//
class CA : public IX,
           public IY 
{
public:
	// IUnknown
	virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;
	virtual ULONG __stdcall AddRef() ;
	virtual ULONG __stdcall Release() ;
	// Интерфейс IX
	virtual void __stdcall Fx() { cout << "Fx" << endl ;}
	// Интерфейс IY
	virtual void __stdcall Fy() { cout << "Fy" << endl ;} 
	// Конструктор
	CA() ;
	// Деструктор
	~CA() ;
private:
	// Счетчик ссылок
	long m_cRef ;
} ;
//
// Конструктор
//
CA::CA() : m_cRef(1)
{ 
	InterlockedIncrement(&g_cComponents) ; 
}
//
// Деструктор
//
CA::~CA() 
{ 
	InterlockedDecrement(&g_cComponents) ; 
	trace("Component:\t\tDestroy self.") ;
}
//
// Реализация IUnknown 
//
HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv)
{    
	if (iid == IID_IUnknown)
	{
		*ppv = static_cast(this) ; 
	}
	else if (iid == IID_IX)
	{
		*ppv = static_cast(this) ;
		trace("Component:\t\tReturn pointer to IX.") ; 
	}
	else if (iid == IID_IY)
	{
		*ppv = static_cast(this) ; 
		trace("Component:\t\tReturn pointer to IY.") ; 
	}
	else
	{
		*ppv = NULL ;
		return E_NOINTERFACE ;
	}
	reinterpret_cast(*ppv)->AddRef() ;
	return S_OK ;
}
ULONG __stdcall CA::AddRef()
{
	return InterlockedIncrement(&m_cRef) ;
}
ULONG __stdcall CA::Release() 
{
	if (InterlockedDecrement(&m_cRef) == 0)
	{
		delete this ;
		return 0 ;
	}
	return m_cRef ;
}
///////////////////////////////////////////////////////////
//
// Фабрика класса
//
class CFactory : public IClassFactory
{
public:
	// IUnknown
	virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;         
	virtual ULONG   __stdcall AddRef() ;
	virtual ULONG   __stdcall Release() ;
	// Интерфейс IClassFactory
	virtual HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter,
	                                         const IID& iid,
	                                         void** ppv) ;
	virtual HRESULT __stdcall LockServer(BOOL bLock) ; 

	// Конструктор
	CFactory() : m_cRef(1) {}
	// Деструктор
	~CFactory() { trace("Class factory:\t\tDestroy self.") ;}
private:
	long m_cRef ;
} ;
//
// Реализация IUnknown для фабрики класса
//
HRESULT __stdcall CFactory::QueryInterface(const IID& iid, void** ppv)
{    
	if ((iid == IID_IUnknown) || (iid == IID_IClassFactory))
	{
		*ppv = static_cast(this) ; 
	}
	else
	{
		*ppv = NULL ;
		return E_NOINTERFACE ;
	}
	reinterpret_cast(*ppv)->AddRef() ;
	return S_OK ;
}
ULONG __stdcall CFactory::AddRef()
{
	return InterlockedIncrement(&m_cRef) ;
}
ULONG __stdcall CFactory::Release() 
{
	if (InterlockedDecrement(&m_cRef) == 0)
	{
		delete this ;
		return 0 ;
	}
	return m_cRef ;
}
//
// Реализация IClassFactory
//
HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter,
                                           const IID& iid,
                                           void** ppv) 
{
	trace("Class factory:\t\tCreate component.") ;

	// Агрегирование не поддерживается.
	if (pUnknownOuter != NULL)
	{
		return CLASS_E_NOAGGREGATION ;
	}
	// Создать компонент.
	CA* pA = new CA ;
	if (pA == NULL)
	{
		return E_OUTOFMEMORY ;
	}

	// Получить запрошенный интерфейс.
	HRESULT hr = pA->QueryInterface(iid, ppv) ;

	// Освободить указатель на IUnknown.
	// (If QueryInterface failed, component will delete itself.)
	pA->Release() ;
	return hr ;
}

// LockServer
HRESULT __stdcall CFactory::LockServer(BOOL bLock) 
{
	if (bLock)
	{
		InterlockedIncrement(&g_cServerLocks) ; 
	}
	else
	{
		InterlockedDecrement(&g_cServerLocks) ;
	}
	return S_OK ;
}
///////////////////////////////////////////////////////////
//
// Экспортируемые функции
//
//
// Можно ли выгружать DLL?
//
STDAPI DllCanUnloadNow()
{
	if ((g_cComponents == 0) && (g_cServerLocks == 0))
	{
		return S_OK ;
	}
	else
	{
		return S_FALSE ;
	}
}
//
// Получить фабрику класса
//
STDAPI DllGetClassObject(const CLSID& clsid,
                         const IID& iid,
                         void** ppv)
{
	trace("DllGetClassObject:\tCreate class factory.") ;
	// Можно ли создать такой компонент?
	if (clsid != CLSID_Component1)
	{
		return CLASS_E_CLASSNOTAVAILABLE ;
	}
	// Создать фабрику класса.
	CFactory* pFactory = new CFactory ; // Счетчик ссылок устанавливается
							// в конструкторе в 1
	if (pFactory == NULL)
	{
		return E_OUTOFMEMORY ;
	}
	// Получить требуемый интерфейс.
	HRESULT hr = pFactory->QueryInterface(iid, ppv) ;
	pFactory->Release() ;
	return hr ;
}
//
// Регистрация сервера
//
STDAPI DllRegisterServer()
{
	return RegisterServer(g_hModule, 
	                      CLSID_Component1,
	                      g_szFriendlyName,
	                      g_szVerIndProgID,
	                      g_szProgID) ;
}
//
// Удаление сервера из Реестра
//
STDAPI DllUnregisterServer()
{
	return UnregisterServer(CLSID_Component1,
	                        g_szVerIndProgID,
	                        g_szProgID) ;
}
///////////////////////////////////////////////////////////
//
// Реализация модуля DLL
//
BOOL APIENTRY DllMain(HANDLE hModule,
                      DWORD dwReason,
                      void* lpReserved)
{
	if (dwReason == DLL_PROCESS_ATTACH)
	{
		g_hModule = hModule ;
	}
	return TRUE ;
}

Клиент:            Вызвать CoCreateInstance для
Клиент:            создания компонента и получения интерфейса IX
DllGetClassObject: Создать фабрику класса.
Фабрика класса:    Создать компонент.
Компонент:         Вернуть указатель на IX.
Фабрика класса:    Саморазрушение.
Клиент:            IX получен успешно.
Fx
Клиент:            Запросить интерфейс IY.
Компонент:         Вернуть указатель на IY.
Клиент:            IY получен успешно.
Fy
Клиент:            Освободить интерфейс IY.
Клиент:            Запросить интерфейс IZ.
Клиент:            Не могу получить интерфейс IZ.
Клиент:            Освободить интерфейс IX.
Компонент:         Саморазрушение.
Только что представленная реализация DllGetClassObject делает три вещи. Во-первых, она проверяет, соответствует ли запрос именно той фабрике, которую она умеет создавать. Затем при помощи операции new создается фабрика класса. Наконец, DllGetClassObject запрашивает у фабрики класса интерфейс, требуемый клиенту. Реализация IClassFactory::CreateInstance похожа на реализацию DllGetClassObject. Обе функции создают компонент и запрашивают у него интерфейс. IClassFactory::CreateInstance создает СА, тогда как DllGetClassObject — CFactory.

Обратите внимание, что DllGetClassObject обладает подробными знаниями о создаваемой ею фабрике класса, а IClassFactory::CreateInstance - о создаваемом компоненте. Эти функции изолируют клиент от деталей реализация компонента. Можно сделать эти функции пригодными для повторного использования, однако в том или ином месте им обязательно потребуются специфические знания о том, как создавать конкретную фабрику класса или конкретный компонент. Способы, используемые DllGetClassObject и IClassFactory::CreateInstance для создания компонентов, полностью оставлены на усмотрение разработчика. Повторно применимая реализация DllGetClassObject и IClassFactory::CreateInstance будет представлена в гл. 9.

Последовательность выполнения

Давайте подробно рассмотрим последовательность выполнения кодов клиента и компонента, приведенных в листингах 7-1 и 7-2. На рис. 7-2 эта последовательность представлена графически. Ось времени на рисунке направлена вниз. Пяти основным структурным элементам — клиенту, библиотеке СОМ, DLL, фабрике класса и компоненту — соответствуют отдельные колонки. С каждым элементом связана вертикальная линия. Сплошная линия означает, что данный элемент был создан и все еще существует. Пунктирная линия показывает, что элемент еще или уже не существует. Прямоугольники, нанесенные поверх линий, соответствуют временам выполнения операций. Горизонтальные линии — это вызовы функций, передающие управление от одного структурного элемента к другому.


РИС. 7-2. CoCreateInstance позаботится за клиента о множестве мелких деталей создания компонента.
Для простоты я опустил вызовы членов lUnknoun и другие мелкие подробности. Кратко рассмотрим содержание рисунка. Сначала клиент вызывает CoCreateInstance, которая реализована в библиотеке СОМ. CoCreateInstance реализована с помощью CoGetClassObject. CoGetClassObject отыскивает компонент в Реестре. Если компонент найден, то CoGetClassObject загружает DLL, являющуюся сервером компонента. После загрузки DLL CoGetClassObject вызывает DllGetClassObject. DllGetClassObject реализована DLL-сервером. Ее эадача — создать фабрику класса, что делается в данном примере при помощи оператора new C++. Кроме того, DllGetClassObject запрашивает у фабрики класса интерфейс ICIassFactory, который возвращается CoCreateInstance. Последняя вызывает метод Createlnstance этого интерфейса. В нашем примере IClassFactory::CreateInstance использует для создания компонента оператор new. Кроме того, она запрашивает у компонента интерфейс IX. Получив его, CoCreateInstance освобождает фабрику класса и возвращает указатель на IX клиенту. Затем клиент может использовать данный указатель для вызова методов компонента. Проще некуда.

Регистрация компонента

Компоненты DLL экспортируют четыре функции. Мы уже рассмотрели DllGetClassObject, которую функции библиотеки СОМ используют для создания фабрики класса. Три другие экспортируемые функции используются для регистрации компонента.

Функции DllRegisterServer и DllUnregisterServer регистрируют и удаляют из Реестра Windows информацию о компоненте. Мы кратко рассматривали их в гл. 6. Реализованы эти функции в файле REGISTRY.CPP. Я не буду объяснять их код — он достаточно прост, и при желании Вы сможете разобраться сами. Мы будем использовать тот же файл REGISTRY.H для регистрации компонентов и в последующих главах книги.

Make-файл примера этой главы содержит строку

regsvr32 -s Cmpnt.dll
после компиляции и компоновки файла CMPNT.DLL. Как пояснялось в предыдущей главе, REGSVR32.ЕХЕ вызывает функцию DllRegisterServer, т. е. фактически выполняет регистрацию компонента. Если Вы не запускали make-файл, этот шаг Вам придется сделать самостоятельно. Для удобства я привожу командный файл REGISTER.BAT, состоящий из одной этой команды. Аналогичные файлы для выполнения регистрации прилагаются и к примерам следующих глав.
DIIMain
Чтобы получить имя файла DLL и зарегистрировать его, функции DllRegisterServer нужен описатель содержащей ее DLL. Этот описатель передается функции DIIMain. В программах на C++ есть функция main, с которой начинается выполнение. Программы для Windows используют WinMain, а DLL — DllMain. Реализовать DIIMain для получения описателя модуля DLL нетрудно:
BOOL APIENTRY DllMain(HANDLE hModule,
				DWORD dwReason,
				void* IpReserved)
{
	if(dwReason == DLL_PROCESS_ATTACH)
	{
		g_hModule = hModule;
	}
	return TRUE;
}
Эта функция заносит описатель в глобальную переменную q_hModule, откуда его могут считать функции DllRegisterServer и DllUnregisterServer.

Несколько компонентов в одной DLL

Я уже говорил выше, что DllGetClassObject позволяет нам поддерживать несколько компонентов в одной DLL. Ключевой момент здесь — передача DllGetClassObject CLSID создаваемого компонента. Для каждого CLSID DllGetClassObject легко может создать особую фабрику класса (см. рис. 7-3).


РИС. 7-3. Одна DLL может содержать несколько компонентов.
Тот факт, что DLL может поддерживать много разных компонентов, — одна из причин, по которым DLL соответствует не компоненту, но серверу компонентов. Концепция DLL, предоставляющей клиенту компоненты по запросу, очень мощна. DLL — это средство распространения реализации компонентов.

Повторное применение реализации фабрики класса

Если Вы похожи на меня, то Вы терпеть не можете писать один и тот же код снова и снова. Необходимость иметь CFoctoryl, CFactory2 и CFactory3, которые будут снова и снова реализовывать один и тот же код для создания компонентов СА, СВ и СС, кажется излишней. В нашем случае в этих разных фабриках класса различались бы только следующие строки:
СА* рА = new СА ;
pA->QueryInterface(...) ;
Если Вы правильно спроектировали свою фабрику класса и компоненты, то Вам понадобится только одна реализация фабрики для всех компонентов. Я предпочитаю для каждого компонента создавать по Одной простой функции. Эта функция создает компонент при помощи операции new и возвращает указатель на IUnknown. Затем я строю из указателей на эти функции таблицу, индексируемую CLSID каждого компонента. DllGetClassObject просто отыскивает в таблице указатель нужной функции, создает фабрику класса и передает ей этот указатель. Затем, вместо того, чтобы непосредственно использовать операцию new, фабрика класса при помощи указателя вызывает соответствующую функцию создания компонента (см. рис. 7-4).


РИС. 7-4. Простая реализация фабрики класса может обслуживать несколько компонентов.
В гл. 9 мы реализуем повторно применимую фабрику класса, которая использует эту структуру.

Прежде чем идти дальше, я хотел бы подчеркнуть один важный момент. Даже если один и тот же код фабрики класса используется несколькими компонентами, данный экземпляр фабрики класса может создавать только компоненты, соответствующие одному CLSID. Соотношение между экземпляром фабрики класса и CLSID всегда будет один к одному. Хотя CFactory могла бы реализовывать все наши фабрики класса, любой конкретный экземпляр CFactory может создавать только компоненты одного CLSID. Это следствие того, что IClassFactory::CreateInstance не передается в качестве параметра CLSID.

Выгрузка DLL

Я достаточно долго избегал разговора о LockServer и DIICanUnloadNow. Теперь пора заняться и ими.

Как мы видели в гл. 5, когда клиент динамически связывается с компонентом, он должен загрузить DLL в память. В Win32 для этой цели используется, функция LoadLibrary(На самом деле библиотека СОМ использует CoLoadLibrary, которая обращается к LoadLibrary). Когда мы закончили работу с DLL, хотелось бы выгрузить из памяти. Не стоит засорять память неиспользуемыми компонентами. Библиотека СОМ предоставляет функцию CoFreeUnusedLibraries, которая, как следует из названия, освобождает, неиспользуемые библиотеки. Клиент должен периодически вызывать эту функцию в периоды бездействия.

Использование DIICanUnloadNow

Но как CoFreeUnusedLibraries определяет, какие из DLL больше не обслуживают ни одного компонента и могут быть выгружены? CoFreeUnusedLibraries опрашивает DLL, вызывая DIICanUnloadNow. DIICanUnloadNow сообщает СОМ, поддерживает ли данная DLL какие-либо объекты. Если DLL никого не обслуживает, CoFreeUnusedLibraries может ее выгрузить. Чтобы проверять, есть ли еще обслуживаемые компоненты, DLL поддерживает их счетчик. Для этого в CMPNT.CPP просто добавляется следующее объявление:
static long g_cComponents = 0 ;
Затем IClassFactory::CreateInstance или конструктор компонента увеличивают значение g_cComponents, а деструктор компонента уменьшает его. DIICanUnloadNow дает положительный ответ тогда, когда g_cComponents равна 0.

LockServer

Обратите внимание, что я подсчитываю только обслуживаемые DLL в данный момент компоненты, но не фабрики класса. Может показаться, что логичнее было бы подсчитывать фабрики класса вместе с самими компонентами. Для серверов внутри процесса Вы, если хотите, можете подсчитывать и их. Но в гл. 10 мы познакомимся с локальными серверами, которые реализуются в ЕХЕ, а не в DLL. С «внутренней» точки зрения, начало и конец работы у серверов внутри и вне процесса отличаются. (С точки зрения «внешней» клиент не видит никаких различий.) Сервер вне процесса запускается таким способом, что внутри него мы не можем подсчитывать фабрики класса, не попадая в порочный круг, в котором сервер никогда не освобождает себя. Более подробно это обсуждается в гл. 10. Достаточно сказать, что присутствие работающей фабрики класса не гарантирует, что сервер будет удерживаться в памяти.

Это создает трудную ситуацию. Выгрузка DLL, у которой имеются исполняющиеся фабрики класса, может вызвать проблемы у клиентов. Предположим, что у клиента имеется указатель на фабрику класса, а соответствующая DLL выгружена. Если клиент попытается использовать указатель на IClassFactory, произойдет сбой. Клиенту необходим некоторый способ удержания DLL в памяти, если он собирается использовать указатель IClassFactory за пределами одной функции. IClassFactory::LockServer позволяет клиенту удерживать сервер в памяти до завершения работы. Клиент просто вызывает LockServer(TRUE) для блокирования сервера и LockServer(FALSE) для деблокирования.

Реализовать LockServer теперь можно простым увеличением и уменьшением счетчика g_cComponents. Многие, и я в их числе, предпочитают использовать отдельные счетчики для компонентов и блокировок. В этом случае DllCanUnloadNow должна проверять на равенство нулю оба счетчика.

Резюме

1 В большинстве случаев для создания компонентов пользуются CoCreateInstance. Однако иногда эта функция не дает достаточной гибкости. Тогда можно воспользоваться CoGetClassObject, которая дает возможность прямо управлять фабрикой класса и интерфейсом, используемым для создания компонентов. Стандартный для создания компонентов интерфейс -IClassFactory, который используется и CoCreateInstance.

Независимо от того, использует ли клиент CoCreateInstance или CoGetClassObject, у компонента всегда имеется отдельная фабрика класса. Фабрика класса - это компонент, создающий компонент. Фабрика класса компонента обычно реализует IClassFactory. Однако некоторые компоненты предъявляют особые требования к процессу создания. Такие компоненты вместо (или в дополнение) к IClassFactory будут реализовывать и другие интерфейсы. Если бы мы не могли собирать,из деталей Lego конструкции, играть с ними было бы не слишком весело, даже при наличии Домашней Фабрики Пластиковых Деталей. Изготовление деталей - только часть процесса, который описывает эта книга. По-настоящему интересные вещи начинаются тогда, когда мы собираем детали вместе. Так и компоненты становятся понастоящему интересными, только когда Вы соединяете их вместе, в новую цельную структуру В следующей главе мы увидим, как построить новые компоненты из уже имеющихся.

[Назад] [Содержание] [Вперед]


Смотрите также:
Всё о программах на жильё - ПРОГРАММЫ ВОССТАНОВЛЕНИЯ ЖИЛЬЯ.



Hosted by uCoz