Сущность технологии СОМ. Библиотека программиста

Бокс Дональд

Глава 3. Классы

 

 

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

 

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

В предыдущей главе интерфейс СОМ был определен как абстрактный набор операций, выражающий некоторую функциональность, которую может экспортировать объект. Интерфейсы СОМ описаны на языке 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, соответствующий классу реализации СОМ.

 

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

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

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

class GorillaClass : public IApe

{

public:

// class objects are singletons, so don't delete

// объекты класса существуют в единственном экземпляре,

// так что не удаляйте их

IMPLEMENTUNKNOWNNODELETE (GorillaClass)

BEGININTERFACETABLE(GorillaClass)

IMPLEMENTSINTERFACE(IApe)

ENDINTERFACETABLE()

// IApe methods

// методы IApe

STDMETHODIMP EatBanana(void);

STDMETHODIMP SwingFromTree(void);

STDMETHODIMP getWeight(long *plbs);

};

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

class Gorilla : public IApe

{

public:

// Instances are heap-based, so delete when done

// копии размещены в куче, поэтому удаляем после выполнения

IMPLEMENTUNKNOWN()

BEGININTERFACETABLE()

IMPLEMENTSINTERFACE(IApe)

ENDINTERFACETABLE()

// IApe methods

// методы IApe

STDMETHODIMP EatBanana(void);

STDMETHODIMP SwingFromTree(void);

STDMETHODIMP getWeight(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: IMPLEMENTUNKNOWNNODELETE(GorillaClass)

BEGININTERFACETABLE(GorillaClass)

IMPLEMENTSINTERFACE(IApeClass)

ENDINTERFACETABLE()

STDMETHODIMP CreateApe(Ape **ppApe)

{

if ((*ppApe = new Gorilla) == 0) return EOUTOFMEMORY;

(*ppApe)->AddRef();

return SOK;

}

STDMETHODIMP GetApe(long nApeID, IApe **ppApe)

{

// assume that a table of well-known gorillas is

// being maintained somewhere else

// допустим, что таблица для известных горилл

// поддерживается где-нибудь еще

extern Gorilla *grgWellKnownGorillas[];

extern int gnMaxGorillas;

// assert that nApeID is a valid index

// объявляем, что nApeID – допустимый индекс

*ррАре = 0;

if (nApeID > gnMaxGorillas || nApeID < 0) return EINVALIDARG;

// assume that the ID is simply the index into the table

// допустим, что ID – просто индекс в таблице

if ((*ppApe = grgWellKnownGorillas[nApeID]) == 0) return EINVALIDARG;

(*ppApe)->AddRef();

return SOK;

}

STDMETHODIMP getAverageWeight(long *plbs)

{

extern *grgWellKnownGorillas[];

extern int gnMaxGorillas;

*plbs = 0;

long lbs;

for (int i = 0; i < gnMaxGorillas; i++)

{

grgWellKnownGorillas[i]->getWeight(&lbs);

*plbs += lbs;

}

// assumes gnMaxGorillas is non-zero

// предполагается, что gnMaxGorillas ненулевой

*plbs /= gnMaxGorillas;

return SOK;

}

};

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

 

Активация

Клиентам требуется механизм для поиска объектов класса. В силу динамической природы СОМ это может привести к загрузке библиотеки 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 внепроцессная активация будет рассматриваться подробно.

 

Использование 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, iidis(riid)] void **ppv);

// put it here!

// поместим его здесь!

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

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

enum tagCLSCTX { CLSCTXINPROCSERVER = 0х1,

// run -inprocess

// запуск в процесс

CLSCTXINPROCHANDLER = 0х2,

// see note

// смотрите сноску

CLSCTXLOCALSERVER = 0х4,

// run out-of-process

// запуск вне процесса

CLSCTXREMOTESERVER = 0х10

// run off-host

// запуск вне хост-машины

} CLSCTX;

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

#define CLSCTXINPROC (CLSCTXINPROCSERVER |

\ CLSCTXINPROCHANDLER)

#define CLSCTXSERVER (CLSCTXINPROCSERVER |

\ CLSCTXLOCALSERVER |

\ CLSCTXREMOTESERVER)

#define CLSCTXALL (CLSCTXINPROCSERVER |

\ CLSCTXINPROCHANDLER |

\ CLSCTXLOCALSERVER |

\ CLSCTXREMOTESERVER)

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

Третий параметр 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), а использует только флаг CLSCTXREMOTESERVER, то для определения того, какая машина будет активировать объект, СОМ использует информацию о конфигурации каждого CLSID. Если клиент передает явное имя хоста, то оно получит приоритет перед любыми ранее сконфигурированными именами хостов, о которых может знать СОМ. Если клиент не желает передавать явную информацию о безопасности или имя хоста в CoGetClassObject, можно применить нулевой указатель COSERVERINFO. 

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

HRESULT GetGorillaClass(IApeClass * &rpgc)

{

// declare the CLSID for Gorilla as a GUID

// определяем CLSID для Gorilla как GUID

const CLSID CLSIDGorilla =

{ 0x571F1680, 0xCC83, 0x11d0,

{ 0x8C, 0х48, 0х00, 0х80, 0xС7, 0х39, 0x25, 0xBA

} };

// call CoGetClassObject directly

// вызываем прямо CoGetClassObject

return CoGetClassObject(CLSIDGorilla, CLSCTXALL, 0, IIDIApeClass, (void**)&rpgc);

}

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

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

HRESULT FindAGorillaAndEatBanana(long nGorillaID)

{

IApeClass *pgc = 0;

// find the class object via CoGetClassObject

// находим объект класса с помощью CoGetClassObject

HRESULT hr = CoGetClassObject(CLSIDGorilla, CLSCTXALL, 0, IIDIApeClass, (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(CLSIDGorilla, CLSCTXALL, 0, IIDIApeClass, (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, эти примеры идентичны. Поскольку объекты класса могут экспортировать сколь угодно сложные интерфейсы, то их можно использовать для моделирования довольно изощренной стратегии активации, инициализации и размещения объектов.

 

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

СОМ-сервер – это двоичный файл, содержащий код метода для одного или более СОМ-классов. Сервер может быть упакован или в динамически подключаемую библиотеку (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 большая часть ее конфигурационной информации записывается под именем

HKEYLOCALMACHINE\Software\Classes

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

HKEYCLASSESROOT

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

HKEYCURRENTUSER\Software\Classes 

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

СОМ хранит информацию, относящуюся к 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 *gRegTable[][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 = SOK;

// look up server's file name

// ищем имя файла сервера

char szFileName[MAXPATH];

GetModuleFileNameA(ghinstDll, szFileName, MAXPATH);

// register entries from table

// регистрируем записи из таблицы

int nEntries = sizeof(gRegTable)/sizeof(*gRegTable);

for (int i = 0; SUCCEEDED(hr) && i < nEntries; i++)

{

const char *pszKeyName = gRegTable[i][0];

const char *pszValueName = gRegTable[i][1];

const char *pszvalue = gRegTable[i][2];

// map rogue value to module file name

// переводим нестандарное значение в имя файла модуля

if (pszValue == (const char*)-1) pszValue = szFileName;

HKEY hkey;

// create the key

// создаем ключ

long err = RegCreateKeyA(HKEYCLASSESROOT, pszKeyName, &hkey);

if (err == ERRORSUCCESS)

{

// set the value

// присваиваем значение

err = RegSetValueExA(hkey, pszVvalueName, 0, REGSZ, (const BYTE*) pszValue, (strlen(pszValue) + 1));

RegCloseKey(hkey);

}

if (err != ERRORSUCCESS)

{

// if cannot add key or value, back out and fail

// если невозможно добавить ключ или значение, то откат и сбой

DllUnregisterServer();

hr = SELFREGECLASS;

}

}

return hr;

}

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

STDAPI DllUnregisterServer(void)

{

HRESULT hr = SOK;

int nEntries = sizeof(gRegTable)/sizeof(*gRegTable);

for (int i = nEntries – 1; i >= 0; i-)

{

const char *pszKeyName = gRegTable[i][0];

long err = RegDeleteKeyA(HKEYCLASSESROOT, pszKeyName);

if (err != ERRORSUCCESS) hr = SFALSE; }

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, iidis(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 sgorillaClass;

static OrangutanClass sorangutanClass;

static ChimpClass schimpClass;

// return interface pointers to known classes

// возвращаем указатели интерфейсов известных классов

if (rclsid == CLSIDGorilla) return sgorillaClass.QueryInterface(riid, ppv);

else if (rclsid == CLSIDOrangutan)

return sorangutanClass.QueryInterface(riid, ppv);

else if (rclsid == CLSIDChimp) return schimpClass.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 CLASSECLASSNOTAVAILABLE;

}

Заметим, что приведенный код не заботится о том, какой интерфейс объявляется каждым из объектов класса. Он просто отправляет запрос 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 = REGDBECLASSNOTREG;

*ppv = 0; if (dwClsCtx & CLSCTXINPROC)

{

// 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[MAXPATH];

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 COEDLLNOTFOUND;

pfnGCO = GetProcAddress(hInst, «DllGetClassObject»);

if (pfnGCO == 0) return COEERRORINDLL;

// 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 & (CLSCTXLOCALSERVER | CLSCTXREMOTESERVER)) && hr == REGDBECLASSNOTREG)

{

// handle out-of-proc/remote request

// обрабатываем внепроцессный/удаленный запрос

}

return hr;

}

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

// from APELIB.DEF

// из APELIB.DEF LIBRARY

APELIB EXPORTS DllGetClassObject private

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

 

Обобщения

В предыдущем примере интерфейс 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, iidis(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(CLSIDGorilla, CLSCTXALL, 0, IIDIOleItemContainer, (void**)&poic);

if (SUCCEEDED(hr))

{

// ask Gorilla class object for Ursus

// запрашиваем объект класса Gorilla на поиск

Ursus hr = poic->GetObject(OLESTR(«Ursus»), BINDSPEEDINDEFINITE, 0, IIDIApe, (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, iidis(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(CLSIDGorilla, CLSCTXALL, 0, IIDIClassFactory, (void **)&pcf);

if (SUCCEEDED(hr))

{

IApe *pApe = 0;

// use the class object to create a gorilla

// используем объект класса для создания gorilla

hr = pcf->CreateInstance(0, IIDIApe, (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:

IMPLEMENTUNKNOWNNODELETE(GorillaClass)

BEGININTERFACETABLE(GorillaClass)

IMPLEMENTSINTERFACE(IClassFactory)

ENDINTERFACETABLE()

STDMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv)

{

*ppv = 0;

if (pUnkOuter != 0)

// we don't support aggregation yet

// мы еще не поддерживаем агрегирование

return CLASSENOAGGREGATION;

// create a new instance of our C++ class Gorilla

// создаем новый экземпляр нашего С++-класса Gorilla

Gorilla *p = new Gorilla;

if (p == 0) return EOUTOFMEMORY:

// 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(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(rgmqi[1].pItf);

assert(peh);

HRESULT hr2 = peh->ContemplateNavel();

assert(SUCCEEDED(hr2));

peh->Release();

}

} }

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

Использование СоСrе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 – создаем нового шимпанзе

0,

// по aggregation – без агрегирования CLSCTX_ALL,

// any locality – любое расположение

0,

// 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(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 – создаем нового шимпанзе

0,

// по 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 на используемых хост-машинах и сети.

 

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

В предыдущих примерах активации со стороны клиента осуществлялись явные вызовы 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 из-за ограничений языка.

 

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

Моникеры часто составляются из других моникеров, чтобы с помощью текстового описания пути можно было осуществлять навигацию по иерархиям объектов. Чтобы обеспечить простую поддержку этого типа управления, в СОМ предусмотрена стандартная реализация моникеров, которая, будучи поставленной справа от другого моникера, запрашивает объект связать ссылку с другим объектом в иерархии. Такой моникер называется моникером элемента (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, позволяет клиенту задавать сколь угодно сложную стратегию активации, не меняя ни единой строки кода.

 

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

В предыдущих разделах было показано, как СОМ автоматически загружает 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 из своего клиента может осуществляться значительно дольше, чем можно ожидать.

 

Классы и 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.

 

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

Часто случается, что разработчики классов желают развернуть новые версии уже существующих классов, чтобы исправить дефекты или расширить функциональные возможности. Полезно придать этим новым реализациям новые идентификаторы класса 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. Необходимо также отметить, что в момент написания этой книги эмуляция классов не работает должным образом для удаленных запросов на активацию.

 

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

Как подчеркивалось в этой главе, основные примитивы активации СОМ требуют, чтобы вызывающей программе при создании новых экземпляров класса было известно его точное имя. Иногда, однако, бывает полезно просто потребовать, чтобы подходящим являлся любой класс, удовлетворяющий некоторым семантическим ограничениям. Кроме того, прежде чем сделать запрос на активацию, было бы полезно знать, какие сервисные средства класс требует от своих клиентов. В этом случае не будут создаваться объекты, которые клиент не готов должным образом поддерживать. Эти проблемы послужили причиной для создания категорий компонентов (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.

 

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

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