WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 | 2 || 4 | 5 |

«Основы 2-е издание, исправленное и переработанное Дейл Роджерсон Оглавление ОТ АВТОРА ...»

-- [ Страница 3 ] --

Некоторые функции библиотеки COM Всем клиентам и компонентам СОМ приходится выполнять много типовых операций. Чтобы сделать выполнение этих операций стандартным и совместимым, СОМ предоставляет библиотеку функций. Библиотека реализована в OLE32.DLL. Для статической компоновки с ней Вы можете использовать OLE32.LIB. В этом разделе мы рассмотрим некоторые из важных типовых операций.

Инициализация библиотеки COM Во-первых, рассмотрим инициализацию самой библиотеки СОМ. Процесс должен вызвать CoInitialize для инициализации библиотеки, прежде чем использовать ее функции (за исключением функции CoBuildVersion, возвращающей номер версии библиотеки). Когда процесс завершает работу с библиотекой СОМ, он должен вызвать CoUninitialize. Прототипы этих функций приведены ниже:

// Значение параметра должно быть NULL HRESULT CoInitialize(void* reserved);

void CoUninitialize();

Библиотека СОМ требует инициализации только один раз для каждого процесса. Много кратные вызовы процессом CoInitialize допустимы, но каждому из них должен соответствовать отдельный вызов CoUninitialize.

Если CoInitilialize уже была вызвана данным процессом, то она возвращает не S_OK, а S_FALSE.

Поскольку в данном процессе библиотеку СОМ достаточно инициализировать лишь один раз, и поскольку эта библиотека используется для создания компонентов, компонентам в процессе не требуется инициализировать библиотеку. По общему соглашению СОМ инициализируется в EXE, а не в DLL.

Использование OleInitialize OLE, построенная «поверх» СОМ, добавляет поддержку библиотек типов, буфера обмена, перетаскивания мышью, документов ActiveX, Автоматизации и управляющих элементов ActiveX. Библиотека OLE содержит дополнительную поддержку этих возможностей. Если Вы хотите использовать все это, следует вызывать OleInitialize и OleUninitialize вместо CoInitialize и CoUninitialize. Обычно проще всего вызвать функции Ole* и забыть о них. Функции Ole* вызывают соответствующие функции Com*. Однако использование Ole* вместо Com* приводит к излишним расходам ресурсов и времени, если расширенные возможности не используются.

CoInitializeEx В операционных системах Windows, поддерживающих DCOM, Вы можете использовать CoInitializeEx, чтобы пометить компонент как использующий модель свободных потоков (free-threaded). Более подробная информация о CoInitializeEx содержится в гл. 12.

Управление памятью Очень часто внутренняя функция компонента выделяет блок памяти, который возвращается клиенту через выходной параметр. Но кто и каким образом будет эту память освобождать? Наибольшая проблема связана с тем, кто освободит память, — ведь клиент и компонент могут быть реализованы разными людьми, написаны на разных языках и даже выполняться в разных процессах. Необходим стандартный способ выделения и освобождения такой памяти.

Решение предоставляет менеджер памяти задачи (task memory allocator) СОМ. С его помощью компонент может передать клиенту блок памяти, который тот будет в состоянии освободить. Кроме того, менеджер «гладко» работает с потоками, поэтому его можно применять в многопоточных приложениях.

Как обычно, менеджер используется через интерфейс. В данном случае интерфейс называется Imalloc и возвращается функцией CoGetMalloc. IMalloc::Alloc выделяет блок памяти, а IMalloc::Free освобождает память, выделенную с помощью IMalloc::Alloc. Однако обычно вызывать CoGetMalloc для получения указателя на интерфейс, вызывать с помощью этого указателя функцию и затем освобождать указатель — значит делать слишком много работы. Поэтому библиотека СОМ предоставляет удобные вспомогательные функции — CoTaskMemAlloc и CoTaskMemFree:

void* CoTaskMemAlloc( // Размер выделяемого блока в байтах ULONG cb };

void CoTaskMemFree( // Указатель на освобождаемый блок памяти void* pv };

Память, выделенную и переданную при помощи выходного параметра, всегда освобождает вызывающая процедура (пользующаяся CoTaskMemFree).

Преобразование строк в GUID В Реестре содержатся строковые представления CLSID. Поэтому нам нужны специальные функции для преобразования CLSID в строку и обратно. В библиотеке СОМ имеется несколько удобных функций такого рода.

StringFromGUID2 конвертирует GUID в строку:

wchar_t szCLSID[39];

int r = ::StringFromGRUID2(CLSID_Component1, szCLSID, 39);

StringFromGUID2 генерирует строку символов Unicode, т.е. строку двухбайтовых символов типа wchar_t, а не char. В системах, не использующих Unicode, Вам придется преобразовать результат в char. Для этого можно прибегнуть к функции ANSI wcstombs, как показано ниже.

#ifndef _UNICODE // Преобразование из строки Unicode в обычную char szCLSID_single[39];

wcstombs(szCLSID_single, szCLSID, 39);

#endif Есть еще несколько функций, выполняющих аналогичные операции:

Функция Назначение StringFromCLSID Безопасное с точки зрения приведения типов преобразование CLSID в строку Функция Назначение StringFromIID Безопасное с точки зрения приведения типов преобразование IID в строку StringFromGUID2 Преобразование GUID в текстовую строку;

строка возвращается в буфер, выделенный вызывающей программой CLSIDFromString Безопасное с точки зрения приведения типов преобразование строки в CLSID IIDFromString Безопасное с точки зрения приведения типов преобразование строки в IID Некоторые их этих функций требуют использовать менеджер памяти задачи из предыдущего раздела:

wchar_t* string;

// Получить строку из CLSID ::StringFromCLSID(CLSID_Component1, &string);

// Использовать строку...

// Освободить строку ::CoTaskMemFree(string);

Резюме Строим ли мы дома (например, в гостиной) самолет, пишем ли ночами книгу или разрабатываем компоненты, большая часть нашего времени и энергии уходит на тысячи деталей. Внимание к деталям и правильное обращение с ними определяет успех. В этой главе Вы узнали, что СОМ использует HRESULT для возвращения кодов успеха или ошибки. Вы узнали о GUID — удивительной структуре данных, которая основана на алгоритме, позволяющем кому угодно, где угодно и когда угодно получать уникальный идентификатор. Вы также видели, что СОМ использует GUID для идентификации практически всех объектов, в том числе компонентов (CLSID) и интерфейсов (IID).

Вы узнали и о том, как CLSID транслируется в имя файла компонента с помощью Реестра Windows. Для регистрации компонента программа установки или REGSVR32.EXE вызывает функцию DllRegisterServer, экспортированную DLL компонента. В минимальном варианте компонент помещает в Реестр свой CLSID и имя файла.

В следующей главе мы увидим, как СОМ создает компонент при помощи CLSID. Это гораздо проще, чем построить самолет в гостиной.

Замечание о макросах определения интерфейсов Существуют макросы, облегчающие программистам переход с C на C++;

они помогают добиться того, чтобы одно и то же определение интерфейса работало в программах на обоих языках. Эти макросы есть как в OBJBASE.H, так и в BASETYPS.H. Ранее в примерах я использовал следующий простой интерфейс:

interface IX : IUnknown { virtual void stdcall Fx() = 0;

};

При использовании упомянутых макросов этот интерфейс выглядит так:

DECLARE_INTERFACE(IX, IUnknown) { // IUnknown STDMETHOD(QueryInterface) (THIS_ REFID, PPVOID) PURE;

STDMETHOD_(ULONG, AddRef) (THIS) PURE;

STDMETHOD_(ULONG, Release) (THIS) PURE;

// IX STDMETHOD_(void, Fx) (THIS) PURE;

} Однако сам я не использую эти макросы, предпочитая писать код так, чтобы он выглядел и работал, как код на С++. Если бы я собирался публиковать свои компоненты, чтобы их использовали другие люди, то писал бы интерфейс на специальном языке. Этот язык описания интерфейсов, называемый IDL, рассматривается в гл.

10 и 11.

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

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

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

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

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

Прототип CoCreateInstance Объявление CoCreateInstance приведено ниже:

HRESULT stdcall CoCreateInstance( const CLSID& clsid, // Внешний компонент IUnknown* pUnknownOuter, // Контекст сервера DWORD dwClsContext, const IID& iid, void** ppv );

У функции четыре входных параметра (in) и единственный выходной (out). Первый параметр — CLSID создаваемого компонента. Второй параметр используется для агрегирования компонентов и будет обсуждаться в следующей главе. Третий параметр — dwClsContext — ограничивает контекст исполнения компонента, с которым данный клиент может работать. Этот параметр мы рассмотрим позже.

Четвертый параметр, iid — это IID интерфейса, который мы хотим использовать для работы с компонентом.

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

Использование CoCreateInstance Используется CoCreateInstance так же просто, как и QueryInterface:

// Создать компонент IX* pIX = NULL;

HRESULT hr = ::CoCreateInstance( CLSID_Component1, NULL, CLSCTX_INPROC_SERVER, IID_IX, (void**)&pIX );

if (SUCCEEDED(hr)) { pIX->Fx();

pIX->Release();

};

В данном примере мы создаем компонент, задаваемый CLSID_Component1. Мы не агрегируем его, поэтому значением второго параметра является NULL. В следующей главе мы будем передавать функции значение, отличное от NULL. Параметр CLSCTX_INPROC_SERVER заставляет CoCreateInstance загружать только те компоненты, которые содержатся в серверах в процесса или в DLL.

Значения, передаваемые CoCreateInstance в качестве двух последних параметров, — те же самые, которые мы передавали бы QueryInterface. В данном примере мы передаем IID_IX, чтобы запросить интерфейс IX, который возвращается указателем pIX. Если вызов 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).

Значение CLSCTX_REMOTE_SERVER добавляется к CLSCTX_ALL и CLSCTX_SERVER, только если перед включением OBJBASE.H Вы определили символ препроцессора _WIN32_WINNT большим или равным 0x0400.

(Тот же эффект даст определение перед включением OBJBASE.H символа препроцессора _WIN32_DCOM.) Предупреждение: если Вы передадите функции CoCreateInstance значение CLSCTX_REMOTE_SERVER на системе, которая не поддерживает DCOM, то CoCreateInstance возвратит ошибку E_INVALIDARG. Это легко может случиться, если Вы компилируете свою программу с _WIN32_WINNT, большим или равным 0x0400, и затем запускаете ее в системе Microsoft Windows NT 3.51 или Microsoft Windows 95, которые не поддерживают DCOM. Более подробно CLSCTX_LOCAL_SERVER и CLSCTX_REMOTE_SERVER рассматриваются в гл. 10.

Таблица 7-1 Предопределенные комбинации признаков контекста исполнения Константы Значения CLSCTX_INPROC CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER CLSCTX_ALL CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER CLSCTX_SERVER CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER Листинг кода клиента В качестве примера в этой главе мы создадим наши первые настоящие клиент и компонент СОМ. Копии всех исходных файлов находятся на прилагающемся диске. Листинг 7-1 содержит код клиента. Единственным его существенным отличием от клиентов в гл. 5 является создание компонента с помощью CoCreateInstance. Среди других особенностей можно назвать использование CoInitialize и CoUninitialize для инициализации библиотеки СОМ (как обсуждается в гл. 6).

CLIENT.CPP // // Client.cpp – реализация клиента // #include #include #include "Iface.h" void trace(const char* msg) { cout << "Клиент: \t\t" << msg << endl;

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

trace("Вызвать CoCreateInstance для создания");

trace(" компонента и получения интерфейса IX");

IX* pIX = NULL;

HRESULT hr = ::CoCreateInstance(CLSID_Component1, NULL, CLSCTX_INPROC_SERVER, IID_IX, (void**)&pIX);

if (SUCCEEDED(hr)) { trace("IX получен успешно");

// Использовать интерфейс IX pIX->Fx();

trace("Запросить интерфейс IY");

IY* pIY = NULL;

hr = pIX->QueryInterface(IID_IY, (void**)&pIY);

if (SUCCEEDED(hr)) { trace("IY получен успешно");

// Использовать интерфейса IY pIY->Fy();

pIY->Release();

trace("Освободить интерфейс IY");

} else { trace("Не могу получить интерфейс IY");

} trace("Запросить интерфейс IZ");

IZ* pIZ = NULL;

hr = pIX->QueryInterface(IID_IZ, (void**)&pIZ);

if (SUCCEEDED(hr)) { trace("Интерфейс IZ получен успешно");

pIZ->Fz();

pIZ->Release();

trace("Освободить интерфейс IZ");

} else { trace("Не могу получить интерфейс IZ");

} trace("Освободить интерфейс IX");

pIX->Release();

} else { cout << "Клиент: \t\tНе могу создать компонент. hr = " << hex << hr << endl;

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

return 0;

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

Выше мы видели, что 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, // Зарезервировано для DCOM COSERVERINFO* pServerInfo, 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.

IClassFactory Microsoft уже объявила еще один интерфейс создания компонентов, дополняющий IClassFactory.

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

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

HRESULT CoCreateInstance(const CLSID& clsid, IUnknown* 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 (SUCCEEDED(hr)) { // Создать компонент hr = pIFactory->CreateInstance(pUnknownOuter, iid, ppv);

// Освободить фабрику класса pIFactory->Release();

} return hr;

} CoCreateInstance вызывает CoGetClassObject и получает указатель на интерфейс IClassFactory фабрики класса.

Затем с помощью полученного указателя CoCreateInstance вызывает IClassFactory::CreateInstance, результатом чего и является создание нового компонента.

Зачем нужна CoGetClassObject?

В большинстве случаев можно забыть о CoGetClassObject и создать компонент при помощи CoCreateInstance.

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

CoGetClassObject дает клиенту столь необходимую возможность управления процессом создания.

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

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

Конкретный экземпляр фабрики класса соответствует одному конкретному CLSID, и как фабрика, так и создаваемый ею компонент реализуются одним и тем же программистом. Поэтому фабрика класса может иметь и имеет специфические знания о создаваемом компоненте. Реализация фабрики класса с использованием специфической информации о создаваемом компоненте — отнюдь не программистская небрежность. Задача фабрики класса состоит в том, чтобы знать, как создается компонент, и инкапсулировать это знание так, чтобы максимально изолировать клиент от требований компонента.

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

Использование DllGetClassObject В гл. 5 функция CallCreateInstance вызывала для создания компонента функцию CreateInstance из DLL. Функции CoGetClassObject также нужна точка входа DLL для создания фабрики класса компонента, которая (фабрика — ред.) реализована в одной DLL с компонентом. Эта точка входа называется DllGetClassObject. CoGetClassObject вызывает функцию DllGetClassObject, которая в действительности создает фабрику класса. DllGetClassObject объявлена так:

STDAPI DllGetClassObject( const CLSID& clsid, const IID& iid, void** ppv );

Три параметра этой функции Вам уже знакомы: это те же параметры, что передаются CoGetClassObject. Первый — идентификатор класса компонентов, которые будет создавать фабрика класса. Второй — идентификатор интерфейса фабрики, который желает использовать клиент. Указатель на этот интерфейс возвращается через третий параметр.

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

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

После того, как фабрика класса создана, клиент использует интерфейс IClassFactory для создания компонента.

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

Клиент Библиотека COM DLL Создает 1 Вызывает CoGetClassObject DllGetClassObject фабрику CoGetClassObject класса Возвращает IClassFactory Вызывает 5 IClassFactory::CreateInstance IClassFactory pIClassFactory Возвращает IX 8 Вызывает IX::Fx pIX IX Создает компонент Рис. 7-1 Пронумерованный точки показывают последовательность создания клиентом компонента с помощью библиотеки СОМ и фабрики класса Теперь мы готовы рассмотреть реализацию компонента.

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

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

CMPNT.CPP // // 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";

// Не зависящий от версии 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("Компонент:\t\tСаморазрушение");

} // // Реализация 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("Компонент:\t\tВернуть указатель на IX");

} else if (iid == IID_IY) { *ppv = static_cast(this);

trace("Компонент:\t\tВернуть указатель на 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("Фабрика класса:\t\tСаморазрушение");

} 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("Фабрика класса:\t\tСоздать компонент");

// Агрегирование не поддерживается if (pUnknownOuter != NULL) { return CLASS_E_NOAGGREGATION;

} // Создать компонент CA* pA = new CA;

if (pA == NULL) { return E_OUTOFMEMORY;

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

// Освободить указатель на IUnknown // (При ошибке в QueryInterface компонент разрушит сам себя) 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:\tСоздать фабрику класса");

// Можно ли создать такой компонент?

if (clsid != CLSID_Component1) { return CLASS_E_CLASSNOTAVAILABLE;

} // Создать фабрику класса // Счетчик ссылок устанавливается CFactory* pFactory = new CFactory;

// в конструкторе в 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;

} Листинг 7-2 Полный код компонента, фабрики класса и функций, экспортируемых из DLL Клиент: Вызвать CoCreateInstance для Клиент: создания компонента и получения интерфейса IX Создать фабрику класса DllGetClassObject:

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

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

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

Клиент Библиотека COM DLL Фабрика класса Компонент CoCreateInstance CoGetClassObject DllGetClassObject new CFactory Время IClassFactory::CreateInstance(IID_IX) new CA IClassFactory::Release pIX->Fx() Рис. 7-2 CoCreateInstance позаботится за клиента о множестве мелких деталей создания компонента Для простоты я опустил вызовы членов IUnknown и другие мелкие подробности. Кратко рассмотрим содержание рисунка. Сначала клиент вызывает CoCreateInstance, которая реализована в библиотеке СОМ. CoCreateInstance реализована с помощью CoGetClassObject. CoGetClassObject отыскивает компонент в Реестре. Если компонент найден, то CoGetClassObject загружает DLL, являющуюся сервером компонента. После загрузки DLL CoGetClassObject вызывает DllGetClassObject. DllGetClassObject реализована DLL-сервером. Ее задача — создать фабрику класса, что делается в данном примере при помощи оператора new С++. Кроме того, DllGetClassObject запрашивает у фабрики класса интерфейс IClassFactory, который возвращается CoCreateInstance. Последняя вызывает метод CreateInstance этого интерфейса. В нашем примере IClassFactory::CreateInstance использует для создания компонента оператор new. Кроме того, она запрашивает у компонента интерфейс IX. Получив его, CoCreateInstance освобождает фабрику класса и возвращает указатель на IX клиенту. Затем клиент может использовать данный указатель для вызова методов компонента. Проще некуда.

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

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

Make-файл примера этой главы содержит строку regsvr32 –s Cmpnt.dll после компиляции и компоновки файла CMPNT.DLL. Как пояснялось в предыдущей главе, REGSVR32.EXE вызывает функцию DllRegisterServer, т. е. фактически выполняет регистрацию клиента. Если Вы не запускали make-файл, этот шаг Вам придется сделать самостоятельно. Для удобства я привожу командный файл REGISTER.BAT, состоящий из одной этой команды. Аналогичные файлы для выполнения регистрации прилагаются и к примерам следующих глав.

DllMain Чтобы получить имя файла DLL и зарегистрировать его, функции DllRegisterServer нужен описатель содержащей ее DLL. Этот описатель передается функции DllMain. В программах на C++ есть функция main, с которой начинается выполнение. Программы для Windows используют WinMain, а DLL — DllMain. Реализовать DllMain для получения описателя модуля DLL нетрудно:

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved) { if (dwReason == DLL_PROCESS_ATTACH) { g_hModule = hModule;

} return TRUE;

} Эта функция заносит описатель в глобальную переменную g_hModule, откуда его могут считать функции DllRegisterServer и DllUnregisterServer.

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

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

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

Повторное применение реализации фабрики класса Если Вы похожи на меня, то Вы терпеть не можете писать один и тот же код снова и снова. Необходимость иметь CFactory1, CFactory2 и CFactory3, которые будут снова и снова реализовывать один и тот же код для создания компонентов CA, CB и CC, кажется излишней. В нашем случае в этих разных фабриках класса различались бы только следующие строки:

CA* pA = new CA;

pA->QueryInterface(...);

Если Вы правильно спроектировали свою фабрику класса и компоненты, то Вам понадобится только одна реализация фабрики для всех компонентов. Я предпочитаю для каждого компонента создавать по одной простой функции. Эта функция создает компонент при помощи операции new и возвращает указатель на IUnknown. Затем я строю из указателей на эти функции таблицу, индексируемую CLSID каждого компонента. DllGetClassObject просто отыскивает в таблице указатель нужной функции, создает фабрику класса и передает ей этот указатель.

Затем, вместо того, чтобы непосредственно использовать операцию new, фабрика класса при помощи указателя вызывает соответствующую функцию создания компонента (см. рис. 7-4).

Клиент DLL CLSID_1 &CreateFunction_ Вызывает CoCreateInstance DllGetClassObject CLSID_2 &CreateFunction_ Фабрика класса 1 CLSID_n &CreateFunction_n Компонент 1 Компонент n CreateFunction_1 CreateFunction_n Рис. 7-4 Простая реализация фабрики класса может обслуживать неколько компонентов В гл. 9 мы реализуем повторно применимую фабрику класса, которая использует эту структуру.

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

Выгрузка DLL Я достаточно долго избегал разговора о LockServer и DllCanUnloadNow. Теперь пора заняться ими.

Как мы видели в гл. 5, когда клиент динамически связывается с компонентом, он должен загрузить DLL в память.

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

Использование DllCanUnloadNow Но как CoFreeUnusedLibraries определяет, какие из DLL больше не обслуживают ни одного клиента и могут быть выгружены? CoCreateUnusedLibraries опрашивает DLL, вызывая DllCanUnloadNow. DllCanUnloadNow сообщает СОМ, поддерживает ли данная DLL какие-либо объекты. Если DLL никого не обслуживает, CoFreeUnusedLibraries может ее выгрузить. Чтобы проверять, есть ли еще обслуживаемые компоненты, DLL поддерживает их счетчик. Для этого в CMPNT.CPP просто добавляется следующее объявление:

static long g_cComponents = 0;

Затем IClassFactory::CreateInstance или конструктор компонента увеличивают значение g_cComponents, а деструктор компонента уменьшает его. DllCanUnloadNow дает положительный ответ тогда, когда g_cComponents равна 0.

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

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

сервер в памяти до завершения работы. Клиент просто вызывает LockServer(TRUE) для блокирования сервера и LockServer(FALSE) для деблокирования.

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

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

Независимо от того, использует ли клиент CoCreateInstance или CoGetClassObject, у компонента всегда имеется отдельная фабрика класса. Фабрика класса — это компонент, создающий компонент. Фабрика класса компонента обычно реализует IClassFactory. Однако некоторые компоненты предъявляют особые требования к процессу создания. Такие компоненты вместо (или в дополнение) к IClassFactory будут реализовывать и другие интерфейсы.

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

8 глава Повторная применимость компонентов: включение и агрегирование Авторы статей в компьютерных журналах любят сравнивать СОМ с самыми разными вещами — например, миллионерами, горохом, С++ и компонентными архитектурами других фирм. Обычно в подобных статьях приводится какая-нибудь таблица вроде этой:

Таблица 8-1 Гипотетическая таблица из компьютерного журнала Свойство Миллионеры Горох С++ СОМ # # $ $ Съедобны % Поддерживают наследование # # # # $ $ $ Могут стать президентом Вы можете не обратить внимания на то, что миллионеры съедобны. Однако, читая эти статьи, Вы не сможете упустить из виду, что СОМ не поддерживает наследования. Авторы, кажется, не обращают внимания на то, что СОМ поддерживает полиморфизм — самую важную концепцию объектно-ориентированного программирования, или что СОМ — небольшая, элегантная и быстро работающая модель, или что компоненты СОМ могут прозрачно работать по сети. Их не интересует и то, что СОМ не зависит от языка программирования;

что компонентов СОМ написано больше, чем компонентов какого-либо другого типа. Их интересует только одно модное словцо — наследование!

Поддерживает ли СОМ наследование? И да, и нет. То, что подразумевается в журнальных статьях — это наследование реализации, которое имеет место, когда класс наследует свой код или реализацию от базового класса. Этот вид наследования СОМ не поддерживается. Однако СОМ поддерживает наследование интерфейса, т.е. наследование классом типа или интерфейса базового класса.

Многие необоснованно утверждают, что СОМ — плохая технология, так как она не поддерживает наследование реализации. Их аргументация напоминает мне о бурных «войнах» в Интернет — OS/2 против Windows, vi против Emacs, Java против Python и т.д. Я не участвую в таких спорах, ибо это пустая трата времени.

СОМ не поддерживает наследование реализации, потому что наследование реализации слишком тесно привязывает один объект к реализации другого. Если изменится реализация базового объекта, то производные объекты не будут работать, и их тоже нужно будет изменять. Для программы среднего размера на С++ это не очень трудно — у Вас есть доступ ко всем исходным текстам, и Вы можете изменить производные классы.

Однако для программ большего размера на изменение всех зависимых классов может уйти недопустимо много времени. Хуже того, у Вас может даже не быть доступа к исходным текстам. Именно поэтому эксперты по созданию больших программ на С++ настоятельно рекомендуют строить приложения на фундаменте абстрактных базовых классов.

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

Наследование реализации не обеспечивает такой защиты.

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

У наследования реализации своя область применения. В следующей главе я буду использовать его, чтобы избавиться от необходимости реализовывать IUnknown для каждого компонента. Но в этой главе мы остановимся на включении компонентов.

Включение и агрегирование Может быть, это национальная особенность, но, по-моему, мало кто в США доволен существующим положением дел. Мы всегда хотим все улучшить — поэтому постоянно изменяем все подряд, от прически до собственного дома. Так мы поступаем и с компонентами. После того, как кто-то дал Вам компонент, Вы наверняка захотите расширить или подстроить его под свои задачи. Кроме того, Вы можете захотеть использовать новый, усовершенствованный компонент. В С++ подстройка реализуется с помощью включения и наследования. В СОМ компоненты подстраиваются (специализируются) с помощью включения (containment) и агрегирования (aggregation).

Включение и агрегирование — это приемы программирования, в которых один компонент использует другой. Я называю эти два компонента внешним (outer component) и внутренним (inner component) соответственно.

Внешний компонент или агрегирует, или включает в себя внутренний.

Включение Включение в СОМ похоже на включение в С++. Однако, как и все в СОМ, включение выполняется на уровне интерфейсов. Внешний компонент содержит указатели на интерфейсы внутреннего. Внешний компонент — просто клиент внутреннего компонента. Используя интерфейсы последнего, он реализует свои собственные интерфейсы (рис. 8-1).

Внешний компонент IX IY Внутренний компонент IZ Рис. 8-1 Внешний компонент содержит внутренний компонент и использует его инетфрейс IZ.

Внешний компонент может также реализовывать заново интерфейс, поддерживаемый внутренним, передавая последнему вызовы этого интерфейса. Внешний компонент может специализировать этот интерфейс, добавляя свой код перед вызовом внутреннего компонента и после этого (рис. 8-2).

Внешний компонент IX IY Внутренний компонент IY Рис. 8-2 Внешний компонент содержит внутренний компонент и повторно использует его реалищацию интерфейса IY.

Агрегирование Агрегирование — это особый вид включения. Когда внешний компонент агрегирует интерфейс внутреннего компонента, он не реализует интерфейс последнего заново и не передает ему вызовы этого интерфейса явно (как при включении). Вместо этого внешний компонент передает указатель на интерфейс внутреннего компонента непосредственно клиенту. Далее клиент напрямую вызывает методы интерфейса, принадлежащего внутреннему компоненту. При таком подходе внешний компонент избавлен от необходимости реализовывать заново функции интерфейса и передавать вызовы внутреннему компоненту (рис. 8-3). Однако внешний компонент не может специализировать какие-либо функции интерфейса. После того, как внешний компонент передаст интерфейс клиенту, тот обращается к внутреннему компоненту самостоятельно. Клиент не должен знать, что он работает с двумя разными компонентами, так как это нарушит инкапсуляцию. Задача агрегирования — заставить внешний и внутренний компоненты вести себя как один компонент. Как Вы увидите далее, эта возможность достигается при помощи QueryInterface.

Внешний компонент IX Внутренний компонент IY Рис. 8-3 Когда внешний компонент агрегирует интерфейс, он передает укаатель на него непосредственно клиенту. Он не реализуется интерфейс заново для передачи вызовов внутреннему компоненту.

Сравнение включения и агрегирования Небольшой пример пояснит различия между включением и агрегированием. Предположим, что Вы хозяин небольшой металлоремонтной мастерской. У Вас есть две работницы — Памела и Анжела. Памела работает уже давно и знает свое дело досконально. Если заказ будет выполнять Памела, достаточно просто направить клиента к ней. Анжела, напротив, новичок и у нее нет опыта работы с металлом. Когда работу поручают ей, обычно нужно дать начальные указания, а иногда и сделать вместо нее наиболее сложную часть. Вы же должны и договариваться с клиентом. После того, как Анжела закончит работу, Вы вместе с ней должны проверить результаты и, может быть, что-то подправить. Случай Памелы сходен с подходом агрегирования. Дав работу, Вы уходите со сцены. Однако в случае Анжелы Вы по-прежнему ведете все переговоры с клиентом, даете начальные указания и проверяете работу в конце. Этот вариант аналогичен включению.

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

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

Поскольку агрегирование — это особый случай включения, мы рассмотрим включение первым.

Реализация включения Включение компонента выполняется столь же просто, как и использование. Каталог \CHAP08\CONTAIN на прилагающемся к книге диске содержит пример кода включения. В этом примере Компонент 1 — внешний;

он реализует два интерфейса: IX и IY. При этом он использует реализацию IY Компонентом 2 — внутренним, включаемым компонентом. Это в точности соответствует схеме рис. 8-2.

Клиент и внутренний компонент — практически те же самые, что и клиент с компонентом из предыдущей главы.

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

Нам остается рассмотреть только внешний компонент — Компонент 1, который включает Компонент 2. В приведенном ниже листинге 8-1 показано объявление и большая часть реализации Компонента 1. Я выделил полужирным некоторые участки кода, относящиеся к включению. Новая переменная-член m_pIY содержит указатель на интерфейс IY включаемого Компонента 2.

Код включения из CONTAIN\CMPNT /////////////////////////////////////////////////////////// // // Компонент // 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() { m_pIY->Fy();

} // Конструктор CA();

// Деструктор ~CA();

// Функция инициализации, вызываемая фабрикой класса для // создания включаемого компонента HRESULT stdcall Init();

private:

// Счетчик ссылок long m_cRef;

// Указатель на интерфейс IY включаемого компонента IY* m_pIY;

};

// // Конструктор // CA::CA() : m_cRef(1), m_pIY(NULL) { ::InterlockedIncrement(&g_cComponents);

} // // Деструктор // CA::~CA() { ::InterlockedDecrement(&g_cComponents);

trace("Самоликвидация");

// Освободить включаемый компонент if (m_pIY != NULL) { m_pIY->Release();

} } // Инициализация компонента путем создания включаемого компонента HRESULT stdcall CA::Init() { trace("Создать включаемый компонент");

HRESULT hr = ::CoCreateInstance(CLSID_Component2, NULL, CLSCTX_INPROC_SERVER, IID_IY, (void**)&m_pIY);

if (FAILED(hr)) { trace("Не могу создать включаемый компонент");

return E_FAIL;

} else { return S_OK;

} } Листинг 8-1 Компонент 1 создает экземпляр включаемого компонента и хранит указатель на интерфейс IY последнего Давайте рассмотрим, как работает этот код внешнего Компонента 1. Новый метод под названием Init создает внутренний Компонент 2 тем же самым способом, которым создают компоненты все клиенты, — посредством вызова CoCreateInstance. При этом внешний компонент запрашивает указатель на IY у внутреннего и, в случае успеха, сохраняет его в m_pIY.

В приведенном листинге не показаны реализация QueryInterface и функций внешнего IUnknown. Она абсолютно та же, что и в случае, когда включение не используется. Когда клиент запрашивает у Компонента 1 интерфейс IY, тот возвращает указатель на свой интерфейс. Затем, когда клиент вызывает метод этого интерфейса, Компонент передает вызов Компоненту 2. Это выполняет следующая строка:

virtual void Fy() { m_pIY->Fy();

} Когда Компонент 1 самоликвидируется, его деструктор вызывает Release для указателя m_pIY, в результате чего Компонент 2 также удаляет себя.

Фабрика класса Компонента 1 мало изменилась по сравнению с фабрикой класса из предыдущей главы.

Единственный новый момент — то, что функция CreateInstance вызывает после создания Компонента 1 его функцию Init. Код этой функции приведен в листинге 8-2.

Код функции CreateInstance из CONTAIN\CMPNT HRESULT stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter, const IID& iid, void** ppv) { // Агрегирование не поддерживается if (pUnknownOuter != NULL) { return CLASS_E_NOAGGREGATION;

} // Создать компонент CA* pA = new CA;

if (pA == NULL) { return E_OUTOFMEMORY;

} // Инициализировать компонент HRESULT hr = pA->Init();

if (FAILED(hr)) { // Ошибка при инициализации. Удалить компонент pA->Release();

return hr;

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

pA->Release();

return hr;

} Листинг 8-2 Фабрика класса внешнего компонента вызывает для вновь созданного компонента функцию Init Вот и все, что необходимо для реализации включения. Теперь рассмотрим, для чего включение может применяться.

Расширение интерфейсов Одно из основных применений включения — расширение интерфейса посредством добавления кода к существующему интерфейсу.

Рассмотрим пример. Имеется класс IAirplane (Самолет), который Вы хотите превратить в IFloatPlane (Гидросамолет). Определения интерфейсов приводятся ниже:

interface IAirplane : IUnknown { void TakeOff();

void Fly();

void Land();

};

interface IFloatPlane : IAirplane { void LandingSurface(UINT iSurfaceType);

void Float();

void Sink();

void Rust();

void DrainBankAccount();

};

Предположим, что IAirplane уже реализован в компоненте MyAirplane. Внешний компонент может просто включить MyAirplane и использовать его интерфейс IAirplane для реализации членов IAirplane, которые наследует интерфейс IFloatPlane:

void CmyFloatPlane::Fly() { m_pIAirplane->Fly();

} Другие члены IAirplane, вероятно, потребуется модифицировать, чтобы поддерживать взлет и посадку на воду:

void CmyFloatPlane::Land() { if (m_iLandingSurface == WATER) { WaterLanding();

} else { m_pIAirplane->Land();

} } Как видите, использовать включение в данном случае просто. Однако если в интерфейсе IAirplane много членов, то написание кода, передающего вызовы от клиента в MyAirplane, будет утомительным. По счастью, это не вопрос поддержки, так как после обнародования интерфейса он не изменяется. Агрегирование дает некоторую поблажку ленивому программисту, который не хочет реализовывать код для передачи вызовов внутренним объектам. Однако при использовании агрегирования к интерфейсу нельзя добавить свой код. Перейдем теперь к агрегированию.

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

Хотя поначалу агрегирование кажется простым, как мы увидим далее, правильная реализация интерфейса IUnknown внутреннего компонента представляет некоторые трудности. Магия агрегирования заключена в реализации QueryInterface. Давайте реализуем QueryInterface для внешнего компонента так, чтобы тот возвращал указатель на объект внутреннего.

C++ и агрегирование В С++ нет средства, эквивалентного агрегированию. Агрегирование — это динамический вид наследования, тогда как наследование С++ всегда статично. Наилучший способ моделирования агрегирования в С++ — это переопределение операции разыменования (operator ->). Эта техника будет рассматриваться при реализации smart-указателей в следующей главе. Переопределение operator -> связано с большими ограничениями, чем агрегирование СОМ. Вы можете передавать вызовы только одному классу, тогда как в СОМ можно агрегировать сколько угодно интерфейсов.

Магия QueryInterface Вот объявление внешнего компонента, который реализует интерфейс IX и предоставляет интерфейс IY посредством агрегирования.

class CA : public IX { 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;

} // Конструктор CA();

// Деструктор ~CA();

// Функция инициализации, вызываемая фабрикой класса для // создания включаемого компонента HRESULT Init();

private:

// Счетчик ссылок long m_cRef;

// Указатель на IUnknown внутреннего компонента IUnknown* m_pUnknownInner;

};

Обратите внимание, что по внешнему виду объявленного компонента нельзя сказать, что он поддерживает интерфейс IY: он не наследует IY и не реализует какие-либо его члены. Этот внешний компонент использует реализацию IY внутреннего компонента. Основные действия внешнего компонента происходят внутри его функции QueryInterface, которая возвращает указатель на интерфейс внутреннего объекта. В приведенном ниже фрагменте кода переменная-член m_pUnknownInner содержит адрес 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);

} else if (iid = IID_IY) { return m_pUnknownInner->QueryInterface(iid, ppv);

} else { *ppv = NULL;

return E_NOINTERFACE;

} reinterpret_cast(*ppv)->AddRef();

return S_OK;

} В этом примере QueryInterface внешнего компонента просто вызывает QueryInterface внутреннего. Все очень хорошо и просто, но если бы это еще правильно работало!

Проблема заключается не в приведенном коде — она в интерфейсе IUnknown внутреннего компонента.

Агрегированный внутренний компонент должен обрабатывать вызовы функций-членов QueryInterface особым образом. Как мы увидим, здесь фактически необходимы две реализации IUnknown.

Теперь давайте рассмотрим, почему обычная реализация IUnknown для внутреннего компонента вызывает проблемы. Затем познакомимся с тем, как для их устранения реализуются два интерфейса IUnknown. Я покажу также, как модифицировать создание внутреннего компонента, чтобы оба компонента получили нужные им указатели. Наконец, мы посмотрим, как внешнему компоненту передаются указатели на интерфейсы внутреннего (это более сложный процесс, чем Вам может показаться).

Неверный IUnknown Задача агрегирования — убедить клиента, что интерфейс, реализованный внутренним компонентом, реализован внешним. Вы должны напрямую передать клиенту указатель на интерфейс внутреннего компонента и внушить ему, что этот указатель принадлежит внешнему компоненту. Если клиенту передать указатель интерфейса, реализованного внутренним компонентом как обычно, то компонент будет представлен клиенту в расщепленном виде. Интерфейс внутреннего компонента использует реализацию QueryInterface внутреннего компонента, тогда как у внешнего компонента имеется своя собственная QueryInterface. Когда клиент прямо запрашивает интерфейсы внутреннего компонента, у него создается иное представление о возможностях компонента, чем если он запрашивает интерфейс у внешнего компонента (рис. 8-4).

Сказанное можно пояснить примером. Предположим, Вы агрегировали компонент. Внешний компонент поддерживает интерфейсы IX и IY. Он реализует IX и агрегирует IY. Внутренний компонент реализует интерфейсы IY и IZ. Создав внешний компонент, мы получаем указатель на его интерфейс IUnknown. С помощью этого интерфейса можно успешно запросить интерфейс IX или IY, но запрос IZ будет всегда возвращать E_NOINTERFACE. Если мы запросим указатель на IY, то получим указатель на интерфейс внутреннего компонента. Если запросить IZ через этот указатель на IY, то запрос будет успешным. Так получается из-за того, что функции интерфейса IUnknown для интерфейса IY реализованы внутренним компонентом. Точно так же, запрос у интерфейса IY интерфейса IX потерпит неудачу, поскольку внутренний компонент не поддерживает IX.

Такая ситуация нарушает фундаментальное правило реализации QueryInterface: если Вы можете попасть в определенное место откуда-нибудь, то туда можно попасть откуда угодно.

Внешний компонент IX QueryInterface Реализация IUnknown AddRef внешнего компонента Release Fx Внутренний компонент IY QueryInterface Реализация IUnknown внутреннего AddRef компонента Release Fy Рис. 8-4 Разные компоненты имеют разные реализации IUnknown Виноват здесь интерфейс IUnknown внутреннего компонента. Клиент видит два разных IUnknown, внутренний и внешний. Это сбивает его с толку — каждый из IUnknown реализует QueryInterface по-своему, и каждая QueryInterface поддерживает разные наборы интерфейсов. Клиент должен быть абсолютно независим от реализации компонента-агрегата. Он не должен знать, что внешний компонент агрегирует внутренний, и никогда не должен видеть IUnknown внутреннего компонента. Как было сказано в гл. 3, два интерфейса реализованы одним и тем же компонентом тогда и только тогда, когда оба они возвращают один и тот же указатель в ответ на запрос указателя на IUnknown. Следовательно, необходимо дать клиенту единственный IUnknown и скрыть от него IUnknown внутреннего компонента. Интерфейсы внутреннего компонента должны использовать интерфейс IUnknown, реализованный внешним компонентом. IUnknown внешнего компонента называют внешним IUnknown (outer unknown), или управляющим IUnknown (controlling unknown).

Интерфейсы IUnknown для агрегирования Самый простой для внутреннего компонента способ использовать внешний IUnknown — передавать ему вызовы своего IUnknown. Для этого внутренний компонент должен знать, что он агрегируется, и должен иметь указатель на внешний IUnknown.

Внешний IUnknown Из гл. 7 Вы помните, что функциям CoCreateInstance и IClassFactory::CreateInstance передается указатель на IUnknown, который мы не использовали:

HRESULT stdcall CoCreateInstance( const CLSID& clsid, // Внешний компонент IUnknown* pUnknownOuter, // Контекст сервера DWORD dwClsContext, const IID& iid, void** ppv );

HRESULT stdcall CreateInstance( IUnknown* pUnknownOuter, const IID& iid, void** ppv );

Внешний компонент передает указатель на свой интерфейс IUnknown внутреннему компоненту с помощью параметра pUnknownOuter. Если указатель на внешний IUnknown не равен NULL, то компонент агрегируется.

Используя указатель на IUnknown, переданный CreateInstance, компонент узнает, агрегируется ли он и кто его агрегирует. Если компонент не агрегируется, он использует собственную реализацию IUnknown. В противном случае он должен делегировать вызовы внешнему IUnknown.

Делегирующий и неделегирующий IUnknown Для поддержки агрегирования внутренний компонент фактически реализует два интерфейса IUnknown.

Неделегирующий (nondelegating) IUnknown реализует IUnknown внутреннего компонента обычным образом.

Делегирующий (delegating) IUnknown передает вызовы методов IUnknown либо внешнему IUnknown, либо неделегирующему IUnknown. Если внутренний компонент агрегируется, делегирующий IUnknown передает вызовы внешнему IUnknown, реализованному внешним компонентом. Клиенты агрегата вызывают делегирующий IUnknown, тогда как внешний компонент работает с внутренним через неделегирующий. Эту ситуацию поясняют рисунки: вариант без агрегирования показан на рис. 8-5, вариант с агрегированием — на рис.

8-6.

Неагрегированный компонент Реализация делегирующего IUnknown IY QueryInterface Реализация AddRef неделегирующего Release IUnknown Fy Рис. 8-5 Если компонент не агрегируется, его делегирующий IUnknown передает вызовы неделегирующему IUnknown На рис. 8-6 представлен компонент, агрегирующий IY. В этом случае делегирующий IUnknown вызывает IUnknown, реализованный внешним компонентом. Внешний компонент вызывает неделегирующий IUnknown для управления временем существования внутреннего компонента. Таким образом, когда некоторый компонент вызывает метод IUnknown через указатель на интерфейс IY, он вызывает делегирующий IUnknown, который перенаправляет вызов внешнему IUnknown. В результате внутренний компонент использует реализацию IUnknown внешнего компонента.

Теперь все, что мы должны сделать, — это реализовать делегирующий и неделегирующий IUnknown.

Внешний компонент IX QueryInterface Реализация внешнего AddRef IUnknown Release Код управления Fx внутренним компонентом Внутренний компонент Реализация IY делегирующего IUnknown QueryInterface Реализация AddRef неделегирующего Release IUnknown Fy Рис. 8-6 Если компонент агрегирован, его делегирующий IUnknown передает вызовы внешнему IUnknown Реализация делегирующего и неделегирующего IUnknown Нам необходимо иметь для компонента две разные реализации IUnknown. Но С++ не позволяет реализовать интерфейс дважды в одном классе. Следовательно, мы изменим имя одного из IUnknown, чтобы избежать конфликта имен. Я выбрал имя InondelegatingUnknown. Вы можете выбрать то, которое Вам больше нравится.

Вспомните, что для СОМ имена интерфейсов не имеют значения;

СОМ интересуется только структурой vtbl.

InondelegatingUnknown объявлен так же, как и IUnknown, за исключением того, что названия этих функций членов имеют префикс «Nondelegating».

struct InondelegatingUnknown { virtual HRESULT stdcall NondelegatingQueryInterface(const IID&, void**) = 0;

virtual ULONG stdcall NondelegatingAddRef() = 0;

virtual ULONG stdcall NondelegatingRelease() = 0;

};

Методы NondelegatingAddRef и NondelegatingRelease интерфейса InondelegatingUnknown реализованы в точности так же, как ранее были реализованы AddRef и Release для IUnknown. Однако в NondelegatingQueryInterface есть небольшое, но очень важное изменение.

HRESULT stdcall CB::NondelegatingQueryInterface(const IID& iid, void** ppv) { if (iid == IID_IUnknown) { *ppv = static_cast(this);

} else if (iid = IID_IY) { *ppv = static_cast(this);

} else { *ppv = NULL;

return E_NOINTERFACE;

} reinterpret_cast(*ppv)->AddRef();

return S_OK;

} Обратите внимание на приведение типа указателя this внутреннего компонента к INondelegatingUnknown. Это приведение очень важно. Преобразуя this к INondelegatingUnknown, мы гарантируем, что будет возвращен неделегирующий IUnknown. Неделегирующий IUnknown всегда возвращает указатель на себя, если у него запрашивается IID_IUnknown. Без этого приведения типа вместо неделегирующего IUnknown возвращался бы делегирующий. Когда компонент агрегируется, делегирующий IUnknown передает все вызовы QueryInterface, Release и AddRef внешнему объекту.

Клиенты агрегируемого компонента никогда не получают указатели на неделегирующий IUnknown внутреннего компонента. Всякий раз, когда клиент запрашивает указатель на IUnknown, он получает IUnknown внешнего компонента. Указатель на неделегирующий IUnknown внутреннего компонента передается только внешнему компоненту. Теперь рассмотрим, как реализовать делегирующий IUnknown.

Реализация делегирующего IUnknown К счастью, реализация делегирующего IUnknown проста — она передает вызовы либо внешнему, либо неделегирующему IUnknown. Далее приводится объявление компонента, поддерживающего агрегирование.

Компонент содержит указатель m_pUnknownOuter. Если компонент агрегирован, указатель ссылается на внешний IUnknown. Если компонент не агрегирован, этот указатель ссылается на неделегирующий IUnknown. Всякий раз при обращении к делегирующему IUnknown вызов переадресуется интерфейсу, на который указывает m_pUnknownOuter. Делегирующий IUnknown реализован функциями, подставляемыми в строку (inline):

class CB : public IY, INondelegatingUnknown { public:

// Делегирующий IUnknown virtual HRESULT stdcall QueryInterface(const IID& iid, void** ppv) { // Делегировать QueryInterface return m_pUnknownOuter->QueryInterface(iid, ppv);

} virtual ULONG stdcall AddRef() { // Делегировать AddRef return m_pUnknownOuter->AddRef();

} virtual ULONG stdcall Release() { // Делегировать Release return m_pUnknownOuter->Release();

} // Неделегирующий IUnknown virtual HRESULT stdcall NondelegatingQueryInterface(const IID& iid, void** ppv);

virtual ULONG stdcall NondelegatingAddRef();

virtual ULONG stdcall NondelegatingRelease();

// Интерфейс IY virtual void Fy() { cout << “Fy” << endl;

} // Конструктор CB(IUnknown* pUnknownOuter);

// Деструктор ~CB();

private:

long m_cRef;

IUnknown* m_pUnknownOuter;

};

Создание внутреннего компонента Теперь, когда мы знаем, как реализовать внутренний компонент, обсудим, как он создается внешним компонентом. Чтобы пройти весь процесс создания, от начала до конца, рассмотрим код трех функций: функция Init внешнего компонента начинает процесс;

затем вступает функция фабрики класса CreateInstance и конструктор внутреннего компонента.

Функция Init внешнего компонента Первое, что делает внешний компонент при агрегировании — создает внутренний компонент. Основное различие между включением и агрегированием состоит в том, что во втором случае внешний компонент передает внутреннему внешний IUnknown. Приведенный ниже фрагмент кода показывает, как внешний компонент создает внутренний. Обратите внимание, что второй параметр CoCreateInstance — это указатель на интерфейс IUnknown внешнего компонента.

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

Фабрика класса будет возвращать указатель на неделегирующий IUnknown внутреннего компонента. Как мы уже видели, это указатель необходим внешнему компоненту для передачи вызовов QueryInstance внутреннему компоненту. Здесь внешний компонент обязан запрашивать указатель на IUnknown;

в противном случае он никогда не сможет получить его. Внешний компонент должен сохранить неделегирующий IUnknown внутреннего компонента для последующего использования.

В данном примере нет необходимости явно приводить указатель this к указателю на IUnknown, так как CA наследует только IX, и, следовательно, неявное преобразование не будет неоднозначным.

HRESULT CA::Init() { IUnknown* pUnknownOuter = this;

HRESULT hr = CoCreateInstance(CLSID_Component2, pUnknownOuter, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&m_pUnknownOuter);

if (FAILED(hr)) { return E_FAIL;

} return S_OK;

} Реализация IClassFactory::CreateInstance внешнего компонента вызывает CA::Init. В других отношениях реализация IClassFactory внешнего компонента остается неизменной. Фабрика же класса внутреннего компонента подверглась некоторым изменениям, которые мы теперь и рассмотрим.

Функция IClassFactory::CreateInstance внутреннего компонента Реализация IClassFactory::CreateInstance внутреннего компонента изменена, чтобы использовать InondelegatingUnknown вместо IUnknown. Код этой функции приведен ниже, отличия от предыдущих вариантов CreateInstance не возвращает автоматически ошибку, если pUnknownOuter не равен NULL (т.е. когда внешний компонент желает агрегировать внутренний). Однако CreateInstance обязана возвратить ошибку, если при этом iid отличен от IID_IUnknown. Когда компонент создается как внутренний в агрегате, он может возвратить только интерфейс IUnknown, иначе внешний компонент никогда не получил бы неделегирующего IUnknown (поскольку вызовы QueryInterface будут делегированы внешнему IUnknown).

HRESULT stdcall Cfactory::CreateInstance(IUnknown* pUnknownOuter, const IID& iid, void** ppv) { // При агрегировании iid должен быть IID_IUnknown if ((pUnknownOuter != NULL) && (iid != IID_IUnknown)) { return CLASS_E_NOAGGREGATION;

} // Создать компонент CB* pB = new CB(pUnknownOuter);

if (pB == NULL) { return E_OUTOFMEMORY;

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

PB->NondelegatingRelease();

return hr;

} Для получения запрашиваемого интерфейса вновь созданного внутреннего компонента показанная выше функция CreateInstance вызывает не QueryInterface, а NondelegatingQueryInterface. Если внутренний компонент агрегируется, вызовы QueryInterface он будет делегировать внешнему IUnknown. Фабрика класса должна возвратить указатель на неделегирующий QueryInterface, поэтому он вызывает NondelegatingQueryInterface.

Конструктор внутреннего компонента В приведенном выше коде CreateInstance указатель на внешний IUnknown передается конструктору внутреннего компонента. Конструктор инициализирует m_pUnknownOuter, которая используется делегирующим IUnknown для передачи вызовов либо неделегирующему, либо внешнему IUnknown. Если компонент не агрегируется (pUnknownOuter есть NULL), конструктор помещает в m_pUnknownOuter указатель на неделегирующий IUnknown. Это показано в приведенном ниже фрагменте:

CB::CB(IUnknown* pUnknownOuter) : m_cRef(1) { ::InterlockedIncrement)&g_cComponents;

if (pUnknownOuter == NULL) { // Не агрегируется: использовать неделегирующий IUnknown m_pUnknownOuter = reinterpret_cast( static_cast(this) );

} else { // Агрегируется: использовать внешний IUnknown m_pUnknownOuter = pUnknownOuter;

} } Указатели внешнего компонента на интерфейсы внутреннего компонента Когда я реализовывал CA::Init при создании внутреннего компонента, я запрашивал интерфейс IUnknown, а не IY.

Однако наш компонент в действительности агрегирует IY. Поэтому неплохо было бы в самом начале проверить, поддерживается ли интерфейс IY внутренний компонент. Но, как указывалось выше, при агрегировании компонента внешний компонент может запрашивать только интерфейс IUnknown. Cfactory::CreateInstance возвращает CLASS_E_NOAGGREGATION, если ей передано что-либо, отличное от IID_IUnknown.

Следовательно, нам необходимо запросить у внутреннего компонента интерфейс IY после его (компонента) создания.

Но здесь необходимо быть аккуратным и не запутаться. Когда Вы вызываете QueryInterface, чтобы получить по m_pUnknownInner указатель на интерфейс IID_IY, эта функция, как примерный школьник, вызывает для возвращаемого указателя AddRef. Поскольку внутренний компонент агрегирован, он делегирует вызов AddRef внешнему IUnknown. В результате увеличивается счетчик ссылок внешнего компонента, а не внутреннего. Я хочу еще раз это подчеркнуть. Когда внешний компонент запрашивает интерфейс через указатель на неделегирующий IUnknown или какой-либо еще интерфейс внутреннего компонента, счетчик ссылок внешнего компонента увеличивается. Это именно то, что требуется, когда интерфейс через указатель на интерфейс внутреннего компонента запрашивает клиент. Но в данном случае указатель на интерфейс IY запрашивает внешний компонент, и счетчик ссылок для этого указателя является счетчиком ссылок внешнего компонента. Таким образом, внешний компонент удерживает одну ссылку сам на себя! Если допустить такое, счетчик ссылок внешнего компонента никогда не станет нулем, и компонент никогда не будет удален из памяти.

Так как время существования указателя на IY, принадлежащего внешнему компоненту, вложено во время существования самого внешнего компонента, нам нет необходимости увеличивать счетчик ссылок. Но не вызывайте для уменьшения счетчика ссылок Release для IY — мы обязаны работать с интерфейсом IY так, как если бы у него был отдельный счетчик ссылок. (В нашей реализации компонента неважно, для какого указателя вызывать Release. Но в других случаях это может быть не так.) Освобождение интерфейса IY может освободить используемые им ресурсы. Следовательно, общее правило состоит в том, чтобы вызывать Release, используя указатель, переданный в CoCreateInstance. Версия CA::Init, запрашивающая интерфейс IY, приведена ниже:

HRESULT stdcall CA::Init() { // Получить указатель на внешний IUnknown IUnknown* pUnknownOuter = this;

// Создать внутренний компонент HRESULT hr = CoCreateInstance(CLSID_Component2, // IUnknown внешнего компонента PUnknownOuter, CLSCTX_INPROC_SERVER, // При агрегировании только IUnknown IID_IUnknown, (void**)&m_pUnknownInner);

if (FAILED(hr)) { // Ошибка при создании компонента return E_FAIL;

} // Этот вызов увеличит счетчик ссылок внешнего компонента // Получить интерфейс IY внутреннего компонента hr = m_pUnknownInner->QueryInterface(IID_IY, (void**)&m_pIY);

if (FAILED(hr)) { // Внутренний компонент не поддерживает интерфейс IY m_pUnknownInner->Release();

return E_FAIL;

} // Нам нужно уменьшить счетчик ссылок на внешний компонент, // увеличенный предыдущим вызовом pUnknownOuter->Release();

return S_OK;

} При реализации QueryInterface у нас есть выбор — либо возвращать m_pIY, либо вызывать QueryInterface внутреннего компонента. Поэтому можно использовать либо else if (iid == IID_IY) { return m_pUnknownOuter->QueryInterface(iid, ppv);

} как мы это делали, либо else if (iid == IID_IY) { *ppv = m_pIY;

} Итак, мы уже создали внутренний компонент, запросили у него интерфейс, скорректировали счетчик ссылок и вернули интерфейс клиенту. Чего мы еще не сделали, так это не освободили интерфейс в деструкторе внешнего компонента. Мы не можем просто вызвать m_pIY->Release, так как у нас нет для него подсчитанной ссылки. Мы убрали ее в функции Init внешнего компонента после того, как получили указатель на IY. Теперь необходимо повторить процедуру в обратном порядке, восстановив счетчик ссылок и вызвав Release для указателя на IY.

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

Таким образом, процесс освобождения нашего указателя на интерфейс внутреннего компонента состоит из трех этапов. Во-первых, необходимо гарантировать, что наш компонент не попытается снова удалить себя. Во-вторых, необходимо вызвать AddRef для внешнего компонента, так как любой вызов Release для внутреннего компонента вызовет Release для внешнего. И, наконец, мы можем освободить указатель на IY, хранящийся во внешнем компоненте. Соответствующий код приведен ниже:

// 1. Увеличить счетчик ссылок во избежание // рекурсивного вызова деструктора m_cRef = 1;

// 2. AddRef для внешнего IUnknown IUnknown* pUnknownOuter = this;

pUnknownOuter->AddRef();

// 3. Освободить интерфейс m_pIY->Release();

Давайте мысленно проследим работу этого кода внешнего компонента. Первое, что мы делаем, — устанавливаем счетчик ссылок в 1. Далее мы увеличиваем его до двух. Затем вызываем Release для интерфейса IY. Внутренний компонент будет делегировать этот вызов внешнему. Последний уменьшит счетчик ссылок с 2 до 1. Если бы мы не установили ранее счетчик ссылок в 1, то компонент попытался бы во второй раз удалить себя.

В нашей реализации, когда внутренний компонент агрегируется, его функция Release просто делегируется внешнему IUnknown. Однако внешний компонент должен работать с внутренним так, как если бы тот вел отдельные счетчики ссылок для каждого интерфейса, поскольку другие реализации внутреннего компонента могут делать больше, чем просто делегировать Release внешнему компоненту. Внутренний компонент может также освобождать некоторые ресурсы или выполнять другие операции.

Большая часть этого кода может показаться избыточной и ненужной. Но если внешний компонент сам агрегирован другим компонентом, выполнение описанных выше шагов становится критически важным. В примере из гл. 9 показан компонент, который агрегирует компонент, агрегирующий другой компонент.

«Вот и все, что необходимо сделать для реализации агрегирования», — сказал он, улыбаясь. В действительности, после того, как Вы написали корректный код агрегирования, он работает отлично, и о нем можно забыть. Однако после первой попытки его написать многие начинают называть его не «aggregation», а «aggravation»*.

Законченный пример Реализуем компонент, который агрегирует некий интерфейс. В данном примере Компонент 1 поддерживает два интерфейса, так же как и в примере с включением. Однако здесь он реализует только IX. Он не будет ни реализовывать IY, ни передавать его вызовы реализации этого интерфейса Компонентом 2. Вместо этого, когда клиент запрашивает у Компонента 1 интерфейс IY, Компонент 1 возвращает указатель на интерфейс IY, реализуемый внутренним Компонентом 2. В листинге 8-3 представлен внешний компонент, а в листинге 8-4 — внутренний. Клиент остался практически неизменным;

ему совершенно неважно, используем ли мы агрегирование или включение.

AGGRGATE\CMPNT // // Cmpnt1.cpp - Компонент // Интересные части кода выделены полужирным шрифтом // // #include #include #include "Iface.h" #include "Registry.h" // Функция Trace void trace(const char* msg) { cout << "Компонент 1:\t" << msg << endl;

} /////////////////////////////////////////////////////////// // // Глобальные переменные // // Статические переменные // Дескриптор модуля DLL static HMODULE g_hModule = NULL;

// Счетчик активных компонентов static long g_cComponents = 0;

// Число блокировок static long g_cServerLocks = 0;

// Дружественное имя компонента const char g_szFriendlyName[] = "Основы COM, Глава 8 Пример 2, Компонент 1";

// Не зависящий от версии ProgID const char g_szVerIndProgID[] = "InsideCOM.Chap08.Ex2.Cmpnt1";

// ProgID const char g_szProgID[] = "InsideCOM.Chap08.Ex2.Cmpnt1.1";

/////////////////////////////////////////////////////////// // // Компонент A // 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 * Aggravation (англ.) — 1. Ухудшение, усугубление;

2. Раздражение, огорчение. — Прим. перев.

virtual void stdcall Fx() { cout << "Fx" << endl;

} /* Компонент 1 агрегирует интерфейс IY, а не реализует его // Интерфейс IY virtual void stdcall Fy() { m_pIY->Fy();

} */ // Конструктор CA();

// Деструктор ~CA();

// Функция инициализации, вызываемая фабрикой класса для // создания агрегируемого компонента HRESULT stdcall Init();

private:

// Счетчик ссылок long m_cRef;

// Указатель на интерфейс IY агрегированного компонента // (Нам необязательно сохранять указатель на IY. Однако мы // можем использовать его в QueryInterface) IY* m_pIY;

// Указатель на IUnknown внутреннего компонента IUnknown* m_pUnknownInner;

};

// // Конструктор // CA::CA() : m_cRef(1), m_pUnknownInner(NULL) { ::InterlockedIncrement(&g_cComponents);

} // // Деструктор // CA::~CA() { ::InterlockedDecrement(&g_cComponents);

trace("Самоликвидация");

// Предотвращение рекурсивного вызова деструктора следующей // ниже пары AddRef/Release m_cRef = 1;

// Учесть pUnknownOuter->Release в методе Init IUnknown* pUnknownOuter = this;

pUnknownOuter->AddRef();

// Правильное освобождение указателя;

возможен поинтерфейсный // подсчет ссылок m_pIY->Release();

// Освободить внутренний компонент if (m_pUnknownInner != NULL) { m_pUnknownInner->Release();

} } // Инициализировать компонент путем создания внутреннего компонента HRESULT stdcall CA::Init() { // Получить указатель на внешний IUnknown // Поскольку этот компонент агрегируется, внешний IUnknown // это то же самое, что и указатель this IUnknown* pUnknownOuter = this;

trace("Создать внутренний компонент");

HRESULT hr = ::CoCreateInstance(CLSID_Component2, // IUnknown внешнего компонента pUnknownOuter, CLSCTX_INPROC_SERVER, // При агрегировании - IUnknown IID_IUnknown, (void**)&m_pUnknownInner);

if (FAILED(hr)) { trace("Не могу создать внутренний компонент");

return E_FAIL;

} // Следующий вызов будет увеличивать счетчик ссылок внешнего компонента trace("Получить интерфейс IY внутреннего компонента");

hr = m_pUnknownInner->QueryInterface(IID_IY, (void**)&m_pIY);

if (FAILED(hr)) { trace("Внутренний компонент не поддерживает интерфейс IY");

m_pUnknownInner->Release();

return E_FAIL;

} // Необходимо уменьшить счетчик ссылок внешнего компонента, увеличенный // предыдущим вызовом. Для этого вызываем Release для указателя, // переданного CoCreateInstance.

pUnknownOuter->Release();

return S_OK;

} // // Реализация 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);

} else if (iid == IID_IY) { trace("Вернуть интерфейс IY внутреннего компонента");

#if // Этот интерфейс можно запросить...

return m_pUnknownInner->QueryInterface(iid,ppv);

#else // либо можно вернуть сохраненный указатель *ppv = m_pIY;

// Проходим дальше, чтобы была вызвана AddRef #endif } 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() {} private:

long m_cRef;

};

// // Реализация IUnknown для фабрики класса // HRESULT stdcall CFactory::QueryInterface(REFIID iid, void** ppv) { IUnknown* pI;

if ((iid == IID_IUnknown) || (iid == IID_IClassFactory)) { pI = static_cast(this);

} else { *ppv = NULL;

return E_NOINTERFACE;

} pI->AddRef();

*ppv = pI;

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) { // Агрегирование не поддерживается if (pUnknownOuter != NULL) { return CLASS_E_NOAGGREGATION;

} // Создать компонент CA* pA = new CA;

if (pA == NULL) { return E_OUTOFMEMORY;

} // Инициализировать компонент HRESULT hr = pA->Init();

if (FAILED(hr)) { // Ошибка инициализации. Удалить компонент.

pA->Release();

return hr;

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

pA->Release();

return hr;

} // LockServer HRESULT stdcall CFactory::LockServer(BOOL bLock) { if (bLock) { ::InterlockedIncrement(&g_cServerLocks);

} else { ::InterlockedDecrement(&g_cServerLocks);

} return S_OK;

} /////////////////////////////////////////////////////////// // // Экспортируемые функции // 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) { // Можем ли мы создать такой компонент?

if (clsid != CLSID_Component1) { return CLASS_E_CLASSNOTAVAILABLE;

} // Создать фабрику класса // В конструкторе нет Addref CFactory* pFactory = new CFactory;

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;

} Листинг 8-3 Реализация внешнего (агрегирующего) компонента AGGRGATE\CMPNT // // Cmpnt2.cpp - Компонент // Помните, что изменения в фабрике класса отмечены полужирным шрифтом // #include #include #include "Iface.h" #include "Registry.h" void trace(const char* msg) { cout << "Компонент 2:\t" << msg << endl;

} /////////////////////////////////////////////////////////// // // Глобальные переменные // // Статические переменные // Дескриптор модуля DLL static HMODULE g_hModule = NULL;

// Счетчик активных компонентов static long g_cComponents = 0;

// Количество блокировок static long g_cServerLocks = 0;

// Дружественное имя компонента const char g_szFriendlyName[] = "Основы COM, Глава 8 Пример 2, Компонент 2";

// Независящий от версии ProgID const char g_szVerIndProgID[] = "InsideCOM.Chap08.Ex2.Cmpnt2";

// ProgID const char g_szProgID[] = "InsideCOM.Chap08.Ex2.Cmpnt2.1";

/////////////////////////////////////////////////////////// // // Неделегирующий интерфейс IUnknown // struct INondelegatingUnknown { virtual HRESULT stdcall NondelegatingQueryInterface(const IID&, void**) = 0;

virtual ULONG stdcall NondelegatingAddRef() = 0;

virtual ULONG stdcall NondelegatingRelease() = 0;

};

/////////////////////////////////////////////////////////// // // Компонент B // class CB : public IY, public INondelegatingUnknown { public:

// Делегирующий IUnknown virtual HRESULT stdcall QueryInterface(const IID& iid, void** ppv) { trace("Делегирующий QueryInterface");

return m_pUnknownOuter->QueryInterface(iid, ppv);

} virtual ULONG stdcall AddRef() { trace("Делегировать AddRef");

return m_pUnknownOuter->AddRef();

} virtual ULONG stdcall Release() { trace("Делегировать Release");

return m_pUnknownOuter->Release();

} // Неделегирующий IUnknown virtual HRESULT stdcall NondelegatingQueryInterface(const IID& iid, void** ppv);

virtual ULONG stdcall NondelegatingAddRef();

virtual ULONG stdcall NondelegatingRelease();

// Интерфейс IY virtual void stdcall Fy() { cout << "Fy" << endl;

} // Конструктор CB(IUnknown* m_pUnknownOuter);

// Деструктор ~CB();

private:

long m_cRef;

IUnknown* m_pUnknownOuter;

};

// // Реализация IUnknown // HRESULT stdcall CB::NondelegatingQueryInterface(const IID& iid, void** ppv) { if (iid == IID_IUnknown) { // !!! ПРИВЕДЕНИЕ ТИПА ОЧЕНЬ ВАЖНО !!!

*ppv = static_cast(this);

} else if (iid == IID_IY) { *ppv = static_cast(this);

} else { *ppv = NULL;

return E_NOINTERFACE;

} reinterpret_cast(*ppv)->AddRef();

return S_OK;

} ULONG stdcall CB::NondelegatingAddRef() { return ::InterlockedIncrement(&m_cRef);

} ULONG stdcall CB::NondelegatingRelease() { if (::InterlockedDecrement(&m_cRef) == 0) { delete this;

return 0;

} return m_cRef;

} // // Конструктор // CB::CB(IUnknown* pUnknownOuter) : m_cRef(1) { ::InterlockedIncrement(&g_cComponents);

if (pUnknownOuter == NULL) { trace("Не агрегируется;

использовать неделегирующий IUnknown");

m_pUnknownOuter = reinterpret_cast (static_cast (this));

} else { trace("Агрегируется;

делегировать внешнему IUnknown");

m_pUnknownOuter = pUnknownOuter;

} } // // Деструктор // CB::~CB() { ::InterlockedDecrement(&g_cComponents);

trace("Саморазрушение");

} /////////////////////////////////////////////////////////// // // Фабрика класса // 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() {} 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) { // При агрегировании iid должен быть IID_IUnknown if ((pUnknownOuter != NULL) && (iid != IID_IUnknown)) { return CLASS_E_NOAGGREGATION;

} // Создать компонент CB* pB = new CB(pUnknownOuter);

if (pB == NULL) { return E_OUTOFMEMORY;

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

pB->NondelegatingRelease();

return hr;

} // LockServer HRESULT stdcall CFactory::LockServer(BOOL bLock) { if (bLock) { ::InterlockedIncrement(&g_cServerLocks);

} else { ::InterlockedDecrement(&g_cServerLocks);

} return S_OK;

} /////////////////////////////////////////////////////////// // // Экспортируемые функции // 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) { // Можем ли мы создать такой компонент?

if (clsid != CLSID_Component2) { return CLASS_E_CLASSNOTAVAILABLE;

} // Создать фабрику класса // В конструкторе нет AddRef CFactory* pFactory = new CFactory;

if (pFactory == NULL) { return E_OUTOFMEMORY;

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

pFactory->Release();

return hr;

} // // Регистрация сервера // STDAPI DllRegisterServer() { return RegisterServer(g_hModule, CLSID_Component2, g_szFriendlyName, g_szVerIndProgID, g_szProgID);

} STDAPI DllUnregisterServer() { return UnregisterServer(CLSID_Component2, g_szVerIndProgID, g_szProgID);

} /////////////////////////////////////////////////////////// // // Информация о модуле DLL // BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved) { if (dwReason == DLL_PROCESS_ATTACH) { g_hModule = hModule;

} return TRUE;

} Листинг 8-4 Реализация внутреннего (агрегируемого) компонента Слепое агрегирование В предыдущем примере внешний компонент агрегирует только один из реализуемых внутренним компонентом интерфейсов. Единственный интерфейс внутреннего компонента, до которого может добраться клиент, это IY.

Если бы внутренний компонент реализовывал IZ, клиент не смог бы получить указатель на IZ, так как внешний компонент возвращал бы E_NOINTERFACE.

А что, если внешний компонент хочет агрегировать несколько интерфейсов внутреннего? Внешний компонент легко модифицировать так, чтобы поддерживать еще один интерфейс внутреннего компонента:

else is ((iid == IID_IY) || (iid == IID_IZ)) { return m_pUnknownInner->QueryInterface(iid, ppv);

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

...

else if (iid == IID_IX) { *ppv = static_cast(this);

} // Нет условия else { return m_pUnknownInner->QueryInterface(iid, ppv);

}...

Данная процедура называется слепым агрегированием (blind aggregation), так как внешний компонент слепо передает идентификаторы интерфейсов внутреннему. При слепом агрегировании внешний компонент не контролирует, какие из интерфейсов внутреннего компонента он предоставляет клиенту. В большинстве случаев лучше не прибегать к слепому агрегированию. Одна из причин этого в том, что внутренний компонент может поддерживать интерфейсы, не совместимые с интерфейсами, которые поддерживаются внешним компонентом.

Например, внешний компонент для сохранения файлом может поддерживать интерфейс ISlowFile, тогда как внутренний для этой же цели поддерживает интерфейс IFastFile. Предположим, что клиент всегда запрашивает IFastFile прежде, чем запрашивать ISlowFile. Если внешний компонент слепо агрегирует внутренний, клиент получит указатель на IFastFile внутреннего компонента. Внутренний компонент ничего не знает о внешнем и поэтому не будет правильно сохранять информацию, связанную с последним. Проще всего избежать таких конфликтов, не используя слепое агрегирование.

Если полное воздержание кажется излишне сильным условием, для устранения подобных конфликтов можно использовать два менее радикальных способа. Во-первых, при реализации внешнего компонента не реализуйте интерфейсы, которые могут дублировать функциональность интерфейсов внутреннего компонента. Во-вторых, можно создавать внешний компонент и клиент либо внешний и внутренний компонент как взаимосвязанные пары.

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

Вероятность конфликта с существующими интерфейсами компонента минимальна в случае метаинтерфейсов (metainterfaces), или интерфейсов класса. Метаинтерфейсы манипулируют самим компонентом, а не абстракцией, которую он представляет.

Предположим, что у нас есть программа преобразования растровых изображений. Пользователь может модифицировать растровое изображение с помощью различных алгоритмов. Последние реализованы как внутренний компонент, который пользователь может добавлять в систему во время работы. Каждый внутренний компонент может считывать и сохранять растровые образы, а также преобразовывать их в соответствии с некоторым особым алгоритмом. У внешнего компонента есть интерфейс ISetColors, устанавливающий цвета, с которыми работает внутренний компонент. Внешний компонент также имеет интерфейс IToolInfo, который отображает значки разных алгоритмов преобразования на панели инструментов и создает внутренний компонент, когда пользователь выбирает соответствующий значок (рис. 8-7).

ISetColors — это пример обычного интерфейса, который расширяет абстракцию алгоритма преобразования.

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

Метаинтерфейсы наиболее полезны, когда они реализованы для набора разных компонентов в системе компонентов. Такие метаинтерфейсы предоставляют системе унифицированный способ работы с компонентами, повышая, таким образом, степень повторной применимости кода посредством полиморфизма. Добавление метаинтерфейсов к существующим компонентам чаще всего выполняют разработчики клиентов. Используя метаинтерфейсы, такой клиент может получать компоненты из самых разных источников и работать со всеми ними единообразно.

Внешний компонент Инструмент преобразования IToolInfo ISetColors Внутренний компонент Алгоритм преобразования IMorph IColors Рис. 8-7 Интерфейс IToolInfo — это метаинтерфейс, вероятность конфликта которого с интерфейсами внутреннего компонента мала. ISetColors не является метаинтерфейсом и действует в области компетенции внутреннего компонента.

ISetColors конфликтует с интерфейсом IColors внутреннего компонента.

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

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

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

Для устранения этих недостатков компонент можно спроектировать так, чтобы способствовать повторной применимости. То же самое верно и для классов С++, которые трудно, если не невозможно, расширять или специализировать, если они не спроектированы надлежащим образом. Классы С++, разработанные с учетом последующей настройки, имеют «защищенные» методы, которые используются производным классом для получения информации о внутреннем состоянии базового класса. Компоненты СОМ могут использовать интерфейсы для того, чтобы облегчить свое включение или агрегирование.

Предоставление информации о внутреннем состоянии В СОМ все делается через интерфейсы. Следовательно, внутренний компонент может предоставить информацию о своем внутреннем состоянии, добавив новый интерфейс. Этот интерфейс мог бы предоставлять внешнему компоненту информацию или сервисы, помогающие настроить внутренний компонент (рис. 8-8).

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

Внешний компонент IX Внешний компонент использует Реализация IInternalState для получения внешнего компонента дополнительной информации о состоянии внутреннего Внутренний компонент IInternalState IInternalState предоставляет IY внешнему компоненту информацию о внутреннем IZ состоянии данного компонента Рис. 8-8 Внутренний компонент может упростить свою настройку, предоставив интерфейсы, которые дают внешнему компоненту доступ к его внутреннему состоянию Интерфейсы, предоставляющие внешнему компоненту такого рода информацию, играют в СОМ важную роль.

Компоненты СОМ обычно состоят из множества небольших интерфейсов. Внутри себя реализации этих интерфейсов взаимозависимы, так как используют общие переменные-члены и другие аспекты внутреннего состояния. Это означает, что Вы не можете взять один из интерфейсов компонента и использовать его сам по себе, потому что этот интерфейс может зависеть (в управлении некоторым аспектом внутреннего состояния компонента) от информации или состояния другого интерфейса (рис. 8-9). Повторю еще раз, внешний компонент — просто клиент внутреннего. У него нет никаких особых возможностей.

Независимо реализованные Интерфейсы с общей реализацией интерфейсы IX Реализация IX IX Реализация IY Реализация IY IY IX, IY, IZ IZ Реализация IZ IZ Реализации интерфейсов Обычно реализации интерфейсов редко бывают независимыми взаимосвязаны Рис. 8-9 Клиент рассматривает интерфейсы как независимые сущности. Однако обычно интерфейсы используют общие детали реализации. Это еще более затрудняет расширение или специализацию компонентов. Добавление интерфейсов, предоставляющих взгляд на компонент изнутри, может упростить его настройку.

Моделирование виртуальных функций Дополнительные интерфейсы могут не только предоставить эквивалент СОМ для защищенных функций-членов С++, но и позволить интерфейсам замещать виртуальные функции. Во многих случаях виртуальные функции используются как функции обратного вызова (callback). Базовый класс может вызывать виртуальную функцию до, во время или после некоторой операции, чтобы дать производному классу возможность модифицировать ее выполнение. Компоненты СОМ могут делать то же самое, определив интерфейс настройки (customization interface). Компонент не реализует такой интерфейс, а, наоборот, вызывает его. Клиенты, желающие настроить компонент для своих нужд, реализуют интерфейс настройки и передают указатель на него компоненту. Эту технику клиенты могут применять, и не используя включение или агрегирование (рис. 8-10).

Клиент Компонент IX Клиент использует IX Компонент вызывает ICustomize, чтобы дать ICustomize клиенту возможность настройки поведенияIX Рис. 8-10 Компонент определяет исходящий интерфейс, который он вызывает для своей настройки Единственное реальное различие между наследованием и использованием функции обратного вызова или исходящего (outgoing) интерфейса, такого как ICustomize, состоит в том, что в последнем случае необходимо вручную присоединить клиент к компоненту. Эту технику мы рассмотрим в гл. 13. Используя ее, необходимо быть осторожным с циклическим подсчетом ссылок. В общем случае ICustomize реализуют как часть своего собственного компонента с отдельным счетчиком ссылок.

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

Теперь у Вас все есть. В сущности, все, что дает наследование реализации, можно заново воссоздать с помощью интерфейсов, включения и агрегирования. Ясно, что наследование реализации С++ более привычно по сравнению с решением СОМ, которое представлено на рис. 8-11. Однако наследование реализации С++ требует доступа к исходному коду базового класса, а это означает, что при изменении базового класса программу придется перекомпилировать. В компонентной архитектуре, где компоненты постоянно и независимо друг от друга изменяются, а исходные коды недоступны, перекомпиляция невозможна. Делая повторное применение компонентов аналогичным простому использованию, СОМ ослабляет взаимозависимость между данным компонентом и компонентом, который он специализирует.

Клиент Компонент IX Клиент использует IX Компонент вызывает интерфейс настройки, который позволяет клиенту настроить его Клиент вызывает ICustomize реализацию IX ICustomize Реализация клиентом ICustomize включает Компонент настройки по Компонент предоставляет или агрегирует компонент настройки, умолчанию компонент настройки по который реализует умолчанию, интерфейс ICustomize по предоставляемый ICustomize умолчанию данным компонентом Рис. 8-11 Компонент предоставляет реализацию исходящего интерфейса по умолчанию Резюме Мы познакомились с тем, как можно повторно использовать, расширять и специализировать компоненты посредством включения и агрегирования. Повторное использование компонента столь же просто, как и включение его в другой компонент. В этом случае внешний компонент — клиент внутреннего. Если Вы хотите специализировать компонент, можете добавить свой код перед вызовом методов его интерфейсов и после него.

Если Вы собираетесь только добавлять интерфейсы, можете вместо включения использовать агрегирование.

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

Внешний компонент не реализует этот интерфейс снова и не перенаправляет ему вызовы.

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

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

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

В этой главе мы научились повторному применению компонентов. В следующей главе будет рассмотрен другой вариант: вместо повторного применения компонентов мы будем повторно применять код на С++. Мы реализуем базовые классы для IUnknown и IClassFactory, от которых смогут наследоваться наши компоненты.

От всех этих разговоров о повторном применении у меня разыгрался аппетит. Наверное, пора узнать, насколько на самом деле съедобны миллионеры. Говорят, они хрустят на зубах.

9 глава Будем проще Что бы ни делал человек, он пытается максимально упростить задачу — особенно, кажется, это касается любых учебных упражнений, которые в результате могут полностью лишиться смысла. Телевидение наводняет реклама — и, похоже, успешная — тренажера для брюшного пресса и тому подобных приспособлений. Такой тренажер механизирует одно из простейших упражнений, облегчая его выполнение до предела. (Как говорят, главное достоинство тренажера в том, что он позволяет сконцентрировать усилия на мышцах живота, а не шеи или чего нибудь еще. Как знать…) Поскольку все хотят упростить себе жизнь, я поддамся общей тенденции и упрощу использование компонентов СОМ. Писать их сложнее, чем тренировать мышцы живота, и, надо сказать, компоненты не становятся ни лучше, ни красивее, если на их создание потратить больше усилий.

Сначала при помощи классов С++ мы сделаем использование компонентов СОМ более похожим на использование классов;

затем разработаем несколько классов, облегчающих разработку компонентов.

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

Поддержка СОМ в компиляторе Компилятор Microsoft Visual C++ версии 5.0 вводит расширения языка С++, упрощающие разработку и использование компонентов СОМ. Для получения более подробной информации обратитесь к документации Visual С++ версии 5.0.

Даже если Вы вписали вызовы Release там, где нужно, Ваша программа может их не выполнить. Обработчики исключений С++ ничего не знают о компонентах СОМ. Поэтому Release не вызывается автоматически после возникновения исключений.

Простая и корректная обработка AddRef и Release — лишь полдела. Нужно еще упростить вызов QueryInterface, Вы, я уверен, давно заметили, что один такой вызов занимает несколько строк. Несколько вызовов внутри одной функции могут легко затенить содержательный код. Я обхожу эту проблему, сохраняя указатели и интерфейсы, а не запрашивая их при всякой нужде. Это повышает производительность и надежность кода — за счет памяти. Но с QueryInterface связана и более тяжелая проблема — вызов требует явного приведения типов. Если Вы перепутаете параметры, передаваемые QueryInterface, компилятор Вам не поможет. Например, следующий код прекрасно компилируется, хотя он и помещает указатель на интерфейс IY в переменную-указатель на IZ:

IZ* pIZ;

PIX->QueryInterface(IID_IY, (void**)&pIZ);

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

Эти проблемы можно устранить при помощи инкапсуляции. Можно либо инкапсулировать указатель на интерфейс при помощи класса smart-указателя, либо инкапсулировать сам интерфейс внутри класса-оболочки (wrapper). Давайте рассмотрим эти методы, начиная со smart-указателей.

Smart-указатели на интерфейсы Первый способ упрощения кода клиента — использование для доступа к компонентам smart-указателей вместо обычных указателей на интерфейс. Smart-указатель используется так же, как обычный указатель С++, но он скрывает подсчет ссылок. Когда поток управления выходит из области действия переменной-smart-указателя, интерфейс автоматически освобождается. Это делает использование интерфейса СОМ аналогичным использованию объекта С++.

Что такое smart-указатель?

Smart-указатель (smart pointer) — это класс, переопределяющий operator-> (оператор выбора метода). Класс smart-указателя содержит указатель на другой объект. Когда для smart-указателя вызывается operator->, этот вызов делегируется или передается smart-указателем объекту, на который ссылается содержащийся в нем указатель. Smart-указатель на интерфейс — это smart-указатель, содержащий указатель на интерфейс.

Рассмотрим простой пример. CFooPointer имеет минимум возможностей, необходимых классу smart-указателя.

Он содержит указатель и переопределяет operator->.

class CFoo { public:

virtual void Bar();

};

class CFooPointer { public:

CFooPointer (Cfoo*p) { m_p = p;

} CFoo* operator->() { return m_p;

} private:

CFoo* m_p;

};

...

void Funky(CFoo* pFoo) { // Создать и инициализировать smart-указатель CFooPointer spFoo(pFoo);

// Следующий оператор эквивалентен pFoo->Bar();

spFoo->Bar();

} В приведенном примере функция Funky создает CFooPointer с именем spFoo и инициализирует его с помощью pFoo. Затем она выполняет разыменование spFoo для вызова функции Bar. Указатель spFoo делегирует этот вызов m_p, которая содержит pFoo. С помощью spFoo можно вызвать любой метод CFoo. Самое замечательное здесь то, что Вам не нужно явно перечислять в CFooPointer все методы CFoo. Для CFoo функция operator-> означает «разыменуй меня»*. В то же время для CFooPointer она означает «разыменуй не меня, а m_p» (см. рис.

9-1)**.

Для умного (smart) указателя CFooPointer глуповат. Он ничего не делает. Хороший компилятор, вероятно, вообще его устранит во время оптимизации. Кроме того, CFooPointer не слишком похож на обычный указатель.

Попробуйте присвоить pFoo переменной spFoo. Присваивание не сработает, так как operator= (оператор присваивания) не переопределен соответствующим образом. Для того, чтобы CFooPointer выглядел так же, как и указатель CFoo, для CFooPointer необходимо переопределить несколько операторов. Сюда входят operator* (оператор разыменования) и operator& (оператор получения адреса), которые должны работать с указателем m_p, а не с самим объектом CFooPointer.

Реализация класса указателя на интерфейс Хотя классов smart-указателей для работы с интерфейсами СОМ и не так много, как классов строк, но число тех и других не сильно различается. ActiveX Template Library (ATL) содержит классы указателей на интерфейсы СОМ CComPtr и CComQIPtr. В библиотеке MFC имеется класс CIP для внутреннего пользования. (Он находится в файле AFXCOM_.H.) CIP — это самый полный вариант класса smart-указателя на интерфейс. Он делает практически все. Здесь я представлю свой собственный, главным образом потому, что мой код легче читается.

Мой класс похож на классы из ATL и MFC, но не столь полон.

* Точнее, для указателя CFoo*. — Прим. перев.

** Функция operator-> означает не «разыменуй меня», а «используй меня для обращения к моим методам или переменным-членам» — Прим..

перев.

Клиент Код Код клиента smart-указателя Указатель Компонент Клиент вызывает на интерфейс члены интерфейса напрямую IX m_pIX с помощью operator-> IY Функции-члены Доступ к функциям-членам IZ класса smart-указателя осуществляется с использованием нотации "точка" Рис. 9-1 Smart-указатели на интерфейсы делегируют вызовы указателю на интерфейс, хранящемуся внутри класса.

Мой класс указателя на интерфейс называется IPtr и реализован в файле PTR.H, который представлен в листинге 9-1. Пусть длина исходного текста Вас не пугает. Кода там очень мало. Я просто вставил побольше пустых строк, чтобы легче было читать.

Шаблон IPtr из PTR.H // // IPtr – Smart-указатель на интерфейс // Использование: IPtr spIX;

// Не используйте с IUnknown;

IPtr // не будет компилироваться. Вместо этого используйте IUnknownPtr.

// template class IPtr { public:

// Конструкторы IPtr() { m_pI = NULL;

} IPtr(T* lp) { m_pI = lp;

if (m_pI != NULL) { m_pI->AddRef();

} } IPtr(IUnknown* pI) { m_pI = NULL;

if (pI != NULL) { pI->QueryInterface(*piid, (void **)&m_pI);

} } // Деструктор ~IPtr() { Release();

} // Сброс в NULL void Release() { if (m_pI != NULL) { T* pOld = m_pI;

m_pI = NULL;

pOld->Release();

} } // Преобразование operator T*() { return m_pI;

} // Операции с указателем T& operator*() { assert(m_pI != NULL);

return *m_pI;

} T** operator&() { assert(m_pI == NULL);

return &m_pI;

} T* operator->() { assert(m_pI != NULL);

return m_pI;

Pages:     | 1 | 2 || 4 | 5 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.