Сущность технологии COM

       

Активация


Клиентам требуется механизм для поиска объектов класса. В силу динамической природы СОМ это может привести к загрузке библиотеки DLL или запуску обслуживающего процесса (server process). Эта процедура вызова объекта к жизни называется активацией объекта.

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

Каждая из описанных трех моделей активации пользуется услугами имеющегося в СОМ диспетчера управления сервисами SCM (Service Control Manager). SCM является основной точкой рандеву для всех запросов на активацию в каждой отдельной машине. Каждая хост-машина, поддерживающая СОМ, имеет свой собственный локальный SCM, который переадресовывает удаленные запросы на активацию на SCM удаленной машины, где этот запрос будет трактоваться как локальный запрос на активацию. SCM используется только для того, чтобы активировать объект и привязать к нему начальный указатель интерфейса. Как только объект активирован, SCM более не связан с вызовом методов клиента и объекта. Как показано на рис. 3.1, под Windows NT SCM реализован в службе RPCSS (Remote Procedure Call Service System — система сервиса удаленного вызова процедур). Службы SCM объявляются в программы как высокоуровневые типы моникеров и как низкоуровневые API-функции, причем все они реализованы в библиотеке СОМ (как это называется в Спецификации СОМ).
Под Windows NT большая часть библиотеки СОМ реализована в OLE32.DLL. Для повышения эффективности библиотека СОМ может использовать локальный или кэшированный режим, чтобы избежать ненужных запросов службы RPCSS со стороны IPC (interprocess communication — межпроцессное взаимодействие).

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

Когда объект активирован внутри процесса, то в процесс клиента загружается та библиотека DLL, которая реализует методы объекта, и все данные-члены хранятся в адресном пространстве клиента. Так как не требуется никаких переключении процессов, то эффективность вызова методов чрезвычайно высока. Кроме того, клиентский поток может быть использован для прямого выполнения кода метода, при условии, что требования по организации поточной обработки (threading requirements) объекта соответствуют клиентским требованиям. Если у клиента и у объекта требования по организации поточной обработки совместимы, то также не нужно никаких переключении потоков. Если вызовы метода могут выполняться с использованием клиентского потока, после активации объекта не требуется участия никакой промежуточной среды времени выполнения, и цена вызова метода просто равна вызову виртуальной функции.


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

Когда объект активирован извне процесса (то есть в другом процессе на локальной или удаленной машине), то код, реализующий методы объекта, выполняется в процессе определенного сервера и все данные-члены объекта сохраняются в адресном пространстве процесса сервера. Чтобы позволить клиенту связываться с внепроцессным (out-of-process) объектом, СОМ прозрачно (скрытно от клиента) возвращает ему "заместитель" (proxy) во время активации. В главе 5 подробно обсуждается, что этот "заместитель" выполняется в клиентском потоке и переводит вызовы метода, преобразованные в RPC-запросы (Remote Procedure Call — удаленный вызов процедуры), в контекст исполнения сервера, где эти RPC-запросы затем преобразуются обратно в вызовы метода текущего объекта. Это делает вызов метода менее эффективным, так как при каждом обращении к объекту требуются переключение потока и переключение процесса. К преимуществам внепроцессной (то есть работающей не в клиентском процессе) активации относятся изоляция ошибок, распределение и повышенная безопасность. В главе 6 внепроцессная активация будет рассматриваться подробно.

1

В Windows NT также имеется подсистема, известная как Service Control Manager, или просто Services, которая используется для запуска процессов, не зависящих от входа в систему. Далее в этой книге мы будем называть этот диспетчер управления сервисами NT SCM, чтобы отличать его от СОМ SCM.

2

Моникеры являются поисковыми объектами, которые скрывают детали активации или связывающего алгоритма. Более подробно моникеры обсуждаются далее в этой главе.

3

Степень изоляции, необходимая для вызова во внешнюю DLL, приблизительно эквивалентна вызову функции через вход таблицы vtbl.


Где мы находимся?


В этой главе представлена концепция СОМ-класса. СОМ-классами называются конкретные типы данных, которые экспортируют один или более интерфейсов и являются основной абстракцией, используемой при активации объектов в СОМ. СОМ поддерживает три примитива активации. CoGetClassObject связывает ссылку с объектом класса, который представляет независимые от экземпляра функциональные возможности класса. CoCreateInstanceEx связывает ссылку с новым экземпляром класса, a CoGetInstanceFromFile связывает ссылку с постоянным экземпляром, находящимся в файле. Моникеры используются в качестве универсальной абстракции для передачи клиентам стратегии связывания и активации, причем MkParseDisplayName выполняет функции точки входа в пространство имен СОМ.





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


Напомним, что SCM поддерживает три примитива активации (связывание с объектами класса, связывание с экземплярами класса, связывание с постоянными экземплярами из файлов). Как показано на рис. 3.2, эти примитивы логически разделены на уровни. Примитивом нижнего уровня является связывание с объектом класса. Этот примитив также наиболее прост для понимания.

Вместо того чтобы вручную загружать код класса, клиенты пользуются услугами SCM посредством низкоуровневой API-функции СОМ CoGetClassObject. Эта функция запрашивает SCM присвоить значение указателю на требуемый объект класса:

HRESULT CoGetClassObject( [in] REFCLSID rclsid, // which class object? // Какой объект класса? [in] DWORD dwClsCtx, // locality? //местонахождение? [in] COSERVERINFO *pcsi, // host/security info //сведения о сервере и обеспечении безопасности [in] REFIID riid, // which interface? // какой интерфейс? [out, iid_is(riid)] void **ppv); // put it here! // поместим его здесь!

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

В качестве второго параметра CoGetClassObject принимает битовую маску (bitmask), которая позволяет клиенту указать характеристики скрытого и живучего состояний объекта (например, будет ли объект запущен в процессе, вне процесса или вообще на другом сервере). Допустимые значения для этой битовой маски определены в стандартном перечислении CLSCTX:

enum tagCLSCTX { CLSCTX_INPROC_SERVER = 0х1, // run -inprocess // запуск в процесс CLSCTX_INPROC_HANDLER = 0х2, // see note // смотрите сноску CLSCTX_LOCAL_SERVER = 0х4, // run out-of-process // запуск вне процесса CLSCTX_REMOTE_SERVER = 0х10 // run off-host // запуск вне хост-машины } CLSCTX;


Эти флаги могут быть подвергнуты побитному логическому сложению (bit-wise-ORed together), и в случае, когда доступен более чем один запрошенный CLSCTX, СОМ выберет наиболее эффективный тип сервера (это означает, что СОМ будет, когда это возможно, использовать наименее значимый бит битовой маски). Заголовочные файлы SDK также включают в себя несколько сокращенных макросов, которые сочетают несколько флагов CLSCTX, используемых во многих обычных сценариях:

#define CLSCTX_INPROC (CLSCTX_INPROC_SERVER | \ CLSCTX_INPROC_HANDLER) #define CLSCTX_SERVER (CLSCTX_INPROC_SERVER | \ CLSCTX_LOCAL_SERVER | \ CLSCTX_REMOTE_SERVER) #define CLSCTX_ALL (CLSCTX_INPROC_SERVER | \ CLSCTX_INPROC_HANDLER | \ CLSCTX_LOCAL_SERVER | \ CLSCTX_REMOTE_SERVER)

Заметим, что такие среды, как Visual Basic и Java, всегда используют CLSCTX_ALL, показывая тем самым, что подойдет любая доступная реализация.

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

typedef struct _COSERVERINFO { DWORD dwReserved1; // reserved, must be zero // зарезервировано, должен быть нуль LPWSTR pwszName; // desired host name, or null // желаемое имя хост-машины или нуль COAUTHINFO *pAuthInfo; // desired security settings // желаемые установки безопасности DWORD dwReserved2; // reserved, must be zero // зарезервировано, должен быть нуль } COSERVERINFO;

Если клиент не указывает имя хоста (host name), а использует только флаг CLSCTX_REMOTE_SERVER, то для определения того, какая машина будет активировать объект, СОМ использует информацию о конфигурации каждого CLSID. Если клиент передает явное имя хоста, то оно получит приоритет перед любыми ранее сконфигурированными именами хостов, о которых может знать СОМ. Если клиент не желает передавать явную информацию о безопасности или имя хоста в CoGetClassObject, можно применить нулевой указатель COSERVERINFO.



Имея в наличии CoGetClassObject, клиент может дать запрос SCM на связывание указателя интерфейса с объектом класса:

HRESULT GetGorillaClass(IApeClass * &rpgc) { // declare the CLSID for Gorilla as a GUID // определяем CLSID для Gorilla как GUID const CLSID CLSID_Gorilla = { 0x571F1680, 0xCC83, 0x11d0, { 0x8C, 0х48, 0х00, 0х80, 0xС7, 0х39, 0x25, 0xBA } }; // call CoGetClassObject directly // вызываем прямо CoGetClassObject return CoGetClassObject(CLSID_Gorilla, CLSCTX_ALL, 0, IID_IApeClass, (void**)&rpgc); }

Отметим, что если запрошенный класс доступен как внутрипроцессный сервер, то СОМ автоматически загрузит соответствующую DLL и вызовет известную экспортируемую функцию, которая возвращает указатель на требуемый объект класса. Когда вызов CoGetClassObject завершен, библиотека СОМ и SCM полностью выходят из игры. Если бы класс был доступен только с внепроцессного или удаленного сервера, СОМ вместо этого возвратила бы заместитель, который позволил бы клиенту получить удаленный доступ к объекту класса.

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

HRESULT FindAGorillaAndEatBanana(long nGorillaID) { IApeClass *pgc = 0; // find the class object via CoGetClassObject // находим объект класса с помощью CoGetClassObject HRESULT hr = CoGetClassObject(CLSID_Gorilla, CLSCTX_ALL, 0, IID_IApeClass, (void**)&pgc); if (SUCCEEDED(hr)) { IApe *pApe = 0; // use the class object to find an existing gorilla // используем объект класса для нахождения существующей гориллы hr = pgc->GetApe(nGorillaID, &pApe); if (SUCCEEDED(hr)) { // tell the designated gorilla to eat a banana // прикажем указанной горилле есть бананы hr = pApe->EatBanana(); pApe->Release(); } pgc->Release(); } return hr; }

Данный пример использует объект класса для того, чтобы Gorilla нашла именованный объект и проинструктировала его есть бананы. Чтобы этот пример работал, нужно, чтобы какой-нибудь внешний посредник дал вызывающему объекту имя какой-нибудь известной гориллы.


Можно построить пример и таким образом, чтобы любая неизвестная горилла могла быть использована для удовлетворения запроса:

HRESULT CreateAGorillaAndEatBanana(void) { IApeClass *pgc = 0; // find the class object via CoGetClassObject // находим объект класса с помощью CoGetClassObject HRESULT hr = CoGetClassObject(CLSID_Gorilla, CLSCTX_ALL, 0, IID_IApeClass, (void**)&pgc); if (SUCCEEDED(hr)) { IApe *pApe = 0; // use the class object to create a new gorilla // используем объект класса для создания новой гориллы hr = pgc->CreateApe(&pApe); if (SUCCEEDED(hr)) { // tell the new gorilla to eat a banana // прикажем новой горилле есть бананы hr = pApe->EatBanana(); pApe->Release(); } pgc->Release(); } return hr; }

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

1

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

2

Внутрипроцессные обработчики (in-process handlers) — в значительной степени пережитки документации OLE. Эти обработчики являются виутрипроцессными компонентами, выступающими в качестве представителей клиентской стороны объекта, который в действительности находится в другом процессе. Обработчики используются в документах OLE для кэширования изображений у клиента с целью сократить поток IPC (interprocess communication — межпроцессное взаимодействие) при перерисовке экрана. Хотя эти обработчики в общем случае производят считывание, они редко используются вне контекста документов OLE. Windows NT 5.0 будет обеспечивать дополнительные возможности для реализации обработчиков, но подробности того, как это будет достигнуто, были еще схематичны во время написания этой книги.

3

Требования параллелизма для класса должны технически соответствовать таким же требованиям в потоке вызова.


Эмуляция классов


Часто случается, что разработчики классов желают развернуть новые версии уже существующих классов, чтобы исправить дефекты или расширить функциональные возможности. Полезно придать этим новым реализациям новые идентификаторы класса CLSID, чтобы клиенты могли четко различать, какая версия им нужна. В качестве примера посмотрим, что происходит, когда развертывается новая версия класса. Если для идентификации нового класса используется новый CLSID, (например, CLSID_Chimp2), то клиентам, определенно желающим использовать новую версию, следует использовать новый CLSID во время активации:

// new client - новый клиент IАре *рАре = 0; hr = CoCreateInstance(CLSID_Chimp2, 0, CLSCTX_ALL, IID_Ape, (void**)&pApe);

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

// old client - старый клиент IАре *рАре = 0; hr = CoCreateInstance(CLSID_Chimp, 0, CLSCTX_ALL, IID_Ape, (void**)&pApe);

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

Чтобы дать возможность разработчику новой версии класса прозрачно удовлетворять запросы на активацию для других CLSID, в СОМ введено понятие эмуляции классов (class emulation). Эмуляция классов позволяет разработчику компонента указать, что старый CLSID заменен новым, альтернативным CLSID, эмулирующим семантику исходного класса. Это позволяет прежним клиентам, делающим запросы на активацию с использованием прежнего CLSID, получать экземпляры нового усовершенствованного класса. Для индикации того, что у класса имеется новая альтернативная версия, в СОМ существует такая API-функция:


HRESULT CoTreatAsClass([in] REFCLSID rclsidOld, [in] REFCLSID rclsidNew);

Пусть Сhimp2 является новой версией класса Chimp, тогда следующий код проинформирует СОМ, что необходимо переадресовать запросы на активацию Chimp на запросы на активацию Chimp2:

// cause Chimp activation calls to activate Chimp2 // заставим запросы на активацию Chimp активизировать Chimp2

HRESULT hr = CoTreatAsClass(CLSID_Chimp, CLSID_Chimp2);

Эта API-функция добавляет следующий ключ реестра (registry key)

[HKCR\CLSID\{CLSID_Chimp}\TreatAs] @={CLSID_Chimp2}

Вызов CoTreatAsClass c CLSID_NULL в качестве второго параметра удаляет настройку TreatAs:

// cause Chimp activation calls to activate Chimps // заставим запросы на активацию Chimp // активизировать Chimps

HRESULT hr = CoTreatAsClass(CLSID_Chimp, CLSID_NULL);

Этот запрос восстанавливает исходную реализацию класса в состояние, предшествующее эмуляции. Клиенты могут запросить установку эмуляции данного класса, используя API-функцию CoGetTreatAsClass:

HRESULT CoGetTreatAsClass ([in] REFCLSID rclsidOld, [out] REFCLSID *pclsidNew);

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

1

Отметим, что CLSID_Chimp и CLSID_Chimp2 являются сокращенной записью канонической формы фактических GUID, состоящих из 32 знаков.


Категории компонентов


Как подчеркивалось в этой главе, основные примитивы активации СОМ требуют, чтобы вызывающей программе при создании новых экземпляров класса было известно его точное имя. Иногда, однако, бывает полезно просто потребовать, чтобы подходящим являлся любой класс, удовлетворяющий некоторым семантическим ограничениям. Кроме того, прежде чем сделать запрос на активацию, было бы полезно знать, какие сервисные средства класс требует от своих клиентов. В этом случае не будут создаваться объекты, которые клиент не готов должным образом поддерживать. Эти проблемы послужили причиной для создания категорий компонентов (component categories).

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

Категория компонентов есть группа логически родственных СОМ-классов, которые разделяют общий ID категории, или CATID. Идентификаторы категории CATID — это GUID, записанные в реестре как атрибуты класса. Каждый класс может иметь два подключа: Implemented Categories и Required Categories (реализованные категории и нужные категории). Представим, что есть две категории компонентов: Simians и Mammals (приматы и млекопитающие). Каждая из этих двух категорий будет иметь уникальный CATID (CATID_Simians и CATID_Mammals соответственно). Допустим, что класс Chimp является членом каждой из этих категорий, и тогда для Chimp ключ реестра Implemented Categories будет содержать в себе каждый GUID как отдельный подключ:

[HKCR\CLSID\{CLSID_Chimp}\Implemented Categories\{CATID_Mammals}]

[HKCR\CLSID\{CLSID_Chimp}\Implemented Categories\{CATID_Simians}]


Эти элементы реестра обычно добавляются во время саморегистрации. Каждая известная категория компонентов в системе имеет запись в разделе реестра

HKEY_CLASSES_ROOT\Component Categories

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

[HKCR\Component Categories\{CATID_Mammals}] 409="Bears live young" [HKCR\Component Categones\{CATID_Simians}] 409="Eats Bananas"

Отметим, что в этом примере используется величина 409, являющаяся кодом локализации, или локальным идентификатором языка LCID (locale identifier), для U.S.English. Другая местная специфика может поддерживаться путем добавления дополнительных именованных величин.

Классы также могут указать, что они требуют от клиента функциональное назначение определенного типа. Обычно такая поддержка принимает вид узловых интерфейсов (site interfaces), которые клиент предоставляет активированному объекту. Для того, чтобы разделить эти предоставляемые клиентом сервисы на категории, не зависящие от отдельного интерфейса, СОМ позволяет классам объявлять второй тип категорий ID; он может использоваться клиентами для гарантии того, что они не активировали компонент, который не могут должным образом принять. Рассмотрим следующие две категории сервисов, предоставляемых клиентом: CATID_HasOxygen и CATID_HasWater. Поскольку для выживания шимпанзе необходимы кислород и вода, разработчик Chimp должен объявить, что эти две категории сервисов, предоставляемых клиентом, необходимы для активации. Это делается с помощью подключей из Required Categories:

[HKCR\CLSID\{CLSID_Chimp}\Required Categories\{CATID_HasOxygen}] [HKCR\CLSID\{CLSID_Chimp}\Required Categories\{CATID_HasWater}]

Кроме того, ID этих двух категорий следует внести в реестр под ключом

HKEY_CLASSES_ROOT\Component Categories



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

Элементы категорий компонентов могут быть зарегистрированы либо с помощью явных функций реестра, либо с использованием предлагаемого СОМ менеджера категорий компонентов (component category manager). Этот менеджер категорий компонентов объявляется в СОМ как создаваемый СОМ-класс (CLSID_StdComponentCategoriesMgr), который реализует интерфейс ICatRegister для регистрации информации о категории и интерфейс ICatInformation для запроса информации о категории. Интерфейс ICatRegister позволяет библиотекам DLL сервера легко добавлять в реестр необходимые элементы:

[object, uuid(0002E012-0000-0000-C000-000000000046)] interface ICatRegister : IUnknown { // description info for a category // описательная информация для категории typedef struct tagCATEGORYINFO { CATID catid; LCID lcid; OLECHAR szDescription[128]; } CATEGORYINFO;

// register cCts category descriptions // регистрируем описания категории cCts HRESULT RegisterCategories([in] ULONG cCts, [in, size_is(cCts)] CATEGORYINFO rgCatInfo[]);

// unregister cCategories category descriptions // отменяем регистрацию описаний категории cCategories HRESULT UnRegisterCategories([in] ULONG cCategories, [in, size_is(cCategories)] CATID rgcatid[]);

// indicate a class implements one or more categories // показываем, что класс реализует одну или более категорий HRESULT RegisterClassImplCategories([in] REFCLSID rclsid, [in] ULONG cCategories, [in, size_is(cCategories)] CATID rgcatid[]);

// deindicate a class implements one or more categories // перестаем показывать, реализует класс одну или более категорий HRESULT UnRegisterClassImplCategories([in] REFCLSID rclsd, [in] ULONG cCategories, [in, size_is(cCategories)] CATID rgcatid[]);

// indicate a class requires one or more categories // показываем, что класс требует одну или более категорий HRESULT RegisterClassReqCategories([in] REFCLSID rclsid, [in] ULONG cCategories, [in, size_is(cCategories)] CATID rgcatid[]):



// deindicate a class requires one or more categories // перестаем показывать, требует ли класс одну или более категорий HRESULT UnRegisterClassReqCategones([in] REFCLSID rclsid, [in] ULONG cCategories, [in, size_is(cCategories)] CATID rgcatid[]); }

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

В случае примера с Chimp следующий код зарегистрирует правильную информацию о каждой категории:

// get the standard category manager // получим стандартный менеджер категорий

ICatRegister *pcr = 0; HRESULT hr = CoCreateInstance( CLSID_StdComponentCategoriesMgr, 0, CLSCTX_ALL, IID_ICatRegister, (void**)&pcr); if (SUCCEEDED(hr)) { // build descriptions of each category // формируем описания каждой категории CATECORYINFO rgcc[4]; rgcc[0].catid = CATID_Simian; rgcc[1].catid = CATID_Mammal; rgcc[2].catid = CATID_HasOxygen; rgcc[3].catid = CATID_HasWater; rgcc[0].lcid = rgcc[1].lcid = rgcc[2].lcid = rgcc[3].lcid = 0х409; wcscpy(rgcc[0].szDescription, OLESTR("Eats Bananas")); wcscpy(rgcc[1].szDescription, OLESTR("Bears live young")); wcscpy(rgcc[2].szDescription, OLESTR("Provides Oxygen")); wcscpy(rgcc[3].szDescription, OLESTR("Provides Water"));

// register information regarding categories // регистрируем информацию о категориях pcr->RegisterCategories(4, rgcc);

// note that Chimps are Simians and mammals // отметим, что Chimps (шимпанзе) являются Simian // (обезьянами) и Mammal (млекопитающими) CATID rgcid[2]; rgcid[0] = CATID_Simian; rgcid[1] = CATID_Mammal; pcr->RegisterClassImplCategories(CLSID_Chimp, 2, rgcid);

// note that Chimps require Oxygen and Water // отметим, что Chimps (шимпанзе) нуждаются // в кислороде (Oxygen) и воде (Water) rgcid[0] = CATID_HasOxygen; rgcid[1] = CATID_HasWater; pcr->RegisterClassReqCategories(CLSID_Chimp, 2, rgcid); pcr->Release(); }



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

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

[object, uuid(0002E013-0000-0000-C000-000000000046)] interface ICatInformation : IUnknown { // get list of known categories // получаем список известных категорий HRESULT EnumCategories([in] LCID lcid, [out] IEnumCATEGORYINFO** ppeci); // get description of a particular category // получаем описание определенной категории HRESULT GetCategoryDesc([in] REFCATID rcatid, [in] LCID lcid, [out] OLECHAR ** ppszDesc); // get list of classes compatible with specified categories // получаем список классов, совместимых с указанными категориями HRESULT EnumClassesOfCategories( [in] ULONG cImplemented, // -1 indicates ignore // (-1) означает игнорировать [in,size_is(cImplemented)] CATID rgcatidImpl[], [in] ULONG cRequired, // -1 indicates ignore // (-1) означает игнорировать [in,size_is(cRequired)] CATID rgcatidReq[], [out] IEnumCLSID** ppenumClsid);

// verify class is compatible with specified categories // проверяем, совместим ли класс с указанными категориями HRESULT IsClassOfCategories([in] REFCLSID rclsid, [in] ULONG cImplemented, [in,size_is(cImplemented)] CATID rgcatidImpl[], [in] ULONG cRequired, [in,size_is(cRequired)] CATID rgcatidReq[]);

// get list of class's implemented categories // получаем список реализованных категорий класса HRESULT EnumImplCategoriesOfClass([in] REFCLSID rclsid, [out] IEnumCATID** ppenumCatid);

// get list of class's required categories // получаем список категорий, необходимых классу HRESULT EnumReqCategoriesOfClass([in] REFCLSID rclsid, [out] IEnumCATID** ppenumCatid); }

Большинство этих методов возвращают свои курсоры на списки идентификаторов категории или класса. Эти указатели называются нумераторами (enumerators) и подробно описываются в главе 7.



Следующий код показывает, как выделить список классов, являющихся членами категории Mammal:

// get the standard category manager // получаем стандартный менеджер категорий

ICatInformation *pci = 0; HRESULT hr = CoCreateInstance( CLSID_StdComponentCategoriesMgr, 0, CLSCTX_ALL, IID_ICatInformat1on, (void**)&pci); if (SUCCEEDED(hr)) { // get the classes that are Simians (ignore required cats) // получаем классы, являющиеся Simian // (игнорируем требуемые категории) IEnumCLSID *pec = 0; CATID rgcid[1]; rgcid[0] = CATID_Simian; hr = pci->EnumClassesOfCategories(1, rgcid, -1, 0, &pec); if (SUCCEEDED(hr)) { // walk list of CLSIDs 64 at a time // просматриваем список CLSID no 64 за проход enum { MAX = 64 }; CLSID rgclsid[MAX]; do { ULONG cActual = 0; hr = pec->Next(MAX, rgclsid, &cActual); if (SUCCEEDED(hr)) { for (ULONG i = 0; i < cActual; i++) DisplayClass(rgclsid[i]); } } while (hr == S_OK); pec->Release(); } pci->Release(); }

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

Рассмотрим следующий вызов EnumClassesOfCategories:

CATID rgimpl[1]; rgimpl[0] = CATID_Simians; CATID rgreq[3]; rgreq[0] = CATID_HasWater; rgreq[1] = CATID_HasOxygen; rgreq[2] = CATID_HasMilk; hr =pci->EnumClassesOfCategories(1, rgimpl, 3, rgreq, &pec);

Результирующий список классов будет содержать всех приматов (Simians), которые не требуют от среды клиента ничего, кроме кислорода (Oxygen), воды (Water) и молока (Milk). Класс Chimp, зарегистрированный ранее, мог бы быть совместимым классом, так как он реализует специфицированную категорию Simian и требует подмножество специфицированных категорий, использованных в запросе.

Заключительным, причем спорным, аспектом категорий компонентов является представление о классе по умолчанию для категории.СОМ допускает регистрацию CATID в качестве CLSID под ключом реестра

HKEY_CLASSES_ROOT\CLSID

Для преобразования CATID в CLSID по умолчанию используется средство TreatAs, введенное эмуляцией. Для указания того, что класс Gorilla является классом по умолчанию для Simian, необходимо добавить следующий ключ реестра:

[HKCR\CLSID\{CATID_Simian}\TreatAs] @={CLSID_Gorilla}

Это простое соглашение позволяет клиентам просто использовать CATID там, где ожидаются CLSID:

// create an instance of the default Simian class // создаем экземпляр класса Simian, принятого по умолчанию hr = CoCreateInstance(CATID_Simian, 0, CLSCTX_ALL, IID_IApe, (void**)&pApe);

Если для указанной категории не зарегистрировано ни одного класса по умолчанию, то вызов активации даст сбой и вернет REGDB_E_CLASSNOTREG.


Классы


int cGorillas = Gorilla::GetCount(); IApe *pApe = new Gorilla(); pApe->GetYourStinkingPawsOffMeYouDamnDirtyApe();

Charleton Heston, 1968

В предыдущей главе обсуждались принципы интерфейсов СОМ вообще и интерфейс IUnknown в частности. Были показаны способы управления указателями интерфейса из C++, и детально обсуждалась фактическая техника реализации IUnknown. Однако не обсуждалось, как обычно клиенты получают начальный указатель интерфейса на объект, или как средства реализации объекта допускают, чтобы их объекты могли быть обнаружены внешними клиентами. В данной главе демонстрируется, как реализации объектов СОМ интегрируют в среду выполнения СОМ, чтобы дать клиентам возможность найти или создать объекты требуемого конкретного типа.



Классы и IDL


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

[uuid(753A8A7D-A7FF-11d0-8C30-0080C73925BA)] coclass Gorilla { interface IApe; interface IWarrior; }

IDL-определения коклассов (coclass) всегда появляются в контексте определения библиотеки (library definition). В IDL определения библиотек используются для группирования набора типов данных (например, интерфейсы, коклассы, определения типов) в логический блок или пространство имен. Все типы данных, появляющиеся в контексте определения библиотеки IDL, будут отмечены в результирующей библиотеке типов. Библиотеки типов используются вместо IDL-файлов такими средами, как Visual Basic и Java.

Как правило, IDL-файл может содержать один библиотечный оператор, и все типы данных, определенные или использованные внутри определения библиотек, появятся в генерируемой библиотеке типа:

// apes.idl // bring in IDL definitions of ape interfaces // введем IDL-определения интерфейсов обезьян import "apeitfs.idl"; [ uuid(753A8A80-A7FF-11d0-8C30-0080C73925BA), // LIBID - идентификатор библиотеки version(1.0), // version number of library - номер версии библиотеки lcid(9), // locale ID of library (english) // код локализации библиотеки (english) helpstring("Library of the Apes") // title of library - заголовок библиотеки ] library ApeLib { importlib("stdole32.tlb");

// bring in std defs. - вносим стандартные опредепения

[uuid(753A8A7D-A7FF-11d0-8C30-0080C73925BA)] coclass Gorilla { [default] interface IApe; interface IWarrior; }

[uuid(753A8A7E-A7FF-11d0-8C30-0080C73925BA)] coclass Chimpanzee { [default] interface IApe; interface IEgghead; }


[uuid(753A8A7F-A7FF-11d0-8C30-O080C73925BA)] coclass Orangutan { [default] interface IApe; interface IKeeperOfTheFaith; } }

Атрибут [default] показывает, какой из интерфейсов наиболее близко представляет внутренний тип класса. В тех языках, которые распознают этот атрибут, [default] позволяет программисту объявлять ссылки объекта, используя только имя кокласса СОМ:

Dim ursus as Gorilla

Исходя из IDL-определения для Gorilla, данный оператор эквивалентен следующему:

Dim ursus as IApe

поскольку IApe является интерфейсом по умолчанию для класса Gorilla. В любом случае программист мог вызывать методы EatBanana и SwingFromTree с переменной ursus. Если атрибут [default] не указан, то он неявно добавляется к первому интерфейсу в определении coclass.

Имея указанное выше библиотечное определение IDL, результирующий заголовочный файл apes.h будет использовать препроцессор С для включения файла apesitfs.h. Этот файл apesitfs.h будет содержать определения абстрактных базовых классов для четырех интерфейсов СОМ IApe, IWarrior, IKeeperOfTheFaith и IEgghead. Кроме того, файл apes.h будет содержать объявления GUID для каждого класса:

extern "С" const CLSID CLSID_Gorilla; extern "С" const CLSID CLSID_Chimpanzee; extern "С" const CLSID CLSID_Orangutan;

Соответствующий файл apes_i.с будет содержать определения этих CLSID. Сгенерированная библиотека типов apes.tlb будет содержать описания каждого из интерфейсов и классов, что позволит программисту на Visual Basic написать следующее:

Dim ape As IApe Dim warrior as IWarrior Set ape = New Gorilla ' ask СОМ for a new gorilla ' запрашиваем СОМ о новой gorilla Set warrior = ape

А вот так выглядит Java-версия того же самого кода:

IАре аре; IWarrior warrior; аре = new Gorilla(); // no cast needed for [default] // никаких приведений не требуется для [default] ??? warrior = (IWarrior)ape;

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



В предыдущем IDL на каждый из интерфейсов IApe, IWarrior, IEgghead и IKeeperOfTheFaith есть ссылки из определения библиотеки. По этой причине их определения присутствуют в генерируемой библиотеке типов, несмотря та то, что они определены вне области действия определения библиотеки. В действительности любые типы данных, используемые как параметры или как базовые интерфейсы для данных интерфейсов, будут в то же время присутствовать в генерируемой библиотеке. Существует хорошая практика — определять оператор с реализацией библиотеки в отдельном IDL-файле, который импортирует все необходимые определения интерфейсов из внешнего IDL-файла, содержащего только описания интерфейсов. Такая практика является обязательной в больших проектах со многими IDL-файлами, так как для IDL-файла, содержащего определение библиотеки, недопустимо импортировать другой IDL-файл, который также содержит определение библиотеки. Путем разделения определений библиотеки по отдельным IDL-файлам можно корректно импортировать интерфейсы, используемые библиотекой, в другие проекты, не беспокоясь о множественных определениях библиотеки. Если не использовать этот способ, то существует только одна возможность импортировать определение интерфейса из IDL-файла, содержащего определение библиотеки, — использовать директиву importlib:

// humans.idl // apeitfs.idl DOESN'T have a library statement, so import // apeitfs.idl HE ИМЕЕТ оператора library, поэтому импортируем import "apeitfs.idl"; [ uuid(753A8AC9-A7FF-11d0-8C30-0080C73925BA), version(1.0), lcld(9), helpstring("Humans that need apes") // "Люди, нуждающиеся в обезьянах" ]

library HumanLib { importlib("stdole32.tlb"); // bring in std defs. - вносим стандартные определения // Dogs.idl DOES have a library definition, so importlib // its corresponding type library // Dogs.idl ИМЕЕТ определение библиотеки, поэтому // импортируем библиотеку соответствующего типа importlib("dogs.tlb"); [uuid(753A8AD1-A7FF-11d0-8C30-0080C73925BA)] coclass DogApe { interface IDog; interface IApe; } }

В простых проектах часто используется один IDL-файл, в котором определяются как интерфейсы, так и классы, экспортируемые из проекта. Для простых интерфейсов это имеет смысл, так как генерируемая библиотека типов будет содержать взаимно однозначные образы исходных определений IDL, что позволит пользователям этой библиотеки применять importlib без потери информации. К сожалению, в случае сложных интерфейсов многие из исходных IDL-измов (IDL-ism) теряются в результирующей библиотеке типов, и тогда importlib не будет работать так, как хотелось бы. Грядущая версия компилятора MIDL, быть может, будет способна генерировать библиотеки типов, которые будут содержать все из исходного IDL.


Классы и серверы


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

Если в запросе на активацию объекта указана внутрипроцессная активация, то вариант сервера на базе DLL должен быть доступен для загрузки в адресном пространстве клиента. Если же в запросе на активацию указаны внепроцессная или внехостовая активация, то для запуска серверного процесса на указанной хост-машине (она может совпадать с машиной клиента) будет использован исполняемый файл. СОМ поддерживает также выполнение DLL-серверов в суррогатных процессах (surrogate processes) с целью разрешить использование внепроцессной и внехостовой активации существующих внутрипроцессных серверов. Подробности того, как суррогатные процессы связаны с внепроцессной и внехостовой активацией, будут изложены в главе 6.

Чтобы клиенты могли активировать объекты, не беспокоясь о том, как упакован сервер или где он инсталлирован, в СОМ предусмотрена конфигурационная база данных, отображающая CLSID на тот сервер, который реализует этот класс. При использовании версий Windows NT 5.0 или выше основным местом расположения этой конфигурационной базы данных является директория NT (NT Directory). Эта директория является рассредоточенной защищенной базой данных, в которой хранится служебная информация об учетных записях пользователей, хост-машинах и прочее. С тем же успехом в директории NT можно хранить информацию и о СОМ-классах. Эта информация записывается в области директории, называемой СОМ Class Store (хранилище СОМ-классов). СОМ использует Class Store для перевода CLSID в файлы реализации (в случае локальных запросов на активацию) или в удаленные хост-имена (в случае удаленных запросов на активацию). Если запрос на активацию для CLSID сделан на данной машине, то в первую очередь опрашивается локальный кэш.
Если в локальном кэше нет доступной конфигурационной информации, то СОМ посылает запрос в Class Store о том, чтобы реализация стала доступной из локальной машины. Это может просто означать добавление некоторой информации в локальный кэш, чтобы переадресовать запрос на другую хост-машину, или же это может привести к загрузке реализации класса на локальную машину и к запуску программы инсталляции. В любом случае, если класс зарегистрирован в Class Store, он доступен для запроса на активацию со стороны клиента в рамках ограничений безопасности.

Локальный кэш, упоминавшийся при обсуждении Class Store, официально называется системным реестром, или базой конфигурации системы (Registry). Реестр является иерархической базой данных, хранящейся в файлах на каждой машине, которую СОМ использует для преобразования CLSID в имена файлов (в случае локальной активации) или удаленные имена хостов (в случае удаленной активации). До Windows NT 5.0 реестр был единственным местом размещения конфигурационной информации СОМ. Быстрый поиск в реестре может быть осуществлен с помощью иерархических ключей (keys), имена которых представляют собой строки, разделенные обратными косыми чертами. Каждый ключ в реестре может иметь одно или несколько значений, которые могут иметь в своем составе строки, целые значения или двоичные данные. В реализации СОМ на Windows NT 4.0 большая часть ее конфигурационной информации записывается под именем

HKEY_LOCAL_MACHINE\Software\Classes

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

HKEY_CLASSES_ROOT

Реализация СОМ на Windows NT 5.0 продолжает использовать HKEY_CLASSES_ROOT для установок в рамках всей машины, но также разрешает каждому пользователю назначить свою конфигурацию CLSID для обеспечения большей безопасности и гибкости. Под Windows NT 5.0 СОМ вначале опрашивает

HKEY_CURRENT_USER\Software\Classes

прежде чем опрашивать HKEY_CLASSES_ROOT. Для удобства записи часто используются аббревиатуры HKLM, HKCR и HKCU вместо HKEY_LOCAL_MACHINE, HKEY_CLASSES_ROOT и HKEY_CURRENT_USER, соответственно.



СОМ хранит информацию, относящуюся к CLSID всех машин, под ключом реестра

HKCR\CLSID

В версии Windows NT 5.0 или выше СОМ ищет информацию о классах каждого пользователя под ключом

HKCU\Software\Classes\CLSID

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

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}] @="Gorilla"

Для обеспечения локальной активации объектов Gorilla запись для CLSID Gorilla в реестре должна иметь подключ, показывающий, какой файл содержит исполняемый код для методов класса. Если сервер упакован как DLL, то требуется такая запись:

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}\InprocServer32] @="C:\ServerOfTheApes.dll"

Чтобы показать, что код упакован в исполняемом файле, требуется такая запись:

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}\LocalServer32] @="С:\ServerOfTheApes.exe"

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

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}\ProgID] @="Apes.Gorilla.1"

Наоборот, для поддержки функции CLSIDFromProgID требуются следующие величины:

[HKCR\Apes.Gorilla.1] @="Gorilla" [HKCR\Apes.Gorilla.1\CLSID] @="\{571F1680-CC83-11d0-8C48-0080C73925BA}

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

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

STDAPI DllRegisterServer(void); STDAPI DllUnregisterServer(void);

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


Эти подпрограммы должны быть явно экспортированы с использованием или файла определения модуля, или переключателей компоновщика, или директив компилятора. Эти подпрограммы используются хранилищем классов Class Store для конфигурирования локального кэша после загрузки файла на машину клиента. Кроме Class Store эти известные подпрограммы используются различными средами (например, Microsoft Transaction Server, ActiveX Code Download, а также различными инсталляционными программами) для инсталляции или деинсталляции серверов на хост-машинах. В Win32 SDK включена утилита REGSVR32.EXE, которая может инсталлировать или деинсталлировать внутрипроцессный сервер СОМ с использованием этих известных экспортированных функций.

Реализации внутрипроцессных серверов DllRegisterServer и DllUnregisterServer должны запросить реестр на добавление или удаление соответствующих ключей, преобразующих CLSID и ProgID сервера в файловые имена сервера. Хотя существуют различные способы реализации этих подпрограмм, наиболее гибким и эффективным из них является создание строковой таблицы, содержащей соответствующие ключи, названия величин, сами величины и простое перечисление всех записей в таблице, путем вызова RegSetValueEx для инсталляции и RegDeleteKey для деинсталляции. Чтобы осуществить регистрацию, основанную на этой технологии, сервер может просто задать массив строк размером Nx3, где каждый ряд массива содержит строки для использования в качестве ключей, имена величин и величины:

const char *g_RegTable[][3] = { // format is { key, value name, value } { "CLSID\\{571F1680-CC83-11d0-8C48-0080C73925BA}", 0, "Gorilla" }, { "CLSID\\{571F1680-CC83-11d0-8C48-0080C73925BA}\\InprocServer32",0, (const char*)-1 // rogue value indicating file name // нестандартное значение, указывающее имя файла },

{ "CLSID\\{571F1680-CC83-11d0-8C48-0080C73925BA}\\ProgID", 0, "Ареs.Gorilla.1" },

{ "Apes.Gorillа.1", 0, "Gorilla" }, { "Apes.Gorilla.1\\CLSID", 0, "{571F1680-CC83-11d0-8C48-0080C73925BA}" }, };



Имея эту таблицу, весьма несложно осуществить реализацию DllRegisterServer:

STDAPI DllRegisterServer(void) { HRESULT hr = S_OK; // look up server's file name // ищем имя файла сервера char szFileName[MAX_PATH]; GetModuleFileNameA(g_hinstDll, szFileName, MAX_PATH); // register entries from table // регистрируем записи из таблицы int nEntries = sizeof(g_RegTable)/sizeof(*g_RegTable); for (int i = 0; SUCCEEDED(hr) && i < nEntries; i++) { const char *pszKeyName = g_RegTable[i][0]; const char *pszValueName = g_RegTable[i][1]; const char *pszvalue = g_RegTable[i][2]; // map rogue value to module file name // переводим нестандарное значение в имя файла модуля if (pszValue == (const char*)-1) pszValue = szFileName; HKEY hkey; // create the key // создаем ключ long err = RegCreateKeyA(HKEY_CLASSES_ROOT, pszKeyName, &hkey); if (err == ERROR_SUCCESS) { // set the value // присваиваем значение err = RegSetValueExA(hkey, pszVvalueName, 0, REG_SZ, (const BYTE*) pszValue, (strlen(pszValue) + 1)); RegCloseKey(hkey); } if (err != ERROR_SUCCESS) { // if cannot add key or value, back out and fail // если невозможно добавить ключ или значение, то откат и сбой DllUnregisterServer(); hr = SELFREG_E_CLASS; } } return hr; }

Соответствующая DllUnregisterServer будет выглядеть так:

STDAPI DllUnregisterServer(void) { HRESULT hr = S_OK; int nEntries = sizeof(g_RegTable)/sizeof(*g_RegTable); for (int i = nEntries - 1; i >= 0; i--) { const char *pszKeyName = g_RegTable[i][0]; long err = RegDeleteKeyA(HKEY_CLASSES_ROOT, pszKeyName); if (err != ERROR_SUCCESS) hr = S_FALSE; } return hr; }

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



Так как СОМ преобразует CLSID в данный файл реализации, то для объявления в СОМ относящихся к серверу объектов класса необходимо использовать определенные стандартные методики. Для сервера, основанного на исполняемой программе, в СОМ предусмотрены явные API-функции для связывания объектов класса с их CLSID. Эти API-функции мы будем подробно обсуждать в главе 6. Для сервера, основанного на DLL, DLL должна экспортировать известную функцию, которая будет вызываться с помощью CoGetClassObject, когда потребуется объект класса. Эту функцию необходимо экспортировать с использованием файла определения модулей, причем она должна иметь следующий вид:

HRESULT DllGetClassObject( [in] REFCLSID rclsid, // which class object? // какой объект класса? [in] REFIID riid, // which interface? // какой интерфейс? [out, iid_is(riid)] void **ppv); // put it here! // разместить его здесь!

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

Рассмотрим сервер, реализующий три класса: Gorilla, Chimp и Orangutan. Сервер, возможно, будет содержать шесть отдельных классов C++: три из них создают экземпляры каждого класса, а другие три — объекты класса для каждого класса. В соответствии с этим сценарием, серверная реализация DllGetClassObject будет выглядеть следующим образом:

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void **ppv) { // define a singleton class object for each class // определяем одноэлементный объект класса // для каждого класса static GorillaClass s_gorillaClass; static OrangutanClass s_orangutanClass; static ChimpClass s_chimpClass; // return interface pointers to known classes // возвращаем указатели интерфейсов известных классов if (rclsid == CLSID_Gorilla) return s_gorillaClass.QueryInterface(riid, ppv); else if (rclsid == CLSID_Orangutan) return s_orangutanClass.QueryInterface(riid, ppv); else if (rclsid == CLSID_Chimp) return s_chimpClass.QueryInterface(riid, ppv); // if we get to here, rclsid is a class we don't implement, // so fail with well-known error code // если мы добрались сюда, то rclsid - это класс, который // мы не реализуем, поэтому сбой с известным кодом ошибки *ppv = 0; return CLASS_E_CLASSNOTAVAILABLE; }



Заметим, что приведенный код не заботится о том, какой интерфейс объявляется каждым из объектов класса. Он просто отправляет запрос QueryInterface соответствующему объекту класса.

Следующий псевдокод показывает, как API-функция CoGetClassObject устанавливает связь с серверным DllGetClassObject:

// pseudo-code from OLE32.DLL // псевдокод из OLE32.DLL

HRESULT CoGetClassObject(REFCLSID rclsid, DWORD dwClsCtx, COSERVERINFO *pcsi , REFIID riid, void **ppv) { HRESULT hr = REGDB_E_CLASSNOTREG; *ppv = 0; if (dwClsCtx & CLSCTX_INPROC) { // try to perform inproc activation // пытаемся выполнить внутрипроцессную активацию HRESULT (*pfnGCO)(REFCLSID, REFIID, void**) = 0; // look in table of already loaded servers in this process // просматриваем таблицу уже загруженных серверов внутри // этого процесса pfnGCO = LookupInClassTable(rclsid, dwClsCtx); if (pfnGCO == 0) { // not loaded yet! // еще не загружен! // ask class store or registry for DLL name // запрашиваем DLL-имя в хранилище классов или в реестре char szFileName[MAX_PATH]; hr = GetFileFromClassStoreOrRegistry(rclsid, dwClsCtx, szFileName); if (SUCCEEDED(hr)) { // try to load the DLL and scrape out DllGetClassObject // пытаемся загрузить DLL и вытащить DllGetClassObject HINSTANCE hInst = LoadLibrary(szFileName); if (hInst == 0) return CO_E_DLLNOTFOUND; pfnGCO = GetProcAddress(hInst, "DllGetClassObject"); if (pfnGCO == 0) return CO_E_ERRORINDLL; // cache DLL for later use // кэшируем DLL для дальнейшего использования InsertInClassTable(rclsid, dwClsCtx, hInst, pfnGCO); } } // call function to get pointer to class object // вызываем функцию для получения указателя на объект класса hr = (*pfnGCO)(rclsid, riid, ppv); } if ((dwClsCtx & (CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER)) && hr == REGDB_E_CLASSNOTREG) { // handle out-of-proc/remote request // обрабатываем внепроцессный/удаленный запрос } return hr; }

Отметим, что реализация CoGetClassObject является единственным местом, откуда вызывается DllGetClassObject.


Чтобы укрепить это обстоятельство, компоновщики в модели СОМ выдадут предупреждение в случае, если входная точка DllGetClassObject экспортируется без использования ключевого слова private в соответствующем файле определения модулей:

// from APELIB.DEF // из APELIB.DEF LIBRARY APELIB EXPORTS DllGetClassObject private

Фактически в модели СОМ компоновщики предпочитают, чтобы во всех связанных с СОМ точках входа использовалось это ключевое слово.

1

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

2

Приведенный здесь способ записи использует стандартный синтаксис REGEDIT4. Строки, содержащиеся внутри скобок, соответствуют именам ключей. Пары имя=значение (name = value) под ключом обозначают значения, присвоенные указанному ключу. Необычное имя "@" показывает значение ключа по умолчанию.


Моникеры и композиция


Моникеры часто составляются из других моникеров, чтобы с помощью текстового описания пути можно было осуществлять навигацию по иерархиям объектов. Чтобы обеспечить простую поддержку этого типа управления, в СОМ предусмотрена стандартная реализация моникеров, которая, будучи поставленной справа от другого моникера, запрашивает объект связать ссылку с другим объектом в иерархии. Такой моникер называется моникером элемента (Item Moniker) и использует интерфейс объекта IOleItemContainer для преобразования имени объекта в интерфейсный указатель.

Следующее отображаемое имя показывает, как моникер элемента использован в тандеме с классовым моникером:

clsid:571F1680-CC83-11d0-8C48-0080C73925BA:!Ursus

Отметим использование символа "!" для отделения отображаемого имени Class Moniker от имени элемента (item name) "Ursus". При анализе MkParseDisplayName сначала использует префикс "clsid" в качестве ProgID для контакта с реализацией Сlass Moniker. Затем MkParseDisplayName предложит реализации Class Moniker проанализировать часть строки — столько, сколько она сможет распознать. Это означает, что после того, как Сlass Moniker извлек свой GUID из строки, ее следующий фрагмент все еще нуждается в анализе:

!Ursus

Поскольку это имя имеет смысл только в области действия объекта, именованного моникером слева от него, фактически MkParseDisplayName присваивает значение крайнему левому моникеру (моникеру классового типа) и запрашивает объект, который он именует (объект класса Gorilla) проанализировать оставшуюся часть строки. Для поддержки анализа отображаемых имен СОМ определяет стандартный интерфейс IParseDisplayName:

[ object,uuid(0000011a-0000-0000-C000-000000000046) ] interface IParseDisplayName : IUnknown { // convert display name to a moniker // преобразуем отображаемое имя в моникер HRESULT ParseDisplayName( [in, unique] IBindCtx *pbc, [in] LPOLESTR pszDisplayName, [out] ULONG *pchEaten, [out] IMoniker **ppmkOut ); }

В случае отображаемого имени, использованного в этом примере, объекту класса Gorilla потребуется реализовать IParseDisplayName и преобразовать строку "!Ursus" в моникер, который MkParseDisplayName поставит справа от моникера классового типа.
Поскольку требуется стандартный моникер элемента, то достаточно будет такой реализации:

STDMETHODIMP GorillaClass::ParseDisplayName(IBindCtx *pbc, LPOLESTR pszDisplayName, ULONG *pchEaten, IMoniker **ppmkOut) { // create an item moniker using explicit API function // создаем отдельный моникер, используя явную API-функцию HRESULT hr = CreateItemMoniker(OLESTR("!"), pszDisplayName + 1, ppmkOut); // indicate how many characters were parsed // показываем, сколько символов было проанализировано if (SUCCEEDED(hr)) *pchEaten = wcslen(pszDisplayName); else *pchEaten = 0; return hr; }

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

Так как было проанализировано два моникера, то MkParseDisplayName будет собирать эти моникеры вместе, используя групповой композитный моникер (generic composite moniker). Групповой композитный моникер просто удерживает два моникера вместе. Реализация группового композитного моникера BindToObject сначала связывает моникер справа, передавая ему указатель на моникер слева через параметр pmkToLeft. Это иллюстрируется следующим псевдокодом:

// pseudo-code from OLE32.0LL // псевдокод из OLE32.DLL STDMETHODIMP GenericComposite::BindToObject (IBindCtx *pbc, IMoniker *pmkToLeft, REFIID riid, void **ppv) { return m_pmkRight->BindToObject(pbc, m_pmkLeft, riid, ppv); }

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

Ранее мы установили, что моникер элемента использует интерфейс IOleItemContainer для связывания интерфейсного указателя. Ниже приведен псевдокод для реализации моникера элемента ВindToObject:

// pseudo-code from OLE32.DLL // псевдокод из OLE32.DLL STDMETHODIMP ItemMoniker::BindToObject( IBindCtx *pbc, IMoniker *pmkToLeft, REFIID riid, void **ppv) { // assume failure // допускаем возможность сбоя *ppv = 0; if (pmkToLeft == 0) //requires a scope - требуется область действия return E_INVALIDARG; // first bind moniker to left // сначала привязываем моникер слева IOleItemContainer *poic = 0; HRESULT hr = pmkToLeft->BindToObject(pbc, 0, IID_IOleItemContainer, (void**)&poic); if (SUCCEEDED(hr)) { // cache the bound object in binding context // кэшируем связанный объект в контексте связывания pbc->RegisterObjectBound(poic); // get bind speed from Bind Context // получаем быстроту связывания из контекста связывания DWORD dwBindSpeed = this->MyGetSpeedFromCtx(pbc); // ask object for named sub-object // запрашиваем объект об именованном подобъекте hr = poic->GetObject(m_pszItem, dwBindSpeed, pbc, riid, ppv); poic->Release(); } }



Эта реализация означает, что такой код:

HRESULT GetUrsus(IApe *&rpApe) { const OLECHAR pwsz[] = OLESTR("clsid:571F1680-CC83-11d0-8C48-0080C73925BA:!Ursus"); return CoGetObject(pwsz, 0, IID_IApe, (void**)&rpApe); }

эквивалентен следующему:

HRESULT GetUrsus(IApe *&rpApe) { IOleItemContainer *poic = 0; HRESULT hr = CoGetClassObject(CLSID_Gorilla, CLSCTX_ALL, 0, IID_IOleItemContainer, (void**)&poic); if (SUCCEEDED(hr)) { hr = poic->GetObject(OLESTR("Ursus"), BINDSPEED_INFINITE, 0, IID_IApe, (void**)&rpApe); poic->Release(); } return hr; }

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


Моникеры и сохраняемость


Обсуждение моникеров не может быть полным без обсуждения файлового моникера (File Moniker). Напомним, что СОМ предусматривает три примитива активации: привязывание к объектам класса, привязывание к новым экземплярам класса и привязывание к постоянным объектам, хранящимся в файлах. В данной главе детально анализировались первые два из этих примитивов. Третий примитив основан на API-функции СОМ CoGetInstanceFromFile:

HRESULT CoGetInstanceFromFile( [in, unique] COSERVERINFO *pcsi, // host/security info - информация о хосте/безопасности [in, unique] CLSID *pClsid, // explicit CLSID (opt) - явный CLSID (opt) [in, unique] IUnknown *punk0uter, // for aggregation - для агрегирования [in] DWORD dwClsCtx, // locality? - локализация? [in] DWORD grfMode, // file open mode - режим открытия файла [in] OLECHAR *pwszName, // file name of object - файловое имя объекта [in] DWORD cmqi, // how many interfaces? - сколько интерфейсов? [out, size_is(cmqi)] MULTI_QI *prgmq // where to put itfs - куда поместить интерфейсы );

Эта функция принимает на вход имя файла, которое относится к постоянному состоянию (persistent state) объекта. CoGetInstanceFromFile удостоверяется в том, что объект исполняется, после чего возвращает один или несколько интерфейсных указателей на (ре)активированный объект. Чтобы выполнить эту работу, CoGetInstanceFromFile в первую очередь требуется определить CLSID данного объекта. CLSID требуется по двум причинам. Если объект не исполняется, то CLSID необходим СОМ для создания нового экземпляра, который будет инициализирован от постоянного (находящегося в файле) образа. Во-вторых, если вызывающий объект не указывает определенное хост-имя до запроса на активацию, то СОМ будет использовать CLSID для выяснения того, на какой машине следует активировать объект.

Если CLSID не передается явно вызывающим объектом, то CoGetInstanceFromFile извлекает CLSID из самого файла с помощью вызова API-функции СОМ GetClassFile:

HRESULT GetClassFile([in, string) OLECHAR *pwszFileName, [out] CLSID *pclsid);


Для определения типа объекта, содержащегося в файле, GetClassFile использует информацию из заголовка файла, а также информацию из реестра.

После того как определены класс и хост-машина, СОМ исследует ROT (Running Object Table — таблица исполняющихся объектов) на целевой хост-машине для того, чтобы выяснить, является ли объект уже активированным. Таблица ROT является инструментом SCM, который преобразует произвольные моникеры в экземпляры объектов, исполняющихся на локальной хост-машине. Ожидается, что постоянные объекты будут регистрировать себя в локальной ROT во время загрузки. Чтобы представить файловое имя постоянного объекта в качестве моникера, СОМ предусматривает стандартный тип моникера — файловый моникер, который оборачивает имя файла в интерфейс IMoniker. Файловые моникеры могут создаваться либо путем передачи файлового имени в МkParseDisplayName, либо вызовом явной API-функции CreateFileMoniker:

HRESULT CreateFileMoniker( [in, string] const OLECHAR *pszFileName, [out] IMoniker **ppmk);

Если постоянный объект уже зарегистрировал в ROT свой файловый моникер, то CoGetInstanceFromFile просто возвращает указатель на уже работающий объект. Если же объект в ROT не найден, то СОМ создает новый экземпляр файлового класса и инициализирует его из постоянного объекта с помощью метода IPersistFile::Load этого экземпляра:

[object, uuid(0000010b-0000-0000-C000-000000000046)] interface IPersistFile : IPersist { // called by CoGetInstanceFromFile to initialize object // вызывается функцией CoGetInstanceFromFile для // инициализации объекта HRESULT Load( [in, string] const OLECHAR * pszFileName, [in] DWORD grfMode ); // remaining methods deleted for clarity // остальные методы удалены для ясности }

Реализация объекта отвечает за загрузку из файла всех постоянных элементов и за саморегистрацию в локальной таблице ROT — с целью убедиться, что для каждого файла в каждый момент может исполняться только один экземпляр:

STDMETHODIMP::Load(const OLECHAR *pszFileName, DWORD grfMode) { // read in persisted object state // считываем сохраненное состояние объекта HRESULT hr = this->MyReadStateFromFile(pszFile, grfMode); if (FAILED(hr)) return hr; // get pointer to ROT from SCM // берем указатель на ROT от SCM IRunningObjectTable *prot = 0; hr = GetRunningObjectTable(0, &prot); if (SUCCEEDED(hr)) { // create a file moniker to register in ROT // создаем файловый моникер для регистрации в ROT IMoniker *pmk = 0; hr = CreateFileMoniker(pszFileName, &pmk); if (SUCCEEDED(hr)) { // register self in ROT // саморегистрация в ROT hr = prot->Register(0, this, pmk, &m_dwReg); pmk->Release(); } prot->Release(); } return hr; }



Метод IPersistFile:: Load нового созданного экземпляра будет вызван диспетчером SCM во время выполнения CoGetInstanceFromFile. В приведенном выше примере для получения указателя на интерфейс IRunningObjectTable в SCM используется API-функция СОМ GetRunningObjectTable. Этот интерфейс затем используется для регистрации своего моникера в ROT, так что последующие вызовы CoGetInstanceFromFile, использующие то же файловое имя, не будут создавать новые объекты, а вместо этого возвратят ссылки на этот объект.

Существование File Moniker обусловлено двумя причинами. Во-первых, он нужен, чтобы позволить объектам самим регистрироваться в ROT, чтобы их мог найти CoGetInstanceFromFile. Во-вторых, чтобы скрыть от клиента использование CoGetInstanceFromFile за интерфейсом IMoniker. Реализация File Moniker из BindToObject просто вызывает CoGetInstanceFromFile:

// pseudo-code from OLE32.DLL // псевдокод из OLE32.DLL

STDMETHODIMP FileMoniker::BindToObject(IBindCtx *pbc, IMoniker *pmkToLeft, REFIID riid, void **ppv) { // assume failure - на случай сбоя *ppv = О; HRESULT hr = E_FAIL; if (pmkToLeft == 0) { // no moniker to left - слева моникеров нет MULTI_QI mqi = { &riid, 0, 0 }; COSERVERINFO *pcsi; DWORD grfMode; DWORD dwClsCtx; // these three parameters are attributes of the BindCtx // эти три параметра являются атрибутами BindCtx this->MyGetFromBindCtx(pbc, &pcsi, &grfMode, &dwClsCtx); hr = CoGetInstanceFromFile(pcsi, 0, 0, dwClsCtx, grfMode, this->m_pszFileName, 1, &mqi); if (SUCCEEDED(hr)) *ppv = mqi.pItf; } else { // there's a moniker to the left - слева есть моникер // ask object to left for IClassActivator // or IClassFactory // запрашиваем объект слева от IClassActivator или от // IClassFactory } return hr; }

При таком поведении File Moniker следующая функция, вызывающая CoGetInstanceFromFile

HRESULT GetCornelius(IApe * &rpApe) { OLECHAR *pwszObject = OLESTR("\\\\server\\public\\cornelius.chmp"); MULTI_QI mqi = { &IID_IApe, 0, 0 }; HRESULT hr = CoGetInstanceFromFile(0, 0, 0, CLSCTX_SERVER, STCM_READWRITE, pwszObject, 1, &mqi); if (SUCCEEDED(hr)) rpApe = mqi.pItf; else rpApe = 0; return hr; }



может быть упрощена, если вместо этого использовать вызов CoGetObject:

HRESULT GetCornelius(IApe * &rpApe) { OLECHAR *pwszObject = OLESTR("\\\\server\\public\\cornelius.chmp"); return CoGetObject(pwszObject, 0, IID_IApe, (void**)&rpApe); }

Как и в предыдущем случае, когда использовался Class Moniker, уровень изоляции, обеспеченный CoGetObject, позволяет клиенту задавать сколь угодно сложную стратегию активации, не меняя ни единой строки кода.

1

Альтернативная версия этой API-функции. CoGetInstanceFromIStorage, вместо имени файла принимает указатель на иерархическое хранилище (storage medium).

2

В дополнение к обычной переадресации CLSID на хост-машины, которое используется функциями CoGetClassObject/CoCreateInstanceEx, CoGetInstanceFromFile может использовать в качестве имени файла UNC-имя хоста (universal naming convention — общее соглашение по именам), чтобы переадресовать запрос на активацию на ту хост-машину, где расположен данный файл. Этот режим активации упоминается в Спецификации СОМ как побитовая активация ("AtВits" activation) и описывается с использованием установок реестра "ActivateAtStorage", как описано в главе 6.

3

На практике областью действия ROT является не вся машина, а только Winstation. Это означает, что по умолчанию не все зарегистрированные сессии (logon sessions) получат доступ к объекту. Чтобы убедиться, что объект является видимым для всех возможных клиентов, при вызове IRunningObjectTable::Register объект должен выставить флаг ROTFLAGS_ALLOWANYCLIENT.


Объекты классов


Основное требование всех СОМ-классов состоит в том, что они должны иметь объект класса. Объект класса — это единственный экземпляр (синглетон), связанный с каждым классом, который реализует функциональность класса, общую для всех его экземпляров. Объект класса ведет себя как метакласс по отношению к заданной реализации, а реализуемые им методы выполняют роль статических функций-членов из C++. По логике вещей, может быть только один объект класса в каждом классе; однако в силу распределенной природы СOМ каждый класс может иметь по одному объекту класса на каждую хост-машину (host machine), на учетную запись пользователя или на процесс, — в зависимости от того, как используется этот класс. Первой точкой входа в реализацию класса является ее объект класса.

Объекты класса являются очень полезными программистскими абстракциями. Объекты класса могут вести себя как известные объекты (когда их идентификатор CLSID выступает в качестве имени объекта), которые позволяют нескольким клиентам связываться с одним и тем же объектом, определенным с помощью данного CLSID. В то время как системы в целом могли быть созданы с использованием исключительно объектов класса, объекты класса часто используются как посредники (brokers) при создании новых экземпляров класса или для того, чтобы найти имеющиеся экземпляры, определенные с помощью какого-нибудь известного имени объекта. При использовании в этой роли объект класса обычно объявляет только один или два промежуточных интерфейса, которые позволят клиентам создать или найти те экземпляры, которые в конечном счете будут выполнять нужную работу. Например, рассмотрим описанный ранее интерфейс IАре. Объявление интерфейса IАре не нарушит законы СОМ для объекта класса:

class GorillaClass : public IApe { public: // class objects are singletons, so don't delete // объекты класса существуют в единственном экземпляре, // так что не удаляйте их IMPLEMENT_UNKNOWN_NO_DELETE (GorillaClass) BEGIN_INTERFACE_TABLE(GorillaClass) IMPLEMENTS_INTERFACE(IApe) END_INTERFACE_TABLE() // IApe methods // методы IApe STDMETHODIMP EatBanana(void); STDMETHODIMP SwingFromTree(void); STDMETHODIMP get_Weight(long *plbs); };


Если для данного класса C++ может существовать лишь один экземпляр (так ведут себя все объекты классов в СОМ), то в любом заданном экземпляре может быть только одна горилла (gorilla). Для некоторых областей одноэлементных множеств достаточно. В случае с гориллами, однако, весьма вероятно, что клиенты могут захотеть создавать приложения, которые будут использовать несколько различных горилл одновременно. Чтобы обеспечить такое использование, объект класса не должен экспортировать интерфейс IApe, а вместо этого должен экспортировать новый интерфейс, который позволит клиентам создавать новых горилл и/или находить известных горилл по их имени. Это потребует от разработчика определить два класса C++: один для реализации объекта класса и другой для реализации действительных экземпляров класса. Для реализации гориллы класс C++, который определяет экземпляры гориллы, будет реализовывать интерфейс IApe:

class Gorilla : public IApe { public: // Instances are heap-based, so delete when done // копии размещены в куче, поэтому удаляем после выполнения IMPLEMENT_UNKNOWN() BEGIN_INTERFACE_TABLE() IMPLEMENTS_INTERFACE(IApe) END_INTERFACE_TABLE() // IApe methods // методы IApe STDMETHODIMP EatBanana(void); STDMETHODIMP SwingFromTree(void); STDMETHODIMP get_Weight(long *plbs): };

Второй интерфейс понадобится для определения тех операций, которые будет реализовывать объект класса Gorilla:

[object, uuid(753A8AAC-A7FF-11d0-8C30-0080C73925BA)] interface IApeClass : IUnknown { HRESULT CreateApe([out, retval] IApe **ppApe); HRESULT GetApe([in] long nApeID, [out, retval] IApe **ppApe); [propget] HRESULT AverageWeight([out, retval] long *plbs); }

Получив это определение интерфейса, объект класса будет реализовывать методы IApeClass или путем создания новых экземпляров С++-класса Gorilla (в случае CreateApe), или преобразованием произвольно выбранного имени объекта (в данном случае типа integer) в отдельный экземпляр (в случае GetApe):

class GorillaClass : public IApeClass { public: IMPLEMENT_UNKNOWN_NO_DELETE(GorillaClass) BEGIN_INTERFACE_TABLE(GorillaClass) IMPLEMENTS_INTERFACE(IApeClass) END_INTERFACE_TABLE() STDMETHODIMP CreateApe(Ape **ppApe) { if ((*ppApe = new Gorilla) == 0) return E_OUTOFMEMORY; (*ppApe)->AddRef(); return S_OK; }



STDMETHODIMP GetApe(long nApeID, IApe **ppApe) { // assume that a table of well-known gorillas is // being maintained somewhere else // допустим, что таблица для известных горилл // поддерживается где-нибудь еще

extern Gorilla *g_rgWellKnownGorillas[]; extern int g_nMaxGorillas;

// assert that nApeID is a valid index // объявляем, что nApeID - допустимый индекс *ррАре = 0; if (nApeID > g_nMaxGorillas nApeID < 0) return E_INVALIDARG; // assume that the ID is simply the index into the table // допустим, что ID - просто индекс в таблице if ((*ppApe = g_rgWellKnownGorillas[nApeID]) == 0) return E_INVALIDARG; (*ppApe)->AddRef(); return S_OK; }

STDMETHODIMP get_AverageWeight(long *plbs) { extern *g_rgWellKnownGorillas[]; extern int g_nMaxGorillas; *plbs = 0; long lbs; for (int i = 0; i < g_nMaxGorillas; i++) { g_rgWellKnownGorillas[i]->get_Weight(&lbs); *plbs += lbs; }

// assumes g_nMaxGorillas is non-zero // предполагается, что g_nMaxGorillas ненулевой *plbs /= g_nMaxGorillas; return S_OK; } };

Отметим, что в этом коде предполагается, что внешняя таблица известных горилл уже поддерживается — или самими копиями Gorilla, или каким-нибудь другим посредником (agent).


Обобщения


В предыдущем примере интерфейс IApeClass рассматривался как интерфейс уровня класса, специфический для классов, которые объявляют интерфейс IАре из своих экземпляров. Этот интерфейс позволяет клиентам создавать новые объекты или находить существующие, но в любом случае результирующие объекты должны реализовывать интерфейс IАре. Если бы новый класс хотел разрешить клиентам создавать или находить объекты, несовместимые с IApe, то объект этого класса должен был бы реализовывать другой интерфейс. Поскольку создание и поиск объектов являются общими требованиями, которые большинство классов хотели бы поддерживать, СОМ определяет стандартные интерфейсы для моделирования поиска и создания объектов унифицированным образом (generically). Один стандартный интерфейс для поиска объектов назван IOleItemContainer:

// from oleidl.idl из oleidl.idl

[ object, uuid(0000011c-0000-0000-C000-000000000046) ] interface IOleItemContainer : IOleContainer { // ask for object named by pszItem // запрашиваем объект, именованный pszItem HRESULT Get0bject( [in] LPOLESTR pszItem, // which object? какой объект? [in] DWORD dwSpeedNeeded, // deadline [in, unique] IBindCtx *pbc, // binding info информация о связывании [in] REFIID riid, // which interface? какой интерфейс? [out, iid_is(riid)] void **ppv); // put it here! разместим его здесь!

// remaining methods deleted for clarity // остальные методы удалены для ясности }

Отметим, что метод GetObject позволяет клиенту задавать тип результирующего интерфейсного указателя. Действительный класс результирующего объекта зависит от контекста и конкретной реализации IOleItemContainer. Следующий пример запрашивает объект класса Gorilla найти объект под именем "Ursus":

HRESULT FindUrsus(IApe * &rpApe) { // bind a reference to the class object // связываем ссылку с объектом класса rpApe = 0; IOleItemContainer *poic = 0; HRESULT hr = CoGetClassObject(CLSID_Gorilla, CLSCTX_ALL, 0, IID_IOleItemContainer, (void**)&poic); if (SUCCEEDED(hr)) { // ask Gorilla class object for Ursus // запрашиваем объект класса Gorilla на поиск Ursus hr = poic->GetObject(OLESTR("Ursus"), BINDSPEED_INDEFINITE, 0, IID_IApe, (void**)&rpApe); poic->Release(); } return hr; }


Хотя такое использование вполне допустимо, интерфейс IOleItemContainer был предназначен для работы в тандеме с моникером элемента (Item Moniker), который будет рассматриваться позже в данной главе.

В СОМ определен еще один стандартный интерфейс для создания объектов. Он называется IClassFactory:

// from unknwn.idl из unknwn.idl [ object, uuid(00000001-0000-0000-C000-000000000046) ] interface IClassFactory : IUnknown { HRESULT CreateInstance( [in] IUnknown *pUnkOuter, [in] REFIID riid, [out, iid_is(riid)] void **ppv) ;

HRESULT LockServer([in] BOOL bLock); }

Хотя экземпляры класса могли бы экспортировать интерфейс IClassFactory, данный интерфейс обычно экспортируется только объектами класса. Объекты класса не обязаны реализовывать IClassFactory, но, для единообразия, они часто делают это. В момент написания этой книги классы, которые будут встраиваться в среду Microsoft Transaction Server (MTS), должны реализовывать IClassFactory (фактически никакие другие интерфейсы объектов класса не будут распознаваться в MTS).

Интерфейс IClassFactory имеет два метода: LockServer и CreateInstance. Метод LockServer вызывается внутри СОМ во время запроса на внепроцессную активацию и подробно обсуждается в главе 6. Метод CreateInstance используется для запроса на создание объектом класса нового экземпляра класса. Как было в случае IApeClass::CreateApe, тип объекта, который будет подвергаться обработке, определяется объектом класса, которому клиент посылает запрос CreateInstance. Первый параметр CreateInstance используется в агрегировании СОМ и обсуждается в главе 4. Пока же, в рамках третьей главы, для простоты изложения положим этот параметр равным нулю. Второй и третий параметры CreateInstance позволяют методу возвращать клиенту динамически типизируемый указатель интерфейса.

Предполагая, что объект класса экспортирует интерфейс IClassFactory вместо IApeClass, клиенты должны использовать метод IClassFactory::CreateInstance для создания новых экземпляров :

HRESULT CreateAGorillaAndEatBanana() { IClassFactory *pcf = 0; // find the class object находим объект класса HRESULT hr = CoGetClassObject(CLSID_Gorilla, CLSCTX_ALL, 0, IID_IClassFactory, (void **)&pcf); if (SUCCEEDED(hr)) { IApe *pApe = 0; // use the class object to create a gorilla // используем объект класса для создания gorilla hr = pcf->CreateInstance(0, IID_IApe, (void**)&pApe); // we're done with the class object, so release it // мы закончили с объектом класса, поэтому освобождаем его pcf->Release(); if (SUCCEEDED(hr)) { // tell the new gorilla to eat a banana // приказываем новой горилле есть банан hr = pApe->EatBanana(); pApe->Release(); } } return hr; }



Этот код является семантически идентичным варианту с функцией, которая использовала интерфейс IApeClass вместо интерфейса IClassFactory.

Для того чтобы предыдущий пример работал корректно, объекту класса Gorilla следует реализовать IClassFactory:

class GorillaClass : public IClassFactory { public: IMPLEMENT_UNKNOWN_NO_DELETE(GorillaClass) BEGIN_INTERFACE_TABLE(GorillaClass) IMPLEMENTS_INTERFACE(IClassFactory) END_INTERFACE_TABLE() STDMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv) { *ppv = 0; if (pUnkOuter != 0) // we don't support aggregation yet // мы еще не поддерживаем агрегирование return CLASS_E_NOAGGREGATION; // create a new instance of our C++ class Gorilla // создаем новый экземпляр нашего С++-класса Gorilla Gorilla *p = new Gorilla; if (p == 0) return E_OUTOFMEMORY: // increment reference count by one // увеличиваем счетчик ссылок на единицу p->AddRef(); // store the resultant interface pointer into *ppv // записываем результирующий указатель интерфейса в *ppv HRESULT hr = p->QueryInterface(riid, ppv); // decrement reference count by one, which will delete the // object if QI fails // уменьшаем на единицу счетчик ссылок, // что уничтожит объект при отказе QI p->Release(); // return result of Gorilla::QueryInterface // возвращаем результат работы Gorilla::QueryInterface return hr; }

STDMETHODIMP LockServer(BOOL bLock); };

Реализация LockServer будет обсуждаться в этой главе позже. Отметим, что реализация CreateInstance, в первую очередь, создает новый объект C++ на базе класса Gorilla и запрашивает объект, поддерживает ли он нужный интерфейс. Если объект поддерживает требуемый интерфейс, то вызов QueryInterface инициирует вызов AddRef, и клиент в конечном счете выполнит соответствующий вызов Release. Если же произойдет отказ QueryInterface, то потребуется некоторый механизм для уничтожения созданного нового объекта. Предыдущий пример использует стандартную технологию "заключения в скобки" (bracketing) вызова QueryInterface между парой AddRef/Release.Если произошел сбой вызова QueryInterface, то вызов Release сбросит счетчик ссылок на нуль, запуская тем самым удаление объекта. Если же вызов QueryInterface прошел успешно, то вызов Release установит счетчик ссылок на единицу. Остающаяся ссылка принадлежит клиенту, который и выполнит последний вызов Release, когда объект более не нужен.


Оптимизации


Одно из преимуществ использования стандартного интерфейса для создания экземпляров (instantiation) состоит в том, что СОМ может обеспечить более эффективную технологию реализации. Рассмотрим следующий код, который создает новый экземпляр класса Chimp:

HRESULT CreateChimp(/* [out] */ IApe * &rpApe) { extern const CLSID CLSID_Chimp; rpApe = 0; IClassFactory *pcf = 0; HRESULT hr = CoGetClassObject(CLSID_Chimp, CLSCTX_ALL, 0, IID_IClassFactory, (void**)&pcf); if (SUCCEEDED(hr)) { hr = pcf->CreateInstance(0, IID_IApe, (void**)&rpApe); pcf->Release(); } return hr; }

Этот код выполняет одну операцию: создание объекта Chimp. Заметим, что для выполнения одной операции понадобилось три вызова подопераций (suboperations) — CoGetClassObject, CreateInstance, Release. Если сервер загружен как внутрипроцессный, то эти три подоперации не обойдутся слишком дорого. Если, однако, сервер является внепроцессным или внехостовым, то каждая из этих подопераций потребует вызова между процессами клиента и сервера. Хотя СОМ использует очень эффективную передачу IPC/RPC (Interprocess Communication/Remote Procedure Call), каждая из этих подопераций потребует немалых исполнительных затрат. В идеале было бы лучше потребовать, чтобы СОМ перешел на серверный процесс один раз и, находясь там, использовал объект класса для вызова CreateInstance от имени клиента. Если объект класса используется только для реализации IClassFactory, то этот способ будет более эффективным, чем трехшаговый способ, показанный ранее. В СОМ имеется API-функция: CoCreateInstanceEx, относящаяся по своему назначению к той же категории, что CoGetClassObject и IClassFactory::CreateInstance — обеспечивающая создание новых объектов за одно обращение между процессами.

CoCreateInstanceEx дает клиенту возможность задать CLSID для определения, какой объект следует реализовать. В случае успешного выполнения СоСгеаteInstanceEx возвращает один или более указателей интерфейса, которые ссылаются на новый экземпляр заданного класса.
При использовании CoCreateInstanceEx клиент никогда не видит промежуточный объект класса, который используется для создания экземпляра объекта. Чтобы клиенты могли вызывать CoCreateInstanceEx, никаких дополнительных функций серверу реализовывать не нужно. С точки зрения сервера все, что необходимо, — это объявить свои объекты классов, как требуется для CoGetClassObject. Реализация CoCreateInstanceEx для нахождения соответствующего объекта класса будет использовать ту же технологию, что и CoGetClassObject. Основное различие заключается в том, что после нахождения объекта CoCreateInstanceEx выполняет дополнительный вызов IClassFactory::CreateInstance, за которым могут последовать один или более вызовов QueryInterface, и все это во время выполнения процесса объекта класса. Можно получить значительную экономию на этапе выполнения, если запрос на активацию закрепить за определенным процессом.

Подобно CoGetClassObject, CoCreateInstanceEx позволяет клиенту задавать желаемые параметры CLSCTX и COSERVERINFO. Кроме того, CoCreateInstanceEx дает клиенту возможность запрашивать более одного указателя интерфейса на вновь создаваемый объект. Это делается путем предоставления клиенту возможности передавать массив структур MULTI_QI, который будет использован для вызова QueryInterface, новому экземпляру во время выполнения серверного процесса:

typedef struct tagMULTI_QI { // which interface is desired? // какой интерфейс требуется? const IID *piid; // null on input, will contain the pointer on output // на входе нуль, на выходе будет содержать указатель [iid_is(piid)] IUnknown *pItf; // will contain the HRESULT from QueryInterface on output // на выходе будет содержать HRESULT от QueryInterface HRESULT hr; } MULTI_QI;

Так как клиент может запрашивать более чем один указатель интерфейса на новый объект, он более не нуждается в явном вызове QueryInterface, если ему требуется более чем один тип указателей интерфейса. Поскольку эти вызовы QueryInterface будут сделаны СОМ от имени клиента по время выполнения внутри процесса объекта класса, более не потребуется никаких обменов данными между клиентом и объектом.


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

После постижения структуры MULTI_QI понять определение CoCreateInstanceEx уже нетрудно:

HRESULT CoCreateInstanceEx( [in] REFCLSID rclsid, // what kind of object? - какого сорта объект? [in] IUnknown *pUnkOuter,// for aggregation - для агрегирования [in] DWORD dwClsCtx, // locality? - размещение? [in] COSERVERINFO *pcsi, // host/security info - информация о хосте/безопасности [in] ULONG cmqi, // how many interfaces? - сколько интерфейсов? [out, size_is (cmqi)] MULTI_QI *prgmq // where to put itfs — куда разместить интерфейсы );

Если все запрошенные интерфейсы доступны в новом объекте, то CoCreateInstanceEx возвращает S_OK. Если хотя бы один (но не все) из запрошенных интерфейсов недоступен, то CoCreateInstanceEx возвратит CO_S_NOTALLINTERFACES, индицируя частичный успех. Затем вызывающий объект должен исследовать индивидуальные HRESULT в каждой структуре MULTI_QI, чтобы определить, какие интерфейсы были доступны, а какие — нет. Если CoCreateInstanceEx не может создать объект или ни один из запрошенных интерфейсов не доступен, то CoCreateInstanceEx возвращает HRESULT с кодом серьезности ошибки SEVERITY_ERROR, который сообщит, почему произошел отказ в операции.

CoCreateInstanceEx чрезвычайно эффективен в случае, когда требуются интерфейсы нескольких типов. Рассмотрим дополнительное определение интерфейса:

[object,uuid(753A8F7C-A7FF-11d0-8C30-0080C73925BA)] interface IEgghead : IUnknown { import "unknwn.idl"; HRESULT ContemplateNavel(void); }

Имея такое определение интерфейса, клиент может привязать оба типа указателей к новому шимпанзе за одно обращение:

void CreateChimpEatBananaAndThinkAboutIt(void) { // declare and initialize an array of MULTI_QI's // объявляем и инициализируем массив MULTI_QI MULTI_QI rgmqi[2] = { { &IID_IApe, 0, 0 }, { &IID_IEgghead, 0, 0 } }; HRESULT hr = CoCreateInstanceEx( CLSID_Chimp, // make a new chimp - создаем нового шимпанзе 0, // no aggregation - без агрегирования CLSCTX_ALL, // any locality - размещение любое 0, // no explicit host/security info // нет явной информации о хосте и безопасности 2, // asking for two interfaces - запрашиваем 2 интерфейса rgmqi); // array of MULTI_QI structs - массив структур MULTI_QI



if (SUCCEEDED(hr)) { // hr may be CO_S_NOTALLINTERFACES, so check each result // hresult может быть равен CO_S_NOTALLINTERFACES, // поэтому проверяем каждый результат if (hr == S_OK SUCCEEDED(rgmqi[0].hr)) { // it is safe to blindly cast the resultant ptr to the type // that corresponds to the IID used to request the interface // безопасно вслепую преобразовать результирующий // указатель к типу, соответствующему тому IID, // который использовался при запросе интерфейса IАре *рАре = reinterpret_cast<IApe*>(rgmqi[0].pItf); assert(pApe); HRESULT hr2 = pApe->EatBanana(); assert(SUCCEEDED(hr2)); pApe->Release(); } if(hr == S_OK SUCCEEDED(rgmqi[1].hr)) { IEgghead *peh = reinterpret_cast<IEgghead*>(rgmqi[1].pItf); assert(peh); HRESULT hr2 = peh->ContemplateNavel(); assert(SUCCEEDED(hr2)); peh->Release(); } } }



Рисунок 3.3 показывает шаги, которые предпринимаются CoCreateInstanceEx для создания нового объекта. Важно отметить, что оба результирующих указателя будут указывать на один и тот же объект. Если нужны два различных объекта, то требуются и два отдельных вызова CoCreateInstanceEx.

Использование СоСгеateInstanceЕх достаточно просто, если нужен только один интерфейс:

HRESULT CreateChimpAndEatBanana(void) { // declare and Initialize a MULTI_QI // определяем и инициализируем MULTI_QI MULTI_QI mqi = { &IID_IApe, 0, 0 }; HRESULT hr = CoCreateInstanceEx( CLSID_Chimp, // make a new chimp - создаем нового шимпанзе О, // по aggregation - без агрегирования CLSCTX_ALL, // any locality - любое расположение О, // no explicit host/security Info // нет явной информации о хосте/безопасности 1, // asking for one interface - запрашиваем один интерфейс &mqi); // array of MULTI_QI structs - массив структур MULTI_QI if (SUCCEEDED(hr)) { IApe *pApe = reinterpret_cast<IApe*>(mqi.pItf); assert(pApe); // use the new object - используем новый объект hr = pApe->EatBanana(); // release the Interface pointer // освобождаем указатель интерфейса pApe->Release(); } return hr; }



Если нужен только один интерфейс и не передается COSERVERINFO, то СОМ предусматривает несколько более удобную версию CoCreateInstanceEx, именуемую CoCreateInstance:

HRESULT CoCreateInstance( [in] REFCLSID rclsid, // what kind of object? - какой тип объекта? [in] IUnknown *pUnkOuter, // for aggregation - для агрегирования [in] DWORD dwClsCtx, // locality? - размещение? [in] REFIID riid, // what kind of interface - какой вид интерфейса [out, iid_is(riid)] void **ppv); // where to put itf — куда разместить интерфейс

Предыдущий пример становится намного проще, если применить CoCreateInstance

HRESULT CreateChimpAndEatBanana(void) { IАре *рАре = 0; HRESULT hr = CoCreateInstance( CLSID_Chimp, // make a new chimp — создаем нового шимпанзе О, // по aggregation - без агрегирования CLSCTX_ALL, // any locality — любое расположение IID_IApe, // what kind of itf - какой вид интерфейса (void**)&pApe); // where to put iff — куда поместить интерфейс

if (SUCCEEDED(hr)) { assert(pApe); // use the new object используем новый объект hr = pApe->EatBanana(); // release the interface pointer // освобождаем указатель интерфейса pApe->Release(); } return hr; }

Оба предыдущих примера функционально эквивалентны. В сущности, реализация CoCreateInstance просто осуществляет внутренний вызов CoCreateInstanceEx:

// pseudo-code for implementation of CoCreateInstance API // псевдокод для реализации API-функции CoCreateInstance HRESULT CoCreateInstance(REFCLSID rclsid, IUnknown *pUnkOuter, DWORD dwCtsCtx, REFIID riid, void **ppv) { MULTI_QI rgmqi[] = { &riid, 0, 0 }; HRESULT hr = CoCreateInstanceEx(rclsid, pUnkOuter, dwClsCtx, 0, 1, rgmqi); *ppv = rgmqi[0].pItf; return hr; }

Хотя возможно выполнить запрос на удаленную активацию с использованием CoCreateInstance, отсутствие параметра COSERVERINFO не позволяет вызывающему объекту задать явное имя хоста. Вместо этого вызов CoCreateInstance и задание только флага CLSCTX_REMOTE_SERVER предписывает SCM использовать конфигурационную информацию каждого CLSID для выбора хост-машины, которая будет использоваться для активации объекта.





Рисунок 3. 4 показывает, как параметры CoCreateInstanceEx преобразуются в параметры CoGetClassObject и IClassFactory::CreateInstance. Вопреки распространенному заблуждению, CoCreateInstanceEx не осуществляет внутренний вызов CoGetClassObject. Хотя между двумя этими методиками нет логических различий, реализация CoCreateInstanceEx будет более эффективной при создании одного экземпляра, так как в этом случае не будет лишних вызовов клиент-сервер, которые могли бы понадобиться, если бы была использована CoGetClassObject. Если, однако, будет создаваться большое число экземпляров, то клиент может кэшировать указатель объекта класса и просто вызвать IClassFactory::CreateInstance несколько раз. Поскольку IClassFactory::CreateInstance является всего лишь вызовом метода и не идет через SCM, он отчасти быстрее, чем вызов CoCreateInstanceEx. Порог, за которым становится более эффективным кэшировать объект класса и обходить CoCreateInstanceEx, будет изменяться в зависимости от эффективности IPC и RPC на используемых хост-машинах и сети.

1

Формально CoCreateInstance возникла первой. CoCreateInstanceEx была добавлена в Windows NT 4.0, когда стало ясно, что некоторые разработчики хотели бы передавать информацию о безопасности и хосте API-функциям активации модели СОМ. В исходном прототипе для CoGetClassObject третий параметр был резервным, и NT 4.0 смог заимствовать этот резервный параметр для COSERVERINFO. К сожалению, в CoCreateInstance не было неиспользуемых параметров, поэтому была создана CoCreateInstanceEx. Можно поспорить, была бы ли также полезной версия CoGetClassObject, использующая MULTI_QI для связывания с более чем одним интерфейсом, но увы — на момент написания книги никакой CoGetClassObjectEx не существует. Тот же аргумент мог бы быть применен и по отношению к IMoniker::BindToObject и MULTI_QI.


Снова интерфейс и реализация


В предыдущих примерах активации со стороны клиента осуществлялись явные вызовы API-функций СОМ для активации. Часто может понадобиться много шагов для корректной связи с требуемым объектом (например, создать один тип объекта, затем запросить его для ссылки на другой объект, основанный на некоторой информации в запросе). Чтобы избавить клиентов от заботы об алгоритмах по поиску объектов или их созданию, СОМ поддерживает стандартный механизм назначения произвольных имен объектам, на которые они ссылаются. Этот механизм основан на использовании локаторных объектов (locator objects), которые инкапсулируют свой алгоритм связи, скрывая его за стандартным постоянным интерфейсом. Эти локаторы объектов формально называются моникерами и являются просто СОМ-объектами, экспортирующими интерфейс IMoniker. Интерфейс IMoniker является одним из наиболее сложных интерфейсов СОМ; тем не менее, он объявляет один метод, чрезвычайно важный для данной дискуссии, а именно BindToObject:

interface IMoniker : IPersistStream { HRESULT BindToObject([in] IBindCtx *pbc, [in, unique] IMoniker *pmkToLeft, [in] REFIID riid, [out, iid_is(riid)] void **ppv); // remaining methods deleted for clarity // остальные методы удалены для ясности }

Напоминаем, что определения интерфейса являются абстрактными и достаточно неопределенными для того, чтобы допустить множество интерпретаций точной семантики каждого метода. Абстрактную семантику BindToObject можно сформулировать так: "запусти свой алгоритм поиска или создания объекта и возврати типизированный интерфейсный указатель на этот объект, когда он создан или найден". Когда клиент вызывает ВindToObject на моникер, у него нет точных представлений о том, как именно моникер превратит свою внутреннюю структуру в указатель на объект. Имея три различных моникера, можно использовать три совершенно различных алгоритма. Такая полиморфность поведения и делает идиому моникера столь действенной.

Клиенты получают моникеры одним из следующих способов. Клиент может получить моникер от какого-нибудь внешнего агента, такого, как результат вызова метода на некоем уже существующем объекте.
Клиенты могут вызвать явную API-функцию, которая создает моникер определенного типа. Клиенты могут просто иметь строку текста, являющуюся "строкоподобным" состоянием моникера. Последний случай является наиболее интересным, так как он позволяет приложениям загружать и сохранять "имена объектов", используя внешние конфигурационные файлы или системный реестр, в текстовом виде (text-based). Если эта методика открыто документирована как часть конфигурационного состояния приложения, системные администраторы или опытные пользователи смогут переконфигурировать свое приложение, чтобы использовать альтернативную технологию для поиска объектов, которая могла быть или не быть предусмотрена разработчиком исходного приложения. Например, моникер, поддерживающий выравнивание нагрузки, может быть переконфигурирован для проведения иной стратегии выбора хост-машин простым изменением текстовой версии моникера, которая хранится в конфигурационном файле приложения.

Текстовое представление моникера формально называется отображаемым именем (display name). Интерфейс IMoniker объявляет метод GetDisplayName, который позволяет клиентам запрашивать моникер о его отображаемом имени. Более интересная задача — превратить произвольные отображаемые имена в моникеры. Эта задача довольно проблематичная, так как клиент не может просто сказать, какому виду моникера соответствует отображаемое имя. Такую работу выполняет MkParseDisplayName - вероятно, наиболее важная API-функция во всем СОМ.

MkParseDisplayName берет произвольное отображаемое имя и превращает его в моникер:

HRESULT MkParseDisplayName( [in] IBindCtx *pbc, // binding Info - информация о связывании [in, string] const OLECHAR *pwszName, // object name - имя объекта [out] ULONG *pcchEaten, // progress on error - сообщение об ошибке [out] IMoniker **ppmk); // the resultant moniker - результирующий моникер

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


Если префиксы совпадают, то создается новый моникер соответствующего типа и ему присваивается отображаемое имя для дальнейшего анализа. Поскольку моникеры имеют иерархическую структуру и могут быть композитными, то результирующий моникер в действительности может содержать два и более моникеров. Клиент может не заботиться об этой детали реализации. Клиент попросту использует для нахождения искомого объекта результирующий интерфейсный указатель IMoniker, который может указывать, а может не указывать на композитный моникер (composite moniker).

Напомним, что начальная точка входа в класс СОМ проходит через объект этого класса. Чтобы связаться с объектом класса, необходим моникер классового типа (Class Moniker). Это моникеры встроенного типа, предоставляемые моделью СОМ. Классовые моникеры поддерживают CLSID в качестве своего начального состояния и могут быть созданы либо с помощью явной API-функции СОМ CreateClassMoniker.

HRESULT CreateClassMoniker([in] REFCLSID rclsid, [out] IMoniker **ppmk);

либо путем передачи отображаемого имени от Class Moniker в MkParseDisplayName:

clsid:571F1680-CC83-11d0-8C48-0080C73925BA:

Отметим, что префикс "сlsid" является программным идентификатором ProgID для Class Moniker. Следующий код демонстрирует использование МkParseDisplayName для создания Class Moniker, который затем используется для связи с объектом класса Gorilla:

HRESULT GetGorillaClass(IApeClass * &rpgc) { rpgc = 0; // declare the CLSID for Gorilla as a display name // объявляем CLSID как отображаемое имя для Gorilla const OLECHAR pwsz[] = OLESTR("clsid:571F1680-CC83-11d0-8C48-0080C73925BA:"); // create a new binding context for parsing // and binding the moniker // создаем новый связующий контекст // для анализа и связывания моникера IBindCtx *pbc = 0; HRESULT hr = CreateBindCtx(0, &pbc); if (SUCCEEDED(hr)) { ULONG cchEaten; IMoniker *pmk = 0; // ask СОМ to convert the display name to a moniker object // запрашиваем СОМ преобразовать отображаемое имя // в объект моникера hr = MkParseDisplayName(pbc, pwsz, &cchEaten, &pmk); if (SUCCEEDED(hr)) { // ask the moniker to find or create the object that it // refers to // запрашиваем моникер найти или создать объект, // на который моникер ссылается hr = pmk->BindToObject(pbc, 0, IID_IApeClass, (void**)&rpgc); // we now have a pointer to the desired object, so release // the moniker and the binding context // теперь у нас есть указатель на желаемый объект, так что // освобождаем моникер и связующий контекст pmk->Release(); } pbc->Release(); } return hr; }



Связующий контекст, который передается одновременно в MkParseDisplayName и IMoniker::BindToObject, является просто вспомогательным объектом, который позволяет дополнительным параметрам передаваться алгоритмам синтаксического анализа и связывания моникера. В случае нашего простого примера все, что требуется, — это новый связующий контекст в роли заполнителя (placeholder), который запрашивается путем вызова API-функции СОМ CreateBindCtx.

В Windows NT 4.0 введена API-функция, упрощающая вызовы MkParseDisplayName и IMoniker::BindToObject:

HRESULT CoGetObject( [in, string] const OLECHAR *pszName, [in, unique] BIND_OPTS *pBindOptions, [in] REFIID riid, [out, iid_is(riid)] void **ppv);

Эта API-функция реализована следующим образом:

// pseudo-code from OLE32.DLL // псевдокод из OLE32.DLL HRESULT CoGetObject(const OLECHAR *pszName, BIND_OPTS *p0pt, REFIID riid, void **ppv) { // prepare for failure // подготовка на случай сбоя *ppv = 0; // create a bind context // создаем контекст связывания IBindCtx *pbc = 0; HRESULT hr = CreateBindCtx(0, &pbc); if (SUCCEEDED(hr)) { // set bind options if provided // устанавливаем опции связывания, если они требуются if (pOpt) hr = pbc->SetBindOptions(pOpt); if (SUCCEEDED(hr)) { // convert the display name into a moniker // преобразуем отображаемое имя в моникер ULONG cch; IMoniker *pmk = 0; hr = MkParseDisplayName(pbc, pszName, &cch, &pmk); if (SUCCEEDED(hr)) { // ask the moniker to bind to the named object // запрашиваем моникер связаться с именованным объектом hr = pmk->BindToObject(pbc, 0, riid, ppv); pmk->Release(); } } pbc->Release(); } return hr; }

При наличии этой функции создание новой гориллы сводится к простому нахождению объекта класса и вызову метода CreateInstance:

HRESULT CreateAGorillaAndEatBanana() { IClassFactory *pcf = 0; // declare the CLSID for Gorilla as a display name // объявляем CLSID как отображаемое имя для Gorilla const OLECHAR pwsz[] = OLESTR("clsid:571F1680-CC83-11d0-8C48-0080C73925BA:"); // find the class object via the gorilla's class moniker // находим объект класса через gorilla's class moniker HRESULT hr = CoGetObject(pwsz, 0, IID_IClassFactory, (void**)&pcf); if (SUCCEEDED(hr)) { IApe *pApe = 0; // use the class object to create a gorilla // используем объект класса для создания gorilla hr = pcf->CreateInstance(0, IID_IApe, (void**)&pApe); if (SUCCEEDED(hr)) { // tell the new gorilla to eat a banana // говорим новой горилле съесть банан hr = pApe->EatBanana(); pApe->Release(); } pcf->Release(); } return hr; }





Рисунок 3. 5 иллюстрирует, какие объекты создаются или находятся посредством каждой операции.

Visual Basic предоставляет функциональные возможности API-функции CoGetObject через встроенную функцию GetObject. Следующий код на Visual Basic также создает новую gorilla и предписывает ей есть бананы:

Sub CreateGorillaAndEatBanana() Dim gc as IApeClass Dim ape as IApe Dim sz as String sz = "clsid:571F1680-CC83-11d0-8C48-0080C73925BA:" ' get the class object for gorillas ' получаем объект класса для gorilla Set gc = GetObject(sz) ' ask Gorilla class object to create a new gorilla ' запрашиваем объект класса Gorilla создать новую gorilla Set ape = gc.CreateApe() ' ask gorilla to eat a banana ' просим gorilla есть бананы ape.EatBanana End Sub

Отметим, что версия этой функции на Visual Basic использует интерфейс IApeClass для обработки объекта. Это связано с тем, что Visual Basic не может использовать интерфейс IClassFactory из-за ограничений языка.

1

Хотя использование MkParseDisplayName будет несколько менее эффективным, оно обладает гораздо большей гибкостью. Как отмечалось ранее, отображаемое имя может быть прочитано из файла или даже из пользовательского интерфейса. Отличным примером такого приложения является Internet Explorer фирмы Microsoft, так как он позволяет пользователям набирать произвольные имена объектов (URL), которые превращаются в моникеры (с использованием расширенной API-функции MkParseDisplayNameEx).

2

Контексты связывания используются композитными моникерами для оптимизации операций синтаксического анализа и связывания. Кроме того, контексты связывания позволяют клиентам выставить флаги CLSCTX, а также COSERVERINFO, хотя текущая реализация Class Moniker проигнорирует оба эти атрибута. Вместо этого Class Moniker предполагает, что он будет скомпонован с тем моникером, который ссылается на реализацию интерфейса IClassActivator, допускающим намного большую гибкость.


Снова об интерфейсе и реализации


В предыдущей главе интерфейс СОМ был определен как абстрактный набор операций, выражающий некоторую функциональность, которую может экспортировать объект. Интерфейсы СОМ описаны на языке IDL (Interface Definition Language — язык определений интерфейса) и имеют логические имена, которые указывают на моделируемые ими функциональные возможности. Ниже приведено IDL-определение СОМ-интерфейса IApe:

[object, uuid(753A8A7C-A7FF-11d0-8C30-0080C73925BA)] interface IApe : Unknown { import "unknwn.idl"; HRESULT EatBanana(void); HRESULT SwingFromTree(void); [propget] HRESULT Weight([out, retval] long *plbs); }

Сопровождающая IApe документация должна специфицировать примерную семантику трех операций: EatBanana, SwingFromTree и Weight. Все объекты, раскрывающие IАре посредством QueryInterface, должны гарантировать, что их реализации этих методов придерживаются семантического контракта IАре. В то же время определения интерфейса почти всегда специально оставляют место для интерпретации разработчиком объекта. Это означает, что клиенты никогда не могут быть полностью уверены в точном поведении любого заданного метода, а только в том, что его поведение будет следовать схематическим правилам, описанным в документации к интерфейсу. Эта контролируемая степень неопределенности является фундаментальной характеристикой полиморфизма и одной из основ развития объектно-ориентированного программного обеспечения.

Рассмотрим только что приведенный интерфейс IАре. Вероятно (и даже возможно), что будет более одной реализации интерфейса IАре. Поскольку определение IАре является общим для всех реализаций, то предположения, которые могут сделать клиенты о поведении метода EatBanana, должны быть достаточно неопределенными, чтобы позволить каждой обезьяне — гориллам, шимпанзе и орангутангам (все они могут реализовывать интерфейс IАре), получить свои допустимые (но слегка различные) интерпретации данной операции. Без этой гибкости полиморфизм невозможен.

СОМ определенно трактует интерфейсы, реализации и классы как три различных понятия.
Интерфейсы являются абстрактными протоколами для связи с объектом. Реализации — это конкретные типы данных, поддерживающие один или несколько интерфейсов с помощью точных семантических интерпретаций каждой из абстрактных операций интерфейса. Классы — это именованные реализации, представляющие собой конкретные типы, которым можно приписывать значения, и формально называются СОМ-классами, или коклассами (coclasses).

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

Кроме того, что реализации могут быть именованы с помощью CLSID, СОМ поддерживает текстовые псевдонимы, так называемые программные идентификаторы (programmatic identifiers), иначе ProgID. Эти ProgID поступают в формате libraryname.classname.version и, в отличие от CLSID, являются уникальными только по соглашению. Клиенты могут преобразовывать ProgID в CLSID и обратно с помощью API-функций СОМ CLSIDFromProgID и ProgIDFromCLSID:

HRESULT CLSIDFromProgID([in, string] const OLECHAR *pwszProgID, [out] CLSID *pclsid); HRESULT ProgIDFromCLSID([in] REFCLSID rclsid, [out, string] OLECHAR **ppwszProgID);



Для преобразования ProgID в CLSID нужно просто вызвать CLSIDFromProgID:

HRESULT GetGorillaCLSID(CLSID& rclsid) { const OLECHAR wszProgID[] = OLESTR("Apes.Gorilla.1"); return CLSIDFromProgID(wszProgID, &rclsid); }

На этапе выполнения будет просматриваться база данных конфигураций СОМ для преобразования ProgID Apes.Gorilla.1 в CLSID, соответствующий классу реализации СОМ.

1

Хотя и мало смысла запрашивать "любую доступную реализацию" данного интерфейса, иногда имеет смысл произвести семантическое группирование реализаций, имеющих определенные общие черты высокого уровня, например, чтобы все они были животными или чтобы все они имели службу регистрации. Чтобы обеспечить обнаружение этого типа компонентов, СОМ поддерживает объявление такой систематики (taxonomy) посредством использования категорий компонентов (component categories). Поскольку часто это тот случай, когда все классы, принадлежащие к одной категории компонентов, будут реализовывать одно и то же множество интерфейсов, то такое условие, без сомнения, является достаточным для принадлежности к одной категории компонентов.


Время жизни сервера


В предыдущих разделах было показано, как СОМ автоматически загружает DLL с целью перенесения реализации объектов в адресное пространство клиентских программ. Однако пока не обсуждалось, как и когда эти DLL выгружаются. Вообще говоря, серверные DLL могут предотвращать преждевременную выгрузку, но именно клиент выбирает момент, когда DLL фактически перестают использоваться. Клиенты, желающие освободить неиспользуемые DLL, вызывают API-функцию СОМ CoFreeUnusedLibraries:

void CoFreeUnusedLibraries(void);

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

Функция DllCanUnloadNow, которую экспортирует DLL каждого сервера, должна соответствовать следующей сигнатуре:

HRESULT DllCanUnloadNow(void);

Если DLL желает быть освобожденной, то она возвращает S_OK. Если DLL хочет остаться загруженной, то она возвращает S_FALSE. Серверные DLL должны оставаться загруженными по крайней мере до тех пор, пока сохраняются интерфейсные указатели на ее объекты. Это означает, что в DLL должен быть счетчик всех существующих ссылок на объекты. Чтобы упростить реализацию этого, большинство DLL содержат одну переменную для счетчика блокировок (lock count) и используют две функции для автоматического инкрементирования и декрементирования этого счетчика:

LONG g_cLocks = 0; void LockModule(void) { InterlockedIncrement(&g_cLocks); } void UnlockModule(void) { InterlockedDecrement(&g_cLocks); }

При наличии этих подпрограмм реализация DllCanUnloadNow становится чрезвычайно простой:

STDAPI DllCanUnloadNow(void) { return g_cLocks == 0 ? S_OK : S_FALSE; }

Oстается только вызывать в подходящее время подпрограммы LockModule и UnlockModule.

Существуют две основные причины, которые должны оставлять DLL сервера загруженной: внешние ссылки на экземпляры объектов и объекты класса, а также невыполненные вызовы IClassFactory::LockServer.
Вполне очевидно, как добавить поддержку DllCanUnloadNow в экземпляры и объекты классов. Объекты, расположенные в динамически распределяемой области памяти (такие, как экземпляры классов) просто инкрементируют счетчик блокировок сервера при первом вызове AddRef:

STDMETHODIMP_(ULONG) Chimp::AddRef(void) { if (m_cRef == 0) LockModule(); return InterlockedIncrement(&m_cRef); } и декрементируют счетчик блокировок при заключительном вызове Release:

STDMETHODIMP_(ULONG) Chimp::Release (void) { LONG res = InterlockedDecrement(&m_cRef); if (res == 0) { delete this; UnlockModule(); } return res; }

Поскольку объекты, не размещенные в динамически распределяемой области памяти (такие, как объекты классов), не содержат счетчика ссылок, при каждом вызове AddRef и Release нужно инкрементировать или декрементировать счетчик блокировок:

STDMETHODIMP_(ULONG) ChimpClass::AddRef(void) { LockModule(); return 2; }

STDMETHODIMP_(ULONG) ChimpClass::Release (void) { UnlockModule(); return 1; }

Объекты классов, которые реализуют IClassFactory, должны устанавливать свои серверные счетчики блокировок на вызовы IClassFactory::LockServer:

STDMETHODIMP ChimpClass::LockServer(BOOL bLock) { if (bLock) LockModule(); else UnlockModule(); return S_OK; }

Как будет обсуждаться в главе 6, IClassFactory::LockServer создана в первую очередь для внепроцессных серверов, но она достаточно проста и для использования во внутрипроцессных серверах.

Следует заметить, что в протоколе CoFreeUnusedLibraries/DllCanUnloadNow неотъемлемо присутствует состояние гонки (race condition). Возможно, что один поток задач будет выполнять заключительное освобождение последнего экземпляра, экспортированного из DLL, в то время как второй поток будет выполнять подпрограмму CoFreeUnusedLibraries. В СОМ приняты все меры предосторожности, чтобы избежать этой ситуации. В частности, в реализацию СОМ под Windows NT 4.0 Service Pack 2 добавлена специальная возможность для борьбы с состоянием гонки. Версия Service Pack 2 библиотеки СОМ определяет, чтобы к DLL обращались из нескольких потоков, и вместо того, чтобы незамедлительно выгружать DLL изнутри CoFreeUnusedLibraries, СОМ ставит DLL в очередь DLL, подлежащих освобождению.Затем СОМ будет ждать неопределенное время, пока не разрешит этим неиспользуемым серверным DLL освободиться посредством последующего вызова CoFreeUnusedLibraries, подтверждающего, что никаких остаточных вызовов Release уже не исполняется. Это означает, что в многопоточных средах выгрузка DLL из своего клиента может осуществляться значительно дольше, чем можно ожидать.

1

Вероятно, в Windows NT 5.0 будет предусмотрена дополнительная поддержка для подтверждения того, что DLL освобождаются быстро и безошибочно. Подробности можно найти в документации SDK.


Объекты


class object { public: template <class T> virtual T * dynamic_cast(const type_info& t = typeid(T) ) };

Аноним, 1995

В главе 2 обсуждались основы интерфейсов СОМ вообще и интерфейс IUnknown в частности. Было дано понятие о том, что путем наследования дополнительным интерфейсам объекты могут выставлять более одного вида функциональных возможностей. Был также продемонстрирован механизм, с помощью которого клиенты могут опрашивать объекты, чтобы найти среди них доступные функциональные возможности. Этот механизм — QueryInterface (Интерфейс запросов) — был выделен как версия С++-оператора преобразования типа dynamic_cast, не зависящая от языка программирования и от компилятора.

В предыдущей главе было показано, что QueryInterface можно реализовать непосредственно, используя статические преобразования типа для того, чтобы ограничить область действия указателя this на объект типом интерфейса, который запрашивается клиентом. На физическом уровне этот способ означает просто преобразование идентификаторов интерфейса в объект с помощью соответствующего смещения, то есть способ, который применяется любым компилятором C++ при реализации dynamic_cast.

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

Динамическая композиция


Если для реализации интерфейса в классе C++ используется множественное наследование или композиция, то в каждом объекте этого класса будут содержаться служебные данные (overhead) указателя vptr размером в четыре байта на каждый поддерживаемый интерфейс (принимая, что sizeof (void*) == 4). Если число интерфейсов, экспортируемых объектом, невелико, то эти служебные данные не играют важной роли, особенно в свете преимуществ, предоставляемых программной моделью СОМ. Если, однако, число поддерживаемых интерфейсов велико, то размер служебных данных vptr может вырасти до такой степени, что часть объекта, не связанная с СОМ, будет казаться маленькой по сравнению с ними. При использовании каждого из этих интерфейсов все время без служебных данных не обойтись. Если же, однако, эти интерфейсы не будут использоваться никогда или использоваться в течение короткого времени, то можно воспользоваться лазейкой в Спецификации СОМ и оптимизировать vptr некоторых неиспользуемых объектов.

Вспомним правило, гласящее, что все запросы QueryInterface на объект относительно IUnknown должны возвращать точно такое же значение указателя. Именно так в СОМ обеспечивается идентификация объектов. В то же время Спецификация СОМ определенно разрешает возвращать другие значения указателей в ответ на запросы QueryInterface относительно любых других типов интерфейсов, кроме IUnknown. Это означает, что для нечасто используемых интерфейсов объект может динамически выделять память для vptr по требованию, не заботясь о возврате того же самого динамически выделенного блока памяти каждый раз, когда запрашивается какой-либо интерфейс. Эта технология временного (transient) размещения композитов впервые была описана в "белой книге" Microsoft Поваренная книга для программистов СОМ (Microsoft white paper The СОМ Programmer's Cookbook), написанной Криспином Госвеллом (Crispin Goswell) (). В этой "белой книге" такие временные интерфейсы называются отделяемыми (tearoff).

Реализация отделяемого интерфейса подобна реализации интерфейса с использованием композиции.
Для отделяемого интерфейса должен быть определен второй класс, наследующий тому интерфейсу, который он будет реализовывать. Чтобы обеспечить идентификацию, QueryInterface отделяемого интерфейса должен делегировать управление функции QueryInterface основного класса. Два основных различия заключаются в том, что: 1) главный объект динамически размещает отделяемый интерфейс вместо того, чтобы иметь элемент данных экземпляра, и 2) отделяемый композит должен содержать явный обратный указатель на главный объект, так как технология фиксированного смещения, используемая в композиции, здесь не работает, поскольку отделяемый интерфейс изолирован от основного объекта. Следующий класс реализует IBoat как отделяемый интерфейс:

class CarBoat : public ICar { LONG m_cRef; CarBoat (void): m_cRef(0) {} public: // IUnknown methods // методы IUnknown STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); // IVehicle methods // методы IVehicle STDMETHODIMP GetMaxSpeed(long *pMax); // ICar methods // методы ICar STDMETHODIMP Brake(void); // define nested class that implements IBoat // определяем вложенный класс, реализующий IBoat struct XBoat : public IBoat { LONG m_cBoatRef; // back pointer to main object is explicit member // обратный указатель на главный объект - явный член CarBoat *m_pThis; inline CarBoat* This() { return m_pThis; } XBoat(CarBoat *pThis); ~XBoat(void); STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP GetMaxSpeed(long *pval); STDMETHODIMP Sink(void); }; // note: no data member of type Xboat // заметим: нет элементов данных типа Xboat };

Для QueryInterface главного объекта необходимо динамически разместить новый отделяемый интерфейс — каждый раз, когда запрашивается IBoat:

STDMETHODIMP CarBoat::QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IBoat) *ppv = static_cast<IBoat*>(new XBoat(this)); else if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this); : : :



Каждый раз при получении запроса на интерфейс IBoat размещается новый отделяемый интерфейс. Согласно стандартной практике QueryInterface вызова AddRef посредством результирующего указателя:

((IUnknown*)*ppv)->AddRef();

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

CarBoat::XBoat::XBoat(CarBoat *pThis) : m_cBoatRef(0), m_pThis(pThis) { m_pThis->AddRef(); }

CarBoat::XBoat::~XBoat(void) { m_pThis->Release(); }

Как и в случае с композицией, методу QueryInterface отделяемого интерфейса требуется идентифицировать объект, делегируя освобождение функции главного объекта. Однако отделяемый интерфейс может выявлять запросы на тот интерфейс (интерфейсы), который он сам реализует, и просто возвращать указатель, обработанный AddRef, себе самому:

STDMETHODIMP CarBoat::XBoat::QueryInterface(REFIID riid, void**ppv) { if (riid != IID_IBoat) return This()->QueryInterface(riid, ppv); *ppv = static_cast<IBoat*>(this); reinterpret_cast<IUnknown*>(*ppv)->AddRef(); return S_OK; }

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

STDMETHODIMP_(ULONG) CarBoat::XBoat::AddRef (void) { return InterlockedIncrement(&m_cRef); }

STDMETHODIMP_(ULONG) CarBoat::X8oat::Release(void) { ULONG res = InterlockedDecrement(&m_cBoatRef); if (res == 0) delete this; // dtor releases main object // деструктор освобождает главный объект return res; }

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


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

На первый взгляд, отделяемые интерфейсы кажутся лучшей из всех возможностей. Когда интерфейс не используется, то на его служебные данные отводится нуль байт объекта. Когда же интерфейс используется, объект косвенно тратит 4 байта на служебные данные отделяемого интерфейса. Подобное впечатление базируется на нескольких обманчивых предположениях. Во-первых, затраты на работающий отделяемый интерфейс составляют отнюдь не только 4 байта памяти для его vptr. Отделяемому интерфейсу требуются также обратный указатель и счетчик ссылок. Во-вторых, несмотря на возможность использования специального распределителя памяти (custom memory allocator), отделяемому интерфейсу потребуется по крайней мере 4 дополнительных байта на выравнивание и/или заполнение заголовков динамически выделенной памяти, используемых С-библиотекой для реализации malloc/operator new. Это означает, что объект действительно экономит 4 байта, когда интерфейс не используется. Но когда интерфейс используется, отделяемый интерфейс тратит как минимум 12 байт, если подключен специальный распределитель памяти, и 16 байт, если, по умолчанию, подключен оператор new. Если интерфейс запрашивается редко, то такая оптимизация имеет смысл, особенно если клиент освобождает этот интерфейс вскоре после получения. Если же клиент хранит отделяемый интерфейс в течение всего времени жизни объекта, то преимущества отделяемого интерфейса теряются.

К сожалению, дело с отделяемым интерфейсом обстоит еще хуже. Как видно из показанной ранее реализации, если объект получает два запроса QueryInterface на тот же самый отделяемый интерфейс, то будут созданы две копии этого отделяемого интерфейса, так как указатель на первый из них полностью забывается главным объектом, поскольку он был возвращен вызывающему объекту. Это означает, что в этом случае отделяемый интерфейс занимает по крайней мере от 24 до 32 байт, так как в памяти находятся оба vptr отделяемого интерфейса, по одному на каждый запрос QueryInterface.


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

Узнав обо всех подводных камнях отделяемых интерфейсов, задаешь себе логичный вопрос: "В каких же случаях отделяемые интерфейсы являются подходящими?" Не существует безусловного ответа; в то же время отделяемые интерфейсы очень хороши для поддержки большого числа взаимно исключающих интерфейсов. Рассмотрим случай, в котором в дополнение к трем транспортным интерфейсам, показанным ранее, имеются интерфейсы ITruck (грузовик), IMonsterТruck (грузовик-монстр), IMotorcycle (мотоцикл), IBicycle (велосипед), IUnicycle (уницикл), ISkateboard (скейтборд) и IHelicopter (вертолет), причем все они наследуют IVehicle. Если бы производящий транспортный класс хотел поддерживать любой из этих интерфейсов, но только по одному из них для каждого заданного экземпляра, то для осуществления этого отделяемые интерфейсы были бы прекрасным способом при условии, что главный объект кэшировал бы указатель на первый отделяемый интерфейс. Определение класса главного объекта выглядело бы примерно так:

class GenericVehicle : public IUnknown { LONG m_cRef; IVehicle *m_pTearOff; // cached ptr to tearoff // кэшированный указатель на отделяемый интерфейс GenericVehicle(void) : m_cRef(0), m_pTearOff(0) {} // IUnknown methods // методы IUnknown STDMETHODIMP QueryInterface(REFIID, void **); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release (void); // define tearoff classes // определяем классы отделяемых интерфейсов



class XTruck : public ITruck { ... }; class XMonsterTruck : public IMonsterTruck { ... }; class XBicycle : public IBicycle { ... }; : : : };

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

STDMETHODIMP GenericVehicle::QueryInterface(REFIID riid ,void **ppv) { if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this); else if (riid == IID_ITruck) { if (m_pTearOff == 0) // no tearoff yet, make one // отделяемого интерфейса еще нет, создаем один m_pTearOff = new XTruck(this); if (m_pTearOff) // tearoff exists, let tearoff QI // отделяемый интерфейс существует, пусть это QI return m_pTearOff->QueryInterface(riid, ppv); else // memory allocation failure // ошибка выделения памяти return (*ppv = 0), E_NOINTERFACE; } else if (riid == IID_IMonsterTruck) { if (in_pTearOff == 0) // no tearoff yet, make one // отделяемого интерфейса еще нет, создаем один m_pTearOff = new XMonsterTruck(this); if (m_pTearOff) // tearoff exists, let tearoff QI // отделяемый интерфейс существует, пусть это QI return m_pTearOff->QueryInterface(riid, ppv); else // memory allocation failure // ошибка выделения памяти return (*ppv = 0), E_NOINTERFACE; } else ... : : : }

На основе показанной здесь реализации QueryInterface на каждый объект будет приходиться по большей части по одному отделяемому интерфейсу. Это значит, что в случае отсутствия запросов на транспортные интерфейсы объект будет тратить в сумме 12 байт (vptr IUnknown + счетчик ссылок + кэшированный указатель на отделяемый интерфейс). Если транспортный интерфейс запрошен, то объект будет тратить в сумме от 24 до 28 байт (исходные 12 байт + наследующий Vehicle vptr + счетчик ссылок + обратный указатель на главный объект + (необязательно) служебная запись malloc (memory allocation - выделение памяти)).



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

class GenericVehicle : public ITruck, public IHelicopter, public IBoat, public ICar, public IMonsterTruck, public IBicycle, public IMotorcycle, public ICar, public IPlane, public ISkateboard { LONG m_cRef; // IUnknown methods - методы IUnknown : : : };

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

1

Служебные данные счетчика ссылок можно сократить, если разработчик желает ограничить клиентское использование AddRef. Это очень опасная оптимизация, возникшая благодаря растущей популярности интеллектуальных указателей, и результатом ее часто является наличие избыточных (но безвредных) пар AddRef/Release.


Двоичная композиция


Композиция и отделяемые интерфейсы — это две технологии на уровне исходного кода, предназначенные для реализации объектов СОМ на C++. Обе эти технологии требуют, чтобы разработчик объекта имел определения для каждого класса композита или отделяемого интерфейса в исходном коде C++, для возможности обработать подобъект, прежде чем возвратить его посредством QueryInterface. Для ряда ситуаций это очень разумно. В некоторых случаях, однако, было бы удобнее упаковать многократно используемую реализацию одного или большего числа интерфейсов в двоичный компонент, который мог бы обрабатываться через границы DLL, не нуждаясь в исходном коде подкомпонента. Это позволило бы более широкой аудитории повторно использовать подкомпонент, избегая слишком тесной связи с ним, как в случае повторного использования на уровне исходного кода (этот случай описан в главе 1). Однако если компонент повторного использования представляет собой двоичный композит или отделяемый интерфейс, то он должен участвовать в общей идентификации объекта.

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

class Car : public ICar { LONG m_cRef; Car(void) : m_cRef(0) {} STDMETHODIMP QueryInterface(REFIID, void **); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP GetMaxSpeed(long *pn); STDMETHODIMP Brake(void); };

STDMETHODIMP Car::QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this); else if (riid == IID_IVehicle) *ppv = static_cast<IVehicle*>(this); else if (riid == IID_ICar) *ppv = static_cast<ICar*>(this); else return (*ppv = 0), E_NOINTERFACE; ((IUnknown*)*ppv)->AddRef(); return S_OK; }

// car class object's IClassFactory::CreateInstance // интерфейс IClassFactory::CreateInstance // объекта класса car STDMETHODIMP CarClass::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv) { Car *pCar = new Car; if (*pCar) return (*ppv = 0), E_OUTOFMEMORY; pCar->AddRef(); HRESULT hr = pCar->QueryInterface(riid, ppv); pCar->Release(); return hr; }


Этот класс просто использует фактические реализации QueryInterface, AddRef и Release.

Рассмотрим второй класс C++, который пытается использовать реализацию Car как двоичный композит:

class CarBoat : public IBoat { LONG m_cRef; Unknown *m_pUnkCar; CarBoat(void); virtual ~CarBoat(void); STDMETHODIMP QueryInterface(REFIID, void **); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP GetMaxSpeed(long *pn); STDMETHODIMP Sink(void); };

Для эмуляции композиции разработчику пришлось бы создать подобъект Car, а деструктору — освободить указатель на подобъект:

CarBoat::CarBoat (void) : m_cRef(0) { HRESULT hr = CoCreateInstance(CLSID_Car, 0, CLSCTX_ALL, IID_IUnknown, (void**)&m_pUnkCar); assert(SUCCEEDED(hr)); }

CarBoat::~CarBoat(void) { if (m_pUnkCar) m_pUnkCar->Release(); }

Интересная проблема возникает в реализации QueryInterface:

STDMETHODIMP CarBoat::QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this); else if (riid == IID_IVehicle) *ppv = static_cast<IVehicle*>(this); else if (riid == IID_IBoat) *ppv = static_cast<IBoat*>(this); else if (riid == IID_ICar) // forward request... // переадресовываем запрос... return m_pUnkCar->QueryInterface(riid, ppv); else return (*ppv = 0), E_NOINTERFACE; ((IUnknown*)*ppv)->AddRef(); return S_OK; }

Поскольку объект Car не имеет понятия о том, что он является частью идентификационной единицы (identity) другого объекта, то он будет причиной неуспеха любых запросов QueryInterface для IBoat. Это означает, что

QI(IBoat)->ICar пройдет успешно, а запрос

QI(QI(IBoat)->ICar)->IBoat

потерпит неудачу, так как полученная QueryInterface будет несимметричной. Вдобавок запросы QueryInterface о IUnknown через интерфейсные указатели ICar и IBoat вернут различные значения, а это означает, что будет идентифицировано два различных объекта. Из подобных нарушений протокола IUnknown следует, что объекты CarBoat попросту не являются действительными объектами СОМ.



Идея составления объекта из двоичных композитов звучит красиво. Действительно, Спецификация СОМ четко и подробно указывает, как реализовать эту идею в стандартной и предсказуемой манере. Технология выставления клиенту двоичного подкомпонента непосредственно через QueryInterface называется СОМ-агрегированием. СОМ-агрегирование является лишь набором правил, определяющих отношения между внешним объектом (агрегирующим) и внутренним (агрегируемым). СОМ-агрегирование — это просто набор правил IUnknown, позволяющих более чем одному двоичному компоненту фигурировать в качестве идентификационной единицы (identity) СОМ.

Агрегирование СОМ несомненно является главной движущей силой для повторного использования в СОМ. Намного проще приписывать объекту значения и использовать его методы в реализации методов других объектов. Только в редких случаях кто-то захочет выставлять интерфейсы другого объекта непосредственно клиенту как часть той же самой идентификационной единицы. Рассмотрим следующий сценарий:

class Handlebar : public IHandlebar { ... }; class Wheel : public IWheel {};

class Bicycle : public IBicycle { IHandlebar * m_pHandlebar; IWheel *m_pFrontWheel; IWheel *m_pBackWheel; }

Было бы опрометчиво для класса Вicycle объявлять интерфейсы IHandlebar (велосипедный руль) или IWheel (колесо) в собственном методе QueryInterface. QueryInterface зарезервирован для выражения отношений "является" (is-a), а велосипед (bicycle) очевидно не является колесом (wheel) или рулем (handlebar). Если разработчик Bicycle хотел бы обеспечить прямой доступ к этим сторонам объекта, то интерфейс IBicycle должен был бы иметь для этой цели аксессоры определенных свойств:

[object, uuid(753A8A60-A7FF-11d0-8C30-0080C73925BA)] interface IBicycle : IVehicle { HRESULT GetHandlebar([out,retval] IHandlebar **pph); HRESULT GetWheels([out] IWheel **ppwFront, [out] IWheel **ppwBack); }

Реализация Bicycle могла бы тогда просто возвращать указатели на свои подобъекты:

STDMETHODIMP Bicycle::GetHandlebar(IHandlebar **pph) { if (*pph = m_pHandlebar) (*pph)->AddRef(); return S_OK; }



STDMETHODIMP Bicycle::GetWheels(IWheel **ppwFront, IWheel **ppwBack) { if (*ppwFront = m_pFrontWheel) (*ppwFront)->AddRef(); if (*ppwBack = m_pBackWheel) (*ppwBack)->AddRef(); return S_OK; }

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

Несмотря на этот пример, все же остаются сценарии, где желательно обеспечить реализацию интерфейса, которая могла бы быть внедрена в идентификационную единицу другого объекта. Чтобы осуществить это, в СОМ-агрегировании требуется, чтобы внутренний объект (агрегируемый) уведомлялся во время его создания, что он создается как часть идентификационной единицы другого объекта. Это означает, что создающая функция (creation function), обычно используемая для создания объекта, требует один дополнительный параметр: указатель IUnknown на идентификационную единицу, которой агрегирующий объект должен передать функции в ее методы QueryInterface, AddRef и Release. Покажем определение метода CreateInstance интерфейса IClassFactory:

HRESULT CreateInstance([in] Unknown *pUnkOuter, [in] REFIID riid, [out, iid_is(riid)] void **ppv);

Этот метод (и соответствующие API-функции CoCreateInstanceEx и CoCreateInstance) перегружен с целью поддержки создания автономных (stand-alone) объектов и агрегатов. Если вызывающий объект передает нулевой указатель и качестве первого параметра CreateInstance (pUnkOuter), то результирующий объект будет автономной идентификационной единицей самого себя. Если же вызывающий объект передает в качестве первого параметра ненулевой указатель, то результирующий объект будет агрегатом с идентификационной единицей, ссылка на которую содержится в pUnkOuter. В случае агрегации агрегат должен переадресовывать все запросы QueryInterface, AddRef и Release непосредственно и безусловно на pUnkOuter. Это необходимо для обеспечения идентификации объекта.



Имея прототип функции, приведенный выше, класс CarBoat после небольшой модификации будет удовлетворять правилам агрегации:

CarBoat::CarBoat(void) : m_cRef(0) { // need to pass identity of self to Create routine // to notify car object it 1s an aggregate // нужно передать свою идентификацию подпрограмме // Create для уведомления объекта car, что он - агрегат HRESULT hr = CoCreateInstance(CLSID_Car, this, CLSCTX_ALL, IID_IUnknown, (void**)&m_pUnkCar); assert(SUCCEEDED(hr)); }

Реализация CarBoat QueryInterface просто переадресовывает запрос ICar внутреннему агрегату:

STDMETHODIMP CarBoat::QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this); else if (riid == IID_ICar) // forward request... // переадресовываем запрос... return m_pUnkCar->QueryInterface(riid, ppv); else if (riid == IID_IBoat) : : :

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

В предыдущем сценарии метод CreateInstance класса Car возвращает внешнему объекту указатель интерфейса, наследующего IUnknown. Если бы этот интерфейсный указатель должен был просто делегировать вызовы функций интерфейсу IUnknown внешнего объекта, то невозможно было бы: 1) уведомить агрегат, что он больше не нужен; 2) запросить интерфейсные указатели при выделении их клиентам главного объекта. На деле результатом приведенной выше реализации QueryInterface будет бесконечный цикл, поскольку внешний объект делегирует функции внутреннему, который делегирует их обратно внешнему.

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


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

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

class Car : public ICar { LONG m_cRef; IUnknown *m_pUnk0uter; public: Car(IUnknown *pUnk0uter); // non-delegating IUnknown methods // неделегирующие методы IUnknown STDMETHODIMP InternalQueryInterface(REFIID, void **); STDMETHODIMP (ULONG) InternalAddRef(void); STDMETHODIMP_(ULONG) InternalRelease(void); // delegating IUnknown methods // делегирующие методы IUnknown STDMETHODIMP QueryInterface(REFIID, void **); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP GetMaxSpeed(*long *pn); STDMETHODIMP Brake(void); // composite to map distinguished IUnknown vptr to // non-delegating InternalXXX routines 1n main object // композит для преобразования определенного vptr IUnknown // в неделегирующие подпрограммы InternalXXX в главном // объекте

class XNDUnknown : public IUnknown { Car* This() { return (Car*)((BYTE*)this - offsetof(Car, m_innerUnknown));} STDMETHODIMP QueryInterface(REFIID r, void**p) { return This()->InternalQueryInterface(r,p); } STDMETHODIMP_(ULONG) AddRef(void) { return This()->InternalAddRef(); } STDMETHODIMP_(ULONG) Release(void) { return This()->InternalRelease(); } };

XNDUnknown m_innerUnknown; // composite instance // экземпляр композита };

Двоичное размещение этого объекта показано на рис. 4.8. Методы делегирования класса чрезвычайно просты:

STDMETHODIMP Car::QueryInterface(REFIID riid, void **ppv) { return m_pUnkOuter->QueryInterface(riid, ppv); }

STDMETHODIMP_(ULONG) Car::AddRef(void) { return m_pUnkOuter->AddRef(); }



STDMETHODIMP_(ULONG) Car::Release (void) { return m_pUnkOuter->Release(); }



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

Для того чтобы объект можно было использовать в обоих сценариях — агрегирования и автономном — разработчик объекта должен установить свой элемент данных m_pUnkOuter так, чтобы в случае автономного режима он указывал на собственный неделегирующий IUnknown:

Car::Car(IUnknown *pUnkOuter) { if (pUnkOuter) // delegate to pUnkOuter // делегируем в pUnkOuter m_pUnkOuter = pUnkOuter; else // delegate to non-delegating self // делегируем неделегирующему себе m_pUnkOuter = &m_innerUnknown; }

Разработчик обеспечивает то, что в обоих случаях m_pUnkOuter указывает на нужную для данного объекта реализацию QueryInterface, AddRef и Release.

Обычные неделегирующие реализации QueryInterface, AddRef и Release являются вполне правильными и предсказуемыми:

STDMETHODIMP Car::InternalQueryInterface(REFIID riid, void **ppv) { if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(&m_innerUnknown); else if (riid = IID_IVehicle) *ppv = static_cast<IVehicle*>(this); else if (riid == IID_ICar) *ppv = static_cast<ICar*>(this); else return (*ppv = 0), E_NOINTERFACE; ((IUnknown*)*ppv)->AddRef(); return S_OK; }

STDMETHODIMP_(ULONG) Car::InternalAddRef(void) { return InterlockedIncrement(&m_cRef); }

STDMETHODIMP_(ULONG) Car::InternalRelease(void) { ULONG res = InterlockedDecrement(&m_cRef); if (res == 0) delete this; return res; }

Единственной отличительной особенностью этих трех методов (кроме их имен) является то, что InternalQueryInterface при запросе IUnknown возвращает указатель на неделегирующую Unknown. Это просто требование Спецификации СОМ, которого следует придерживаться.

И наконец, подпрограмму создания Car требуется модифицировать для поддержки агрегирования:



STDMETHODIMP CarClass::CreateInstance(IUnknown *punk0uter, REFIID riid, void **ppv) { // verify that aggregator only requests IUnknown as // initial interface // проверяем, что агрегатор только запрашивает IUnknown как // начальный интерфейс if (pUnkOuter != 0 && riid != IID_IUnknown) return (*ppv = 0), E_INVALIDARG; // create new object/aggregate // создаем новый объект или агрегат Car *р = new Car(pUnkOuter); if (!p) return (*ppv = 0), E_OUTOFMEMORY; // return resultant pointer // возвращаем результирующий указатель p->InternalAddRef(); HRESULT hr = p->InternalQueryInterface(riid, ppv); p->InternalRelease(); return hr; }

Отметим, что здесь используются неделегирующие версии QueryInterface, AddRef и Release. Если создается автономная идентификационная единица, то это, конечно, допустимо. Если же создается агрегат, то необходимо убедиться, что AddRef обработал внутренний, а не внешний объект. Отметим также, что внешний объект в качестве начального интерфейса должен запросить IUnknown. Все это регламентировано Спецификацией СОМ. Если бы внешний объект мог запрашивать произвольный начальный интерфейс, то внутреннему объекту пришлось бы хранить два дублирующих набора указателей vptr: один набор делегировал бы свои реализации QueryInterface, AddRef и Release, а другой — нет. При допущении в качестве начального интерфейса одного IUnknown разработчик объекта может выделить только один vptr, который будет действовать как неделегирующий IUnknown.

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


Внутренний объект хранит указатель на IUnknown управляющего внешнего объекта с неподсчитанными ссылками. Формально эти соотношения зафиксированы в специальной формулировке правил СОМ для счетчиков ссылок. Вообще-то методику использования указателей без подсчета ссылок применять нельзя, поскольку ее невозможно реализовать в случае удаленного доступа к объектам. Более эффективный способ избежать зацикливания счетчика ссылок состоит в том, чтобы ввести промежуточные идентификационные единицы (identities) объектов, счетчики ссылок которых не повлияют на время жизни никакого объекта.

Еще одна проблема при программировании агрегирования может возникнуть, когда необходимо связать между собой внутренний и внешний объекты. Для того чтобы организовать связь внутреннего объекта с внешним, нужно вызвать QueryInterface посредством управляющего IUnknown. Однако этот запрос QueryInterface вызовет AddRef через результирующий указатель, который имеет обыкновение без спросу обрабатывать внешний объект с помощью AddRef. Если бы внутренний объект хранил этот указатель в качестве элемента данных, то возник бы цикл, поскольку внутренний объект уже неявно обработал внешний объект с помощью AddRef. Это означает, что внутренний объект должен избрать одну из двух стратегий. Внутренний объект может получать и освобождать указатель по потребности, храня его ровно столько времени, сколько это необходимо:

STDMETHODIMP Inner::MethodX(void) { ITruck *pTruck = 0; // outer object will be AddRefed after this call... // после этого вызова внешний объект будет обработан // с помощью AddRef... HRESULT hr = m_pUnkOuter->QueryInterface(IID_ITruck, (void**)&pTruck); if (SUCCEEDED(hr)) { pTruck->ShiftGears(); pTruck->HaulDirt(); // release reference to outer object // освобождаем ссылку на внешний объект pTruck->Release(); } }

Второй способ заключается в том, чтобы получить указатель один раз во время инициализации и освободить соответствующий внешний объект немедленно после получения.



HRESULT Inner::Initialize(void) { // outer object will be AddRefed after this call... // после этого вызова внешний объект будет обработан // с помощью AddRef... HRESULT hr = m_pUnkOuter->QueryInterface(IID_ITruck, (void**)&m_pTruck); // release reference to outer object here and DO NOT // release it later in the object's destructor // освобождаем здесь ссылку на внешний объект и // НЕ ОСВОБОЖДАЕМ ее потом в деструкторе объекта if (SUCCEEDED(hr)) m_pTruck->Release(); }

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

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

Outer::Outer(void) { ++m_cRef; // protect against delete this // защищаем против удаления this CoCreateInstance(CLSID_Inner, this, CLSCTX_ALL, IID_IUnknown, (void**)&m_pUnkInner); --m_cRef; // allow delete this // позволяем удалить this }



Данная методика стабилизации предотвращает преждевременное разрушение, когда внутренний объект освобождает указатели, которые он, быть может, получил в свой код инициализации. Эта методика настолько общепринята, что большинство СОМ-оболочек программирования включают в себя явный метод перекрытия (overridable), который работает внутри области действия пары инкремент/декремент. В MFC (Microsoft Foundation Classes — библиотека базовых классов Microsoft) этот метод называется CreateAggregates, в ATL — FinalConstruct.

Поскольку показанные выше методики реализации агрегируемого объекта не требуют никаких дополнительных базовых классов, кроме классов C++, то альтернативная форма макроса IMPLEMENT_UNKNOWN может прозрачно реализовать раздвоенную реализацию IUnknown. Определение исходного класса:

class Car : public ICar { Car(void); IMPLEMENT_UNKNOWN(Car) BEGIN_INTERFACE_TABLE(Car) IMPLEMENTS_INTERFACE(ICar) IMPLEMENTS_INTERFACE(IVehicle) END_INTERFACE() // IVehicle methods // методы IVehicle STDMETHODIMP GetMaxSpeed(long *pn); // ICar methods // методы ICar STDMETHODIMP Brake(void); };

просто переводится в следующее:

class Car : public ICar { Car(void); //indicate that aggregation is required // показываем, что требуется агрегирование IMPLEMENT_AGGREGATABLE_UNKNOWN(Car) BEGIN_INTERFACE_TABLE(Car) IMPLEMENTS_INTERFACE(ICar) IMPLEMENTS_INTERFACE(IVehicle) END_INTERFACE() // IVehicle methods // методы IVehicle STDMETHODIMP GetMaxSpeed(long *pn); // ICar methods // методы ICar STDMETHODIMP Brake(void); };

Встроенное расширение макроса IMPLEMENT_AGGREGATABLE_UNKNOWN включено в код, приложенный к этой книге.

1

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


Единственность и идентификация


Предыдущий раздел был посвящен запросам QueryInterface, которые представляют собой ответы типа "да/нет" вызывающим объектам. QueryInterface действительно возвращает S_OK (да) или E_NOINTERFACE (нет). Впрочем, когда QueryInterface возвращает S_OK, то он также возвращает объекту интерфейсный указатель. Для СОМ значение этого указателя чрезвычайно важно, так как оно позволяет клиентам определить, действительно ли на один и тот же объект указывают два интерфейсных указателя.



Где мы находимся?


В данной главе обсуждались законы идентификации в СОМ. В этих законах определено, что означает быть объектом СОМ. Законы идентификации СОМ предоставляют разработчику объекта потрясающую гибкость при разделении реализации объекта. В качестве технологии для освобождения подсчета ссылок для каждого интерфейса была представлена композиция. Для сокращения размножения vptr, а также для более эффективного управления состоянием объекта были описаны отделяемые интерфейсы. Затем было показано агрегирование в качестве способа создания одной идентификационной единицы (identity) из двух или более двоичных компонентов. Каждая из этих технологий позволяет более чем одному объекту выступать в качестве одной идентификационной единицы СОМ. Каждая технология имеет свои преимущества, и использование любой из них или всех вместе полностью скрыто от клиентов объекта.



Множественные интерфейсы и имена методов


Множественное наследование является очень эффективной и простой технологией для реализации интерфейсов СОМ в классе C++. Это требует написания очень короткого явного кода, так как большая часть работы компилятора и компоновшика заключается в построении соответствующих СОМ указателей vptr и таблиц vtbl. Если имя метода появляется более чем в одном базовом классе с идентичными типами параметров, то компилятор и компоновщик заполняют каждый элемент vtbl таким образом, чтобы он указывал на одну реализацию метода в классе. Этот режим применяется к таким методам, как QueryInterface, AddRef и Release, так как все интерфейсы СОМ начинаются с этих методов, и все же разработчику класса требуется написать каждый метод только один раз (и это хорошо). Этот же режим применяется и к методам любых интерфейсов, где происходит повтор имени и сигнатуры. Здесь есть одна возможная ловушка множественного наследования.

Иерархия транспортных интерфейсов из этой главы содержит конфликт имен. В интерфейсе ICar (автомобиль) имеется метод, названный GetMaxSpeed (развить максимальную скорость). В интерфейсах IBoat (лодка) и IPlane (самолет) также имеются методы, именуемые GetMaxSpeed с идентичной сигнатурой. Это означает, что при использовании множественного наследования разработчик класса пишет метод GetMaxSpeed один раз, а компилятор и компоновщик инициализируют таблицы vtbl, совместимые с ICar, IBoat и IPlane так, чтобы они указывали только на эту реализацию.

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

struct IXCar : public ICar { // add new non-clashing method as pure virtual // добавляем новый неконфликтный метод как чисто виртуальный virtual HRESULT STDMETHODCALLTYPE GetMaxCarSpeed(long *pval) = 0; // implement clashing method by upcalling // non-clashing implementation in derived class // реализуем конфликтный метод путем вызова // неконфликтной реализации в производном классе STDMETHODIMP GetMaxSpeed(long *pval) { return GetMaxCarSpeed(pval); } };


Допуская, что интерфейсы IBoat и IPlane подвергнуты подобной операции, можно реализовывать различные версии GetMaxSpeed простым наследованием от расширенных версий интерфейсов и переопределением неконфликтных версий каждого метода GetMaxSpeed:

class CarBoatPlane : public IXCar, public IXBoat, public IXPlane { public: // Unknown methods - методы IUnknown STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void);

// IVehicle methods - методы IVehicle // do not override GetMaxSpeed! // не подменяем GetMaxSpeed!

// ICar methods - методы ICar STDMETHODIMP Brake(void);

// IBoat methods - методы IBoat STDMETHODIMP Sink(void);

// IXPlane methods - методы IXPlane STDMETHODIMP TakeOff(void);

// upcalled from IXCar::GetMaxSpeed // вызвано из IXCar::GetMaxSpeed STDMETHODIMP GetMaxCarSpeed(long *pval);

// upcalled from IXBoat::GetMaxSpeed // вызвано из IXBoat::GetMaxSpeed STDMETHODIMP GetMaxBoatSpeed(long *pval);

// called from IXPlane::GetMaxSpeed // вызвано из IXPlane::GetMaxSpeed STDMETHODIMP GetMaxPlaneSpeed(long *pval); }

Рисунок 4.6 иллюстрирует представление этого класса и форматы таблиц vtbl. Отметим, что конфликтный метод GetMaxSpeed не реализован в этом классе. Поскольку каждый из базовых классов CarBoatPlane подменяет этот чисто виртуальный метод, то CarBoatPlane не нуждается в создании своей собственной реализации. Действительно, если бы в CarBoatPlane нужно было подменить GetMaxSpeed, то одна его реализация этого метода подменила бы версии, вызываемые из каждого базового класса, аннулировав результат использования IXCar, IXBoat и IXPlane. В силу этой проблемы данная технология годится только в тех ситуациях, когда можно быть уверенным, что класс реализации (или любые возможные производные классы) никогда не станет подменять конфликтный метод.



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


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

class CarPlane { LONG m_cRef; CarPlane(void) : m_cRef(0) {} public: // Main IUnknown methods // Главные методы IUnknown STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); private: // define nested class that implements ICar // определяем вложенный класс, реализующий ICar struct XCar : public ICar { // get back pointer to main object // получаем обратный указатель на главный объект inline CarPlane* This(); STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP GetMaxSpeed(long *pval); STDMETHODIMP Brake(void); };

// define nested class that implements IPlane // определяем вложенный класс, реализующий IPlane struct XPlane : public IPlane { // Get back pointer to main object // получаем обратный указатель на главный объект inline CarPlane* This(); STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP GetMaxSpeed(long *pval); STDMETHODIMP TakeOff(void); }; // declare instances of nested classes // объявляем экземпляры вложенных классов XCar m_xCar; XPlane m_xPlane; };



Использование вложенных классов не является обязательным, но оно подчеркивает, что эти подчиненные классы не имеют смысла вне контекста класса CarPlane. Рисунок 4.7 показывает двоичное размещение этого класса и размещения соответствующих vtbl.



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

STDMETHODIMP CarPlane::XCar::GetMaxSpeed(long *pn) { // set *pn to max speed for cars // устанавливаем *pn для максимальной скорости автомобилей }

STDMETHODIMP CarPlane::XPlane::GetMaxSpeed(long *pn) { // set *pn to max speed for planes // устанавливаем *pn для максимальной скорости самолетов }

Тот факт, что две реализации GetMaxSpeed встречаются в различных определениях вложенных классов, позволяет определить метод дважды и к тому же гарантирует то, что таблицы vtbl, соответствующие ICar и IPlane, будут иметь различные элементы для GetMaxSpeed.

Необходимо также отметить, что хотя класс CarPlane, находящийся на верхнем уровне, реализует методы IUnknown, он не наследует никакому производному от IUnknown классу. Вместо этого объекты CarPlane имеют элементы данных, которые наследуют интерфейсам СОМ. Это значит, что вместо того, чтобы использовать static_cast для вхождения в объект и нахождения определенного указателя vptr, реализация QueryInterface в CarPlane должна возвратить указатель на тот элемент данных, который реализует запрашиваемый интерфейс:

STDMETHODIMP CarPlane::QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(&m_xCar); else if (riid == IID_IVehicle) *ppv = static_cast<IVehicle*> (&m_xCar); else if (riid == IID_ICar) *ppv = static_cast<ICar*>(&m_xCar); else if (riid == IID_IPlane) *ppv = static_cast<IPlane*>(&m_xPlane); else return (*ppv = 0), E_NOINTERFACE;

((IUnknown*)(*ppv))->AddRef(); return S_OK; }



Для обеспечения идентификации объекта каждый из элементов данных CarPlane должен или воспроизвести этот код в своей собственной реализации QueryInterface, или просто передать управление главной функции QueryInterface в CarPlane. Чтобы осуществить это, необходим механизм перехода к главному объекту со стороны функции-члена составного элемента данных. Определение класса CarPlane::XCar содержит встроенную подпрограмму, которая использует фиксированные смещения для вычисления указателя this главного объекта от указателя this составного элемента данных.

inline CarPlane CarPlane::XCar::This(void) { return (CarPlane*)((char*)this // ptr to composite - указатель на композит - offsetof (CarPlane, m_xCar)); }

inline CarPlane CarPlane::XPlane::This(void) { return (CarPlane*)((char*)this // ptr to composite - указатель на композит - offsetof(CarPlane, m_xPlane)); }

Такая технология вычисления обратного указателя (back-pointer) компактна и чрезвычайно эффективна, так как не требует явных элементов данных для нахождения главного объекта внутри реализации метода элемента данных. При наличии таких алгоритмов вычисления обратного указателя реализация композитного QueryInterface становится тривиальной:

STDMETHODIMP CarPlane::XCar::QueryInterface(REFIID r, void**p) { return This()->QueryInterface(r, p); }

STDMETHODIMP CarPlane::XPlane::QueryInterface(REFIID r, void**p) { return This()->QueryInterface(r, p); }

Такая же передача this потребуется для AddRef и Release для получения обобщенного представления о времени жизни объекта в случае составных (композитных) элементов данных.

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


Например, MFC (Microsoft Foundation Classes — библиотека базовых классов Microsoft) использует эту технологию. Причиной применения композиции при реализации новых классов является получение отдельных реализации метода, определенного одинаково более чем в одном интерфейсе. К счастью, стандартные интерфейсы, определяемые СОМ, очень редко создают такие конфликты, а те немногие, которые создают, почти всегда преобразуются в семантически эквивалентные функции. Для разрешения коллизий, подобных тем, что произошли в сценарии с GetMaxSpeed, композиция, вероятно, и не требуется, так как в первом приближении для преобразования двойников в уникальные объекты достаточно использования промежуточных классов. Эта методика проста, эффективна и фактически не требует дополнительного кода. Основная причина использования композиции в новом коде заключается в том, что нужно обеспечить подсчет ссылок в каждом интерфейсе.

Иногда желательно разместить ресурсы в объекте на базе уже использующихся интерфейсов. В то же время из использования множественного наследования для реализации интерфейсов СОМ следует, что в каждой таблице vtbl будет использована только одна реализация AddRef и Release. Хотя можно выявить первый запрос на заданный интерфейс и разместить ресурсы по требованию:

STDMETHODIMP QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IBoat) { // allocate resource the first time through // размещаем ресурсы при первом проходе if (m_pTonsOfMemory == 0) m_pTonsOfMemory = new char[4096 * 4096]; *ppv = static_cast<IBoat*>(this); } else if ... }

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



class CarBoatPlane : public ICar, public IPlane { LONG m_cRef; char *m_pTonsOfMemory; CarBoatPlane (void) : m_cRef(0), m_pTonsOfMemory (0) {} public: // IUnknown methods - методы IUnknown STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); // IVehicle methods - методы IVehicle STDMETHODIMP GetMaxSpeed(long *pMax); // ICar methods - методы ICar STDMETHODIMP Brake(void); // IPlane methods - методы IPlane STDMETHODIMP TakeOff(void);

// define nested class that implements IBoat // определяем вложенный класс, реализующий IBoat struct XBoat : public IBoat { // get back pointer to main object // получаем обратный указатель на главный объект inline CarBoatPlane* This(); LONG m_cBoatRef; // per-interface ref count // счетчик ссылок на каждый интерфейс XBoat(void) : m_cBoatRef(0) {} STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP GetMaxSpeed(long *pval); STDMETHODIMP Sink(void); }; XBoat m_xBoat; };

Реализация AddRef и Release из IBoat могут теперь следить за числом ссылок типа IBoat и высвободить ресурсы, когда они больше не нужны:

STDMETHODIMP_(ULONG) CarBoatPlane::XBoat::AddRef() { ULONG res = InterlockedIncrement(&m_cBoatRef); if (res == 1) { // first AddRef - первый AddRef // allocate resource and forward AddRef to object // размещаем ресурсы и пересылаем AddRef на объект This()->m_pTonsOfMemory = new char[4096*4096]; This()->AddRef(); } return res; }

STDMETHODIMP_(ULONG) CarBoatPlane::XBoat::Release() { ULONG res = InterlockedDecrement(&m_cBoatRef); if (res == 0) { // last Release - последний Release // free resource and forward Release to object // освобождаем ресурсы и пересылаем Release на объект delete [] This()->m_pTonsOfMemory; This()->Release(); } return res; }

Чтобы эта методика работала, все пользующиеся интерфейсными указателями должны придерживаться требований спецификации СОМ: функция Release должна вызываться через указатель, посредством которого вызывается соответствующая функция AddRef.


Поэтому правильной концовкой QueryInterface будет следующая:

((IUnknown*)(*ppv))->AddRef(); // use exact ptr // используем точный указатель return S_OK;

вместо такого:

AddRef(); // just call this->AddRef // только вызов this->AddRef return S_OK;

Первый вариант гарантирует, что если клиент пишет следующий правильный код

IBoat *pBoat = 0; HRESULT hr = pUnk->QueryInterface(IID_IBoat, (void**)&pBoat); if (SUCCEEDED(hr)) { hr = pBoat->Sink(); pBoat->Release(); }

то для AddRef и для Release обязательно будет использовано одно и то же значение указателя.

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

class CarBoatPlane : public ICar, public IPlane { public: struct XBoat : public IBoat { // composite QI/AddRef/Release/This() // композит из QI/AddRef/Release/This() IMPLEMENT_COMPOSITE_UNKNOWN(CarBoatPlane, XBoat, m_xBoat) STDMETHODIMP GetMaxSpeed(long *pval); STDMETHODIMP Sink(void); };

XBoat m_xBoat;

// IVehicle methods // методы IVehicle STDMETHODIMP GetMaxSpeed(long *pMax);

// ICar methods // методы ICar STDMETHODIMP Brake(void);

// IPlane methods // методы IPlane STDMETHODIMP TakeOff(void);

// standard heap-based QI/AddRef/Release // стандартные расположенные в "куче" QI/AddRef/Release

IMPLEMENT_UNKNOWN(CarBoatPlane) BEGIN_INTERFACE_TABLE(CarBoatPlane) IMPLEMENTS_INTERFACE_AS(IVehicle, ICar) IMPLEMENTS_INTERFACE(ICar) IMPLEMENTS_INTERFACE(IPlane) // macro that calculates offset of data member // макрос, вычисляющий смещение элемента данных IMPLEMENTS_INTERFACE_WITH_COMPOSITE(IBoat, XBoat, m_xBoat) END_INTERFACE_TABLE() };

В приведенном выше определении класса опущены только определения методов объекта вне QueryInterfасе, AddRef и Release.


Два новых макроса, использованных в определении класса, определяются следующим образом:

// inttable.h // (book-specific header file) // (заголовочный файл, специфический для данной книги)

#define COMPOSITE_OFFSET(ClassName, BaseName, \ MemberType, MemberName) \ (DWORD(static_cast<BaseName*>(\ reinterpret_cast<MemberType*>(0x10000000 + \ offsetof(ClassName, MemberName)))) - 0х10000000)

#define IMPLEMENTS_INTERFACE_WITH_COMPOSITE(Req,\ MemberType, MemberName) \ { &IID_##Req,ENTRY_IS_OFFSET, COMPOSITE_OFFSET(_IT,\ Req, MemberType, MemberName) },

// impunk.h // (book-specific header file) // (заголовочный файл, специфический для данной книги)

#def1ne IMPLEMENT_COMPOSITE_UNKNOWN(OuterClassName,\ InnerClassName, DataMemberName) \ OuterClassName *This() \ { return (OuterClassName*)((char*)this - \ offsetof(OuterClassName, DataMemberName)); }\ STDMETHODIMP QueryInterface(REFIID riid, void **ppv)\ { return This()->QueryInterface(riid, ppv); }\

STDMETHODIMP_(ULONG) AddRef(void) \ { return This()->AddRef(); }\ STDMETHODIMP_(ULONG) Release(void) \ { return This()->Release(); }

Эти макросы препроцессора просто дублируют фактические реализации QueryInterface, AddRef и Release, использованные в композиции.


Объекты имеют статический тип


Один из выводов, который можно сделать из трех требований QueryInterfасе, состоит в том, что множество интерфейсов, поддерживаемых объектом, не может изменяться во времени. Спецификация СОМ четко требует, чтобы этот вывод был верен для всех объектов. Из этого требования следует, что иерархия типов объекта является статичной, несмотря на тот факт, что для определения множества поддерживаемых типов данных клиенты должны опрашивать объекты динамически. Если объект отвечает "да" на запрос интерфейса типа А, то он должен отвечать "да", начиная с этой точки. Если объект отвечает "нет" на запрос интерфейса типа А, то он должен отвечать "нет", начиная с этой точки. Фраза "начиная с этой точки" (from that point on) буквально переводится как "до тех пор, пока есть хотя бы один внешний указатель интерфейса на объект". Обычно это соответствует жизненному циклу базового объекта C++, но язык Спецификации СОМ обладает достаточной свободой, чтобы предоставить разработчикам определенную гибкость (например, иерархия типов глобальной переменной может изменяться, когда все указатели освобождены).

Из того, что все объекты СОМ имеют статическую иерархию типов, следует, что утверждение, записанное в следующем коде, никогда не должно быть ложным, несмотря на то, что идентификатор интерфейса используется в качестве второго параметра:

void AssertStaticType(IUnknown *pUnk, REFIID riid) { IUnknown *pUnk1 = 0, *pUnk2 = 0; HRESULT hr1 = pUnk->QueryInterface(riid, (void**)&pUnk1); HRESULT hr2 = pUnk->QueryInterface(riid, (void**)&pUnk2); // both requests for the same interface should // yield the same yes/no answer // оба запроса того же самого интерфейса // должны получить тот же самый ответ да/нет assert(SUCCEEDED(hr1) == SUCCEEDED(hr2)); if (SUCCEEDED(hr1)) pUnk1->Release(); if (SUCCEEDED(hr2)) pUnk2->Release(); }

Это требование означает, что в СОМ запрещены следующие программные технологии:

Использование временной информации при решении вопроса о том, удовлетворять или нет запрос QueryInterface (например, выдавать интерфейс IMorning (утро) только до 12:00).


Использование переменной информации о состоянии при решении вопроса о том, удовлетворять или нет запрос QueryInterface (например, выдавать интерфейс INotBusy (не занят), только если количество внешних интерфейсных указателей меньше десяти).

Использование маркера доступа (security token) вызывающего объекта для решения, удовлетворять или нет запрос QueryInterface. Как будет объяснено в главе 6, на самом деле это не обеспечивает никакой реальной безопасности из-за протокола передачи (wire protocol), используемого СОМ.

Использование успешного захвата динамических ресурсов для решения вопроса о том, удовлетворять или нет запрос QueryInterface (например, выдавать интерфейс IHaveTonsOfMemory (у меня тонны памяти) только при успешном выполнении malloc(4096*4096)).

Эта последняя методика может быть до некоторой степени смягчена, если разработчик объекта желает поупражняться с выражением спецификации СОМ "barring catastrophic failure" (за исключением катастрофического сбоя).

Эти ограничения не означают, что два объекта одного и того же класса реализации не могут давать различные ответы "да/нет" при запросе одного и того же интерфейса. Например, класс может реализовать показанные ранее интерфейсы ICar, IBoat и IPlane, но может разрешить только одному интерфейсу быть использованным в каком-то определенном объекте. Эти ограничения также не означают, что объект не может использовать постоянную или временную информацию для решения вопроса о том, дать ли исходное "да" или "нет" для данного интерфейса. В примере для класса, который разрешает только один из трех интерфейсов, следующая идиома была бы вполне допустимой:

class СВР : public ICar, public IPlane, public IBoat { enum TYPE { CAR, BOAT, PLANE, NONE }; TYPE m_type; CBP(void) : m_type(NONE) { } STDMETHODIMP QueryInterface(REFIID riid, void **ppv) { if (md == IID_ICar) { // 1st QI Initializes type of object // первая QI инициализирует тип объекта if (m_type == NONE) m_type = CAR; // only satisfy request if this object is a car // удовлетворяем запрос, только если данный объект // является car (автомобилем) if (m_type == CAR) *ppv = static_cast<ICar*>(this); else return (*ppv = 0), E_NOINTERFACE; } else if (md == IID_IBoat) { // similar treatment for IBoat and IPlane // IBoat и IPlane обрабатываются сходным образом } };

Из требования, чтобы множество поддерживаемых интерфейсов было статичным, следует простой вывод, что разработчикам объектов не разрешается создавать конструкции, состоящие из одного объекта, который дает два различных ответа "да/нет" на запрос определенного интерфейса. Одна из причин того, что иерархия типов объекта должна оставаться неизменной на всем протяжении своего жизненного цикла, состоит в том, что СОМ не гарантирует отправления всех клиентских запросов QueryInterface такому объекту в случае, когда к нему имеется удаленный доступ. Неизменность иерархии типов позволяет "заместителям" на стороне клиента (client-side proxies) кэшировать результаты QueryInterface во избежание чрезмерных обменов клиент-объект. Такая оптимизация очень важна для эффективности СОМ, но она разрушает конструкции, использующие QueryInterface для передачи динамической семантической информации вызывающему объекту.


QueryInterface и IUnknown


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

void AssertSameObject(IUnknown *pUnk) { IUnknown *pUnk1 = 0, *pUnk2 = 0; HRESULT hr1 = pUnk->QueryInterface(IID_IUnknown, (void **)&pUnk1); HRESULT hr2 = pUnk->QueryInterface(IID_IUnknown, (void **)&pUnk2); // QueryInterface(IUnknown) must always succeed // QueryInterface(IUnknown) должно всегда быть успешным assert(SUCCEEDED(hr1) && SUCCEEDED(hr2)); // two requests for IUnknown must always yield the // same pointer values // два запроса на IUnknown должны всегда выдавать // те же самые значения указателя assert(pUnk1 == pUnk2); pUnk1->Release() ; pUnk2->Release() ; }

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

bool IsSameObject(IUnknown *pUnk1, IUnknown *pUnk2) { assert(pUnk1 && pUnk2); bool bResult = true; if (pUnk1 != pUnk2) { HRESULT hr1, hr2; IUnknown *p1 = 0, *p2 = 0; hr1 = pUnk1->QueryInterface(IID_IUnknown, (void **)&p1); assert(SUCCEEDED(hr1)); hr2 = pUnk2->QueryInterface(IID_IUnknown, (void **)&p2); assert(SUCCEEDED(hr2)); // compare the two pointer values, as these // represent the identity of the object // сравниваем значения двух указателей, // так как они идентифицируют объект bResult = (р1 == р2); p1->Release(); p2->Release(); } return bResult; }

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


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

class CarBoatPlane : public ICar, public IBoat, public IPlane { public: // IUnknown methods - методы IUnknown STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); // IVehicle methods - методы IVehicle STDMETHODIMP GetMaxSpeed(long *pMax); // ICar methods - методы ICar STDMETHODIMP Brake(void); // IBoat methods - методы IBoat STDMETHODIMP Sink(void); // IPlahe methods - методы IPlane STDMETHODIMP TakeOff(void); };

Ниже приведена стандартная реализация QueryInterface в CarBoatPlane:

STDMETHODIMP QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IUnknown) *ppv = static_cast<ICar*>(this); else if (riid == IID_IVehicle) *ppv = static_cast<ICar*>(this); else if (riid == IID_ICar) *ppv = static_cast<ICar*>(this); else if (riid == IID_IBoat) *ppv = static_cast<IBoat*>(this); else if (riid == IID_IPlane) *ppv = static_cast<IPlane*>(this); else return (*ppv = 0), E_NOINTERFACE; ((IUnknown*)*ppv)->AddRef(); return S_OK; }

Для того чтобы быть объектом СОМ, реализация CarBoatPlane QueryInterface должна полностью придерживаться правил IUnknown, приведенных в данной главе.

Класс CarBoatPlane выставляет интерфейсы только типа ICar, IPlane, IBoat, IVehicle и IUnknown. Каждая таблица vtbl CarBoatPlane будет ссылаться на единственную реализацию QueryInterface, показанную выше. К каждому поддерживаемому интерфейсу можно обращаться через эту реализацию QueryInterface, так что невозможно найти два несимметричных интерфейса, то есть не существует двух интерфейсов A и B, для которых неверно следующее:

If QI(A)->B Then QI(QI(A)->B)->A

Если следовать той же логике, то поскольку все пять интерфейсов принадлежат к одной и той же реализации QueryInterface, не существует трех интерфейсов А, В и С, для которых неверно следующее:



If QI(QI(A)->B)->C Then QI(A)->C

Наконец, поскольку реализация QueryInterface всегда удовлетворяет запросы на пять возможных интерфейсных указателей, которые могут поддерживаться клиентом, то следующее утверждение должно быть верным для каждого из пяти поддерживаемых интерфейсов:

QI(A)->A

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

Реализация также корректно выполняет правило СОМ об идентификации, возвращая только одно значение указателя при запросе IUnknown:

if (riid == IID_IUnknown) *ppv = static_cast<ICar*>(this);

Если бы реализация QueryInterface возвращала различные указатели vptr для каждого запроса:

if (riid == IID_IUnknown) { int n = rand() % 3; if (n == 0) *ppv = static_cast<ICar*>(this); else if (n == 1) *ppv = static_cast<IBoat*>(this); else if (n == 2) *ppv = static_cast<IPlane*>(this); }

то реализация была бы корректной только в терминах чисто С++-отношений типа (то есть все три интерфейса были бы совместимы по типу с запрошенным типом IUnknown). Эта реализация, однако, не является допустимой с точки зрения СОМ, поскольку правило идентификации для QueryInterface было нарушено.


QueryInterface рефлективна


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

QI(A)->A

всегда должен быть верным.

Это требование проиллюстрировано рис. 4.4 и в следующем фрагменте кода:

void AssertReflexive(ICar *pCar) { if (pCar) { ICar *pCar2 = 0; // request same type of interface // запрос интерфейса того же типа HRESULT hr = pCar->QueryInterface(IID_ICar, (void**)&pCar2); // if the following assertion fails, pCar // did not point to a valid СОМ object // если следующее утверждение неверно, то pCar // не указывает на корректный объект СОМ assert(SUCCEEDED(hr)); pCar2->Release(); } }

Из этого кода следует, что все реализации ICar должны быть способны удовлетворить дополнительные запросы QueryInterface для ICar через интерфейсный указатель ICar. Если бы это не соблюдалось, то было бы невозможно передавать жестко типизированные интерфейсы через параметры базового типа без невосполнимой потери исходного типа:

extern void GetCar(ICar **ppcar); extern void UseVehicle(IVehicle *pv);

ICar *pCar; GetCar(&pCar); UseVehicle(pCar); // ICar-ness is syntactically lost // ICar-ность синтаксически потеряна

void UseVehicle(IVehicle *pv) { ICar *pCar = 0; // try to regain syntactic ICar-ness // пытаемся восстановить синтаксическую ICar-ность HRESULT hr = pv->QueryInterface(IID_ICar, (void**)&pCar); }

Поскольку указатель, использованный в функции UseVehicle, имеет то же самое значение, что и указатель ICar, переданный вызывающим объектом, то выглядело бы неестественным (counterintuitive), если бы этот тип не мог быть восстановлен внутри функции.

Из того, что QueryInterface является симметричным, рефлексивным и транзитивным, следует, что любой интерфейсный указатель на объект должен выдавать тот же самый ответ "да/нет" на данный запрос QueryInterface. Это позволяет клиентам рассматривать иерархию типов объекта как простой граф, все вершины которого непосредственно соединены друг с другом (и с самими собой) с помощью открытых (explicit) ребер. На рис. 4.5 изображен такой граф. Отметим, что в любую вершину графа можно попасть из любой другой вершины, пройдя вдоль только одного ребра.



QueryInterface симметрична


Спецификация СОМ требует, чтобы, если запрос QueryInterface на интерфейс B

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

QI(A)->B

то также должно быть верным

QI(QI(A)->B)->A

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

void AssertSymmetric(ICar *pCar) { if (pCar) { IPlane *pPlane = 0; // request a second type of interface // запрашиваем второй тип интерфейса HRESULT hr = pCar->QueryInterface(IID_IPlane, (void**)&pPlane); if (SUCCEEDED(hr)) { ICar *pCar2 = 0; // request original type of interface // запрашиваем исходный тип интерфейса hr = pPlane->QueryInterface(IID_ICar, (void**)&pCar2); // if the following assertion fails, pCar // did not point to a valid СОМ object // если следующее утверждение не будет правильным, // то pCar не укажет на правильный СОМ-объект assert(SUCCEEDED(hr)); pCar2->Release(); } pPlane->Release(); } }

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



QueryInterface транзитивна


Спецификация СОМ требует также, чтобы, если запрос QueryInterface на интерфейс В удовлетворяется через интерфейсный указатель типа A, а второй запрос QueryInterface на интерфейс C удовлетворяется через указатель типа В, то запрос QueryInterface на интерфейс C через исходный указатель типа A был бы также успешным. Это означает, что если верно

QI(QI(A)->B)->C

то должно быть верным и

QI(A)->C

Это условие иллюстрируется рис. 4.3 и означает, что утверждение, приведенное в нижеследующем коде, должно всегда быть верным:

void AssertTransitive(ICar *pCar) { if (pCar) { IPlane *pPlane = 0; // request intermediate type of interface // запрос промежуточного типа интерфейса HRESULT hr = pCar->QueryInterface(IID_IPlane, (void**)&pPlane); if (SUCCEEDED(hr)) { IBoat *pBoat1 = 0; // request terminal type of interface // запрос конечного типа интерфейса hr = pPlane->QueryInterface(IID_IBoat, (void**)&pBoat1); if (SUCCEEDED(hr)) { IBoat *pBoat2 = 0; // request terminal type through the original pointer // запрос конечного типа через исходный указатель hr = pCar->QueryInterface(IID_IBoat, (void**)&pBoat2); // if the following assertion fails, pCar // did not point to a valid СОМ object // если следующее утверждение неверно, то pCar // не указывал на корректный СОМ-объект assert(SUCCEEDED(hr)); pBoat2->Release(); } pBoat1->Release(); } pPlane->Release(); } }

Из транзитивности QueryInterface следует, что все интерфейсы, которые выставляет объект, равноправны и не требуют, чтобы их вызывали в какой-то определенной последовательности. Если бы это было не так, то клиентам пришлось бы заботиться о том, какой указатель на объект использовать для различных запросов QueryInterface. Из транзитивности и симметричности QueryInterface следует, что любой интерфейсный указатель на объект выдаст тот же самый ответ "да/нет" на любой запрос QueryInterface. Единственная ситуация, не охватываемая транзитивностью и симметричностью, это повторные запросы одного и того же интерфейса. Эта ситуация требует, чтобы QueryInterface был и рефлективным.



Снова IUnknown


IUnknown не имеет реализации по умолчанию, которая являлась бы частью интерфейса системного вызова СОМ. Заголовочные файлы SDK не содержат базовых классов, макросов или шаблонов, предусматривающих реализации QueryInterface, AddRef и Release, которые должны использоваться во всех программах на С или C++. Вместо этого Спецификация СОМ (Component Object Model Specification) предоставляет очень точные правила относительно допущений, которые клиенты и объекты могут делать относительно этих трех методов. Этот набор правил формирует протокол IUnknown и позволяет каждому разработчику объекта преобразовать три указанных метода IUnknown во все, что имеет смысл для его или ее объекта.

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

Правила IUnknown в совокупности определяют, что значит быть объектом СОМ. Чтобы понять правила IUnknown, полезно начать с конкретного примера. Рассмотрим следующую иерархию интерфейсов:

import "unknwn.idl"; [object, uuid(CD538340-A56D-11d0-8C2F-0080C73925BA)] interface IVehicle : IUnknown { HRESULT GetMaxSpeed([out, retval] long *pMax); }

[object, uuid(CD53834l-A56D-11d0-8C2F-0080C73925BA)] interface ICar : IVehicle { HRESULT Brake(void); }

[object, uuid(CD538342-A56D-11d0-8C2F-0080C73925BA)] interface IPlane : IVehicle { HRESULT TakeOff(void); }

[object, uuid(CD538343-A56D-11d0-8C2F-0080C73925BA)] interface IBoat : IVehicle { HRESULT Sink(void); }


СОМ использует стандартную технологию для визуального представления объектов. Эта технология находится в рамках принципа СОМ отделения интерфейса от реализации и не раскрывает никаких деталей реализации объекта, кроме списка выставляемых им интерфейсов. Эта технология также визуально усиливает многие из правил IUnknown. Рисунок 4.1 показывает стандартное представление класса CarBoatPlane, который реализует все только что определенные интерфейсы. Заметим, что единственный вывод, который можно сделать из этого рисунка, таков: если не произойдет катастрофического сбоя, объекты CarBoatPlane будут выставлять пять интерфейсов: IBoat, IPlane, ICar, IVehicle и IUnknown.

Первое правило IUnknown, подлежащее исследованию, — это требование, чтобы QueryInterface был симметричным, транзитивным и рефлексивным (Symmetric/Transitive/Reflexive). Эти требования определяют отношения между всеми интерфейсными указателями объекта и начинают определять понятие идентификации (identity) объектов СОМ. Подобно всем правилам IUnknown, эти требования должны исполняться всегда, за исключением катастрофических сбоев, теми, кто хочет считаться действительным объектом СОМ.




Включение


Не все классы способны к агрегированию. Для того чтобы выставить неагрегируемые классы как часть индивидуальности другого объекта, необходимо, чтобы внешние объекты явно передавали вызовы методов внутренним объектам. Эта технология СОМ часто называется включением (containment).

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