Апартаменты
STDMETHODIMP CMyClass::MethodX(void) { EnterCr1t1calSect1on(&m_cs); if (TryToPerformX() == false) return E_UNEXPECTED: LeaveCriticalSect1on(&m_cs); return S_OK; } Аноним, 1996
В предыдущей главе обсуждались основы идентификации в СОМ и было формально определено, что именно отличает объекты СОМ от объектов памяти с произвольной организацией. Были представлены правила IUnknown и способы использования этих правил для придания разработчику объектов максимальной гибкости. В данной главе уточняется понятие идентификации в СОМ с учетом базисных элементов (примитивов) операционной системы (например, потоков, процессов), а также распределенного доступа. Этот альянс базисных элементов системы и распределения формируют основу архитектуры удаленного доступа СОМ.
Архитектура стандартного маршалинга
Как уже упоминалось ранее в этой главе, СОМ использует протокол ORPC для всех обращений между апартаментами. Это обстоятельство может представлять интерес с точки зрения архитектуры, но некоторые разработчики желают программировать коммуникационный код низкого уровня. Для того чтобы воспользоваться ORPC-коммуникациями, объектам СОМ не требуется делать ничего, кроме реализации IUnknown, для осуществления межапартаментных обращений по протоколу ORPC. По умолчанию при первом вызове CoMarshalInterface для объекта этот объект опрашивается, желает ли он управлять своими собственными межапартаментными связями. Этот вопрос приходит в форме запроса QueryInterface об интерфейсе IMarshal. Большинство объектов не реализуют интерфейс IMarshal и дают отказ на этот запрос со стороны QueryInterface, показывая тем самым, что их вполне удовлетворяет управление всеми связями самой СОМ посредством вызовов ORPC. Те объекты, которые реализуют интерфейс IMarshal, показывают этим, что ORPC им не подходит и что разработчик объекта предпочитает управлять всеми межапартаментными связями посредством специальных заместителей. Когда объект реализует интерфейс IMarshal, то все ссылки на этот объект будут подвергнуты специальному маршалингу. Специальный маршалинг будет обсуждаться позже в данной главе. Если же объект не реализует интерфейс IMarshal, то все ссылки на этот объект будут маршалированы стандартно. Большинство объектов предпочитают использовать стандартный маршалинг, и поэтому ему уделено основное внимание в данном разделе.
Когда CoMarshalInterface впервые определяет, что объект желает использовать стандартный маршалинг, то создается специальный СОМ-объект под названием администратор загушек (stub manager). Это программный модуль, управляющий всеми интерфейсными заглушками активного объекта.
Администратор заглушек действует как идентификационная единица объекта во всей сети и единственным образом идентифицируется Идентификатором Объектов (Object Identifier — OID), который является идентификатором этого объекта во всех апартаментах.
Между администраторами заглушек и идентификационными единицами СОМ- объектов имеется взаимно однозначное соответствие. Каждый администратор заглушек ссылается на ровно один СОМ-объект. Каждый СОМ-объект, использующий стандартный маршалинг, будет иметь ровно один администратор заглушек. Администратор заглушек содержит но крайней мере одну неосвобожденную ссылку на объект, которая удерживает ресурсы объекта в памяти. В этом смысле администратор заглушек является еще одним внутрипроцессным клиентом для объекта. Администратор заглушек следит за числом неосвобожденных внешних ссылок и будет существовать до тех пор, пока где-либо в сети останется хотя бы одна неосвобожденная ссылка. Большинство внешних ссылок являются просто заместителями, хотя промежуточные маршалированные объектные ссылки могут удерживать заглушки, чтобы быть уверенными, что в момент создания первого заместителя объект еще существует. Когда неосвобожденные заместители или ссылки уничтожаются, администратор заглушек извещается об этом и декрементирует свой счетчик внешних ссылок. Если уничтожена последняя внешняя ссылка на администратор заглушек, то последний самоуничтожается, освобождая свои неосвобожденные ссылки на действующий объект. Это имитирует эффект наличия на стороне клиента ссылок, поддерживающих объект. Методы явного контроля за временем жизни заглушки будут обсуждаться далее в этой главе.
Администратор заглушек действует лишь как сетевой идентификатор объекта и не понимает, как обрабатывать поступающие ORPC-запросы, предназначенные для объекта. Для того чтобы преобразовать поступающие ORPC-запросы в действительные вызовы методов объекта, администратору заглушек нужен вспомогательный объект, который знает детали сигнатур интерфейсных методов. Этот вспомогательный объект называется интерфейсной заглушкой (interface stub). Он должен правильно демаршалировать параметры [in], которые присутствуют в блоке ORPC-запроса, вызвать метод в действующий объект и затем маршалировать HRESULT и любые параметры [out] в ответный блок ORPC.
Интерфейсные заглушки идентифицируются внутри апартамента с помощью Идентификаторов Интерфейсных Указателей (Interface Pointer Identifiers - IPIDs), которые внутри апартамента являются уникальными. Подобно администратору заглушек, каждая интерфейсная заглушка содержит ссылку на объект. Однако поддерживаемый интерфейс будет интерфейсом определенного типа, а не просто IUnknown. На рис. 5.3 показана взаимозависимость между администратором заглушек, интерфейсными заглушками и объектом. Отметим, что некоторые интерфейсные заглушки знают, как декодировать более чем один интерфейсный тип, в то время как другие понимают только один интерфейс.
Когда CoUnmarshalInterface демаршалирует стандартно маршалированную объектную ссылку, фактически эта функция возвращает указатель администратору заместителей (proxy manager). Этот администратор заместителей действует как копия объекта со стороны клиента и, подобно администратору заглушек, не имеет никакой априорной информации ни об одним из интерфейсов СОМ. Однако администратор заместителей знает, как реализовать три метода IUnknown. Любые дополнительные вызовы AddRef или Release просто увеличивают или уменьшают на единицу внутренний счетчик ссылок в администраторе заместителей и никогда не передаются с использованием ORPC. Последний Release в администраторе заместителей уничтожает заместитель, посылая в апартамент объекта требование о прекращении связи. Запросы QueryInterface в администраторе заместителей обрабатываются несколько иначе. Подобно администратору заглушек, администратор заместителей не имеет никакой априорной информации об интерфейсах СОМ. Вместо этого администратор заместителей должен загружать интерфейсные заместители, выставляющие тот интерфейс, на который в данный момент идет запрос. Интерфейсный заместитель преобразует вызовы метода в запросы ORPC. В отличие от администратора заглушек, администратор заместителей является непосредственно видимым для программистов, и для обеспечения правильных отношений идентификации интерфейсные заместители агрегируются в идентификационную единицу администратора заместителей.
Это создает у клиента иллюзию, что все интерфейсы выставляются одним СОМ-объектом. На рис. 5.4 показаны отношения между администратором заместителей, интерфейсными заместителями и заглушкой.
Как показано на рис. 5.4, заместитель связывается с заглушкой через третий объект, называемый каналом. Канал — это поддерживаемая СОМ обертка вокруг слоя RPC на этапе выполнения. Канал выставляет интерфейс IRpcChannelBuffer
[ uuid(D5F56B60-593B-101A-B569-08002B2DBF7A), local, object ] interface IRpcChannelBuffer : IUnknown { // programmatic representation of ORPC message // программное представление сообщения ORPC typedef struct tagRPCOLEMESSAGE { void *reserved1; unsigned long dataRepresentation; // endian/ebcdic // endian /расширенный двоично-десятичный код // для обмена информацией void *Buffer; // payload goes here // полезная нагрузка идет сюда ULONG cbBuffer; // length of payload // длина полезной нагрузки ULONG iMethod; // which method? // чей метод? void *reserved2[5]; ULONG rpcFlags; } RPCOLEMESSAGE;
// allocate a transmission buffer // выделяем буфер для передачи HRESULT GetBuffer([inl RPCOLEMESSAGE *pMessage, [in] REFIID riid); // send an ORPC request and receive an ORPC response // посылаем ORPC-запрос и получаем ORPC-ответ HRESULT SendReceive([in,out] RPCOLEMESSAGE *pMessage, [out] ULONG *pStatus); // deallocate a transmission buffer // освобождаем буфер передачи HRESULT FreeBuffer([in] RPCOLEMESSAGE *pMessage); // get distance to destination for CoMarshalInterface // получаем расстояние до адресата для CoMarshalInterface HRESULT GetDestCtx([out] DWORD *pdwDestCtx, [out] void **ppvDestCtx); // check for explicit disconnects // проверяем явные отсоединения HRESULT IsConnected(void); }
Интерфейсные заместители используют метод SendReceive этого интерфейса, чтобы заставить канал послать блок запросов ORPC и получить блок ответов ORPC.
Интерфейсные заместители и заглушки являются обычными внутрипроцессными объектами СОМ, которые создаются администраторами соответственно заместителей и заглушек с использованием обычной СОМ-технологии активизации.
Интерфейсная заглушка должна выставить интерфейс IRpcStubBuffer:
[ uuid(D5F56AFC-593B-101A-B569-08002B2DBF7A), local, object ] interface IRpcStubBuffer : IUnknown { // called to connect stub to object // вызван для соединения заглушки с объектом HRESULT Connect([in] IUnknown *pUnkServer), // called to inform stub to release object // вызван для информирования заглушки об освобождении объекта void Disconnect(void); // called when ORPC request arrives // вызывается, когда поступает запрос ORPC HRESULT Invoke ([in] RPCOLEMESSAGE *pmsg, [in] IRpcChannelBuffer *pChannel); // used to support multiple itf types per stub // используется для поддержки нескольких типов интерфейсов // для одной заглушки IRpcStubBuffer *IsIIDSupported([in] REFIID riid); // used to support multiple itf types per stub // используется для поддержки нескольких интерфейсов // для одной заглушки ULONG CountRefs(vold); // used by ORPC debugger to find pointer to object // используется отладчиком ORPC для поиска указателя на объект HRESULT DebugServerQueryInterface(void **ppv); // used by ORPC debugger to release pointer to object // используется отладчиком ORPC для освобождения указателя на объект void DebugServerRelease(void *pv); }
Метод Invoke будет вызываться библиотекой СОМ, когда поступит запрос ORPC на объект. При вводе маршалированные [in]-параметры будут находиться в RPCOLEMESSAGE, а при выводе заглушка должна маршалировать HRESULT метода и любые [out]-параметры, которые будут возвращены в блоке ответов ORPC.
Интерфейсный заместитель должен выставлять интерфейс (интерфейсы), за удаленный доступ к которым он отвечает, в дополнение к интерфейсу IRpcProxyBuffer:
[ uuid(D5F56A34-593B-101A-B569-08002B2DBF7A), local, object ] interface IRpcProxyBuffer : IUnknown { HRESULT Connect([in] IRpcChannelBuffer *pChannelBuffer); void Disconnect(void); }
Интерфейс IRpcPгoxуBuffer должен быть неделегирующим интерфейсом IUnknown интерфейсного заместителя. Все остальные интерфейсы, которые выставляет интерфейсный заместитель, должны делегировать администратору заместителей свои методы IUnknown.
Именно в реализациях метода этих других интерфейсов интерфейсный заместитель должен использовать канал для посылки запросов ORPC на метод интерфейсной заглушки Invoke, который затем обрабатывает этот метод в апартаменте объекта.
Интерфейсные заместители и интерфейсные заглушки динамически связываются и совместно используют единый CLSID как для заместителя, так и для заглушки. Такую раздвоенную реализацию часто называют интерфейсным маршалером (interface marshaler). Объект класса интерфейсного маршалера выставляет интерфейс IPSFactoryBuffer:
[ uuid(D5F569DO-593B-101A-B569-08002B2DBF7A), local, object ] interface IPSFactoryBuffer : IUnknown { HRESULT CreateProxy( [in] IUnknown *pUnkOuter, // ptr to proxy manager // указатель на администратор заместителей [in] REFIID riid, // the requested itf to remote // запрошенный интерфейс для удаленного доступа [out] IRpcProxyBuffer **ppProxy, // ptr. to proxy itf. // указатель на интерфейс заместителя [out] void **ppv // ptr to remoting interface // указатель на удаленный интерфейс );
HRESULT CreateStub( [in] REFIID riid, // the requested itf to remote // запрошенный интерфейс для удаленного доступа [in] IUnknown *pUnkServer, // ptr to actual object // указатель на действующий объект [out] IRpcStubBuffer **ppStub // ptr to stub on output // указатель на заглушку на выходе ); }
Администратор заместителей вызывает метод CreateProxy с целью агрегирования нового интерфейсного заместителя. Администратор заглушек вызывает метод CreateStub с целью создания новой интерфейсной заглушки.
Когда в объекте запрашивается новый интерфейс, то администраторы заместителей и заглушек должны преобразовать запрошенные IID и CLSID интерфейсного маршалера. Под Windows NT 5.0 хранилише класса совершает эти преобразования в директории NT, и они кэшируются в локальном реестре каждой хост-машины. Отображения IID в CLSID всей машины кэшируются в
HKEY_CLASSES_ROOT\Interface
а отображения каждого пользователя кэшируются в
HKEY_CURRENT_USER\Software\Classes\Interface
Один из этих ключей или оба будут содержать подключи для каждого известного интерфейса. Под Windows NT 4.0 и в более ранних версиях не существует хранилища классов и используется только область локального реестра HKEY_CLASSES_ROOT\Interface.
Если в интерфейсе установлен интерфейсный маршалер, то в реестре будет дополнительный подключ (ProxyStubClsid32). который показывает CLSID интерфейсного маршалера. Ниже показаны необходимые ключи реестра для интерфейсного маршалера:
[HKCR\Interface\{1A3A29F0-D87E-11d0-8C4F-0080C73925BA}] @="IRacer" [HKCR\Interface\{1A3A29F0-D87E-11d0-8C4F-OB80C73925BA}\ProxyStubClsid32] @="{1A3A29F3-D87E-lld0-8C4F-0080C73925BA}"
Эти элементы реестра означают, что существует внутрипроцессный сервер с CLSID, равным {1A3A29F3-D87E-11d0-8C4F-0080C73925BA}, который реализует интерфейсные заместитель и заглушку для интерфейса IRacer ({1A3A29F0-D87E-11d0-8C4F-0080C73925BA}). Из этого следует, что HKCR\CLSID будет иметь подключ для интерфейсного маршалера, отображающего CLSID в соответствующее имя файла DLL. Опять же под Windows NT 5.0 это отображение может содержаться в хранилище классов, которое способно динамически заполнять локальный реестр. Поскольку интерфейсный маршалер должен выполняться в том же апартаменте, что и администратор заместителей или администратор заглушек, они должны использовать (флаг) ThreadingModel="Both" для гарантии того, что они всегда могут загрузиться в нужный апартамент.
1
Логически администратор заглушек обрабатывает удаленные вызоны методов IUnknown. Фактически, однако, эта задача выполняется тем объектом апартамента, который выставляет интерфейс IRemUnknown.
Где мы находимся?
В данной главе была описана абстракция апартаментов как логическое группирование объектов, которые подчиняются правилам параллелизма и реентерабельности. Процессы имеют один или более апартаментов. Потоки выполняются в ровно одном апартаменте, а для реализации межапартаментных связей СОМ поддерживает маршалинг объектных ссылок через границы апартаментов. Заместитель является локальным представителем объекта, постоянно находящимся в другом апартаменте. Стандартные заместители для передачи запросов методов с удаленного объекта используют ORPC. Специальные заместители имеют полную свободу для обеспечения корректной семантики. Апартамент является фундаментальной абстракцией, которая используется во всей архитектуре удаленного доступа модели СОМ.
Маршалер свободной поточной обработки (FreeThreaded Marshaler)
Если в классе установлена опция ThreadingModel="Both", то она показывает, что экземпляры класса, а также объект класса могут безопасно находиться в любых апартаментах: STA или МТА. В то же время, согласно правилам СОМ, любой данный экземпляр будет находиться только в одном апартаменте. Если бы разработчик объекта прошел все этапы проверки того, что объект может благополучно находиться в МТА, то в этом случае объекту вообще не нужно было бы заботиться об апартаментах. Одновременный доступ к подобному объекту мог бы быть не только для нескольких потоков внутри МТА, но также от потоков вне МТА (например, от потоков, выполняемых в STA). В то же время клиенты не могут знать, что такой доступ является безопасным для отдельно взятого объекта, поэтому любое совместное использование интерфейсного указателя в нескольких апартаментах должно быть установлено с использованием явной технологии маршалинга. Это означает, что доступ к внутрипроцессному объекту будет осуществляться через ORPC-вызовы, если только вызывающий объект не выполняется в том же самом апартаменте, где был создан объект.
В отличие от клиентов, объекты знают о своих отношениях с апартаментами, о своем параллелизме и реентерабельности. Объекты, удовлетворяющиеся ORPC-запросами при доступе из нескольких апартаментов одного и того же процесса, ведут себя так по умолчанию. А объект, которого не устраивает доступ ORPC, имеет возможность обойти это путем реализации специального маршалинга. Довольно просто использовать специальный маршалинг для обхода администратора заглушек и преобразования исходного указателя на объект в маршалированную объектную ссылку. При использовании этой технологии реализация специального заместителя могла бы просто считывать исходный указатель из маршалированной объектной ссылки и передавать его вызывающему объекту в импортирующем апартаменте. Клиентские потоки по-прежнему передавали бы интерфейсный указатель через границу апартамента с помощью явного или неявного вызова CoMarshalInterface / CoUnmarshalInterface.
Однако объект мог бы договориться со специальным заместителем о том, чтобы просто передать исходный указатель нужному объекту. Хотя данная технология безупречно работает для внутрипроцессного маршалинга, она, к сожалению, не приводит к успеху в случае межпроцессного маршалинга. Но, к счастью, реализация объекта может просто обратиться к стандартному маршалеру за другим контекстом маршалинга, отличным от MSHCTX_INPROC.
Поскольку только что описанное поведение является полезным для большого класса объектов, в СОМ предусмотрена агрегируемая реализация IMarshal, выполняющая в точности то, что было описано. Эта реализация называется маршалером свободной поточной обработки (FreeThreaded Marshaler - FTM) и может быть осуществлена с помощью вызова API-функции CoCreateFreeThreadedMarshaler:
HRESULT CoCreateFreeThreadedMarshaler( [in] IUnknown *pUnkOuter, [out] IUnknown **ppUnkInner);
Класс, который желает использовать FTM, просто агрегирует экземпляр либо во время инициализации, либо по требованию при первом запросе QueryInterface об интерфейсе IMarshal. Следующий класс заранее обрабатывает FTM во время построения.
class Point : public IPoint { LONG m_cRef; IUnknown *m_pUnkFTM; long m_x; long m_y; Point(void) : m_cRef(0), m_x(0), m_y(0) { HRESULT hr = CoCreateFreeThreadedMarshaler(this,&m_pUnkFTM); assert(SUCCEEDED(hr)) ; } virtual ~Point(void) { m_pUnkFTM->Release(); } };
Соответствующая реализация QueryInterface просто запросила бы интерфейс IMarshal из FTM:
STDMETHODIMP Point::QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IUnknown riid == IID_IPoint) *ppv = static_cast<IPoint*>(this); else if (riid == IID_IMarshal) return m_pUnkFTM->QueryInterface(riid, ppv); else return (*ppv = 0), E_NOINTERFACE; ((IUnknown* )*ppv)->AddRef(); return S_OK; }
Поскольку используется FTM, не понадобится никаких заместителей, как бы ни маршалировались через внутрипроцессные границы апартамента ссылки на объекты Point. Это применимо к явным вызовам CoMarshalInterface / CoUnmarshalInterface, а также в случаях, когда ссылки на объекты Point передаются как параметры метода на внутрипроцессные заместители объектов, не являющихся объектами Point.
FTM занимает не менее 16 байт памяти. Поскольку многие внутрипроцессные объекты никогда не используются за пределами своего апартамента, то предварительное выделение памяти для FTM не является лучшим использованием имеющихся ресурсов. В высшей степени вероятно, что объект уже имеет некий примитив для синхронизации потоков. В таком случае FTM может быть отложенно агрегирован (lazy-aggregated) при первом же запросе QueryInterface о IMarshal. Для того чтобы добиться этого, рассмотрим такое определение класса:
class LazyPoint : public IPoint { LONG m_cRef; IUnknown *m_pUnkFTM; long m_x; long m_y; LazyPoint (void) : m_cRef (0) .m_pUnkFTM(0),m_x(0), m_y(0) {} virtual ~LazyPoint(void) { if (m_pUnkFTM) m_pUnkFTM->Release(); } void Lock(void); // acquire object-specific lock // запрашиваем блокировку, специфическую для объектов void Unlock(void); // release object-specific lock // освобождаем блокировку, специфическую для объектов : : : };
Основываясь на таком определении класса, следующая реализация QueryInterface осуществит корректное агрегирование FTM по требованию:
STDMETHODIMP Point::QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IUnknown riid == IID_IPoint) *ppv = static_cast<IPoint*>(this); else if (riid == IID_IMarshal) { this->Lock(); HRESULT hr = E_NOINTERFACE; *ppv = 0; if (m_pUnkFTM == 0) // acquire FTM first time through // получаем первый FTM CoCreateFreeThreadedMarshaler(this, &m_pUnkFTM); if (m_pUnkFTM != 0) // by here, FTM is acquired // здесь получен FTM hr = m_pUnkFTM->QueryInterface(riid, ppv); this->Unlock(); return hr; } else return (*ppv = 0), E_NOINTERFACE; ((IUnknown *)*ppv)->AddRef(); return S_OK; }
Недостатком данного подхода является то, что все запросы QueryInterface на IMarshal будут сериализованы (преобразованы в последовательную форму); тем не менее, если IMarshal вообще не будет запрошен, то будет запрошено меньше ресурсов.
Теперь, когда мы убедились в относительной простоте использования FTM, интересно обсудить случаи, в которых FTM не годится.
Конечно, те объекты, которые могут существовать только в однопотоковых апартаментах, не должны использовать FTM, так как маловероятно, что они будут ожидать одновременного обращения к ним. В то же время объекты, способные работать в апартаментах МТА, отнюдь не обязаны использовать FTM. Рассмотрим следующий класс, который использует для выполнения своих операций другие СОМ-объекты:
class Rect : public IRect { LONG m_cRef; IPoint *m_pPtTopLeft; IPoint *m_pPtBottomRight; Rect(void) : m_cRef(0) { HRESULT hr = CoCreateInstance(CLSID_Point, 0, CLSCTX_INPROC, IID_Ipoint, (void**) &m_pPtTopLeft); assert(SUCCEEDED (hr)); hr = CoCreateInstance(CLSID_Point, 0, CLSCTX_INPROC, IID_Ipoint, (void**)&m_pPtBottomRight); assert (SUCCEEDED(hr)); } ; ; ; }
Пусть класс Rect является внутрипроцессным и помечен как ThreadingModel = "Both". Разработчик данного Rect-объекта всегда будет выполняться в апартаменте потока, вызывающего CoCreateInstance (CLSID_Rect). Это означает, что два вызова CoCreateInstance (CLSID_Point) будут также выполняться в апартаменте клиента. Правила же СОМ гласят, что элементы данных m_pPtTopLeft и m_pPtBottomRight могут быть доступны только из того апартамента, который выполняет вызовы CoCreateInstance.
Похоже на то, что по меньшей мере один из методов Rect использует в своей работе два интерфейсных указателя в качестве элементов данных:
STDMETHODIMP Rect::get_Area(long *pn) { long top, left, bottom, right; HRESULT hr = m_pPtTopLeft->GetCoords(&left, &top); assert(SUCCEEDED(hr)); hr = m_pPtBottomRight->GetCoords(&right, &bottom); assert (SUCCEEDED (hr)); *pn = (right - left) * (bottom - top); return S_OK; }
Если бы класс Rect должен был использовать FTM, тогда можно было бы вызывать этот метод из апартаментов, отличных от того апартамента, который осуществлял начальные вызовы CoCreateInstance. К сожалению, это заставило бы метод get_Area нарушить правила СОМ, поскольку два элемента данных — интерфейсные указатели — являются легальными только в исходном апартаменте.
Если бы класс Point также использовал FTM, то формально это не было бы проблемой. Тем не менее, в общем случае клиенты (такие, так класс Rect), не должны делать допущений относительно этой специфической исключительно для реализаций детали. Фактически, если объекты Point не используют FTM и окажутся созданными в другом апартаменте из-за несовместимости с ThreadingModel, то в этом случае объект Rect содержал бы указатели на заместители. Известно, что заместители четко следуют правилам СОМ и послушно возвращают RPC_E_WRONG_THREAD в тех случаях, когда к ним обращаются из недопустимого апартамента.
Это оставляет разработчику Rect выбор между двумя возможностями. Одна из них — не использовать FTM и просто принять к сведению, что когда клиенты передают объектные ссылки Rect между апартаментами, то для обращения к экземплярам класса Rect будет использоваться ORPC. Это действительно является простейшим решением, так как оно не добавляет никакого дополнительного кода и будет работать, не требуя умственных усилий. Другая возможность - не содержать исходные интерфейсные указатели как элементы данных, а вместо этого держать в качестве элементов данных некую маршалированную форму интерфейсного указателя. Именно для этого и предназначена глобальная интерфейсная таблица (Global Interface Table - GIT). Для реализации данного подхода в классе Rect следовало бы иметь в качестве элементов данных не исходные интерфейсные указатели, а "закладку" (cookies) DWORD:
class SafeRect : public IRect { LONG m_cRef; // СОМ reference count // счетчик ссылок СОМ IUnknown *m_pUnkFTM; // cache for FTM lazy aggregate // кэш для отложенного агрегирования FTM DWORD m_dwTopLeft; // GIT cookie for top/left // закладка GIT для верхнего/левого DWORD m_dwBottomR1ght; // GIT cookie for bottom/right // закладка GIT для нижнего/правого
Разработчик по-прежнему создает два экземпляра Point, но вместо хранения исходных указателей регистрирует интерфейсные указатели с помощью глобальной таблицы GIT:
SafeRect::SafeRect(void) : m_cRef(0), m_pUnkFTM(0) { // assume ptr to GIT is initialized elsewhere // допустим, что указатель на GIT инициализирован // где-нибудь в другом месте extern IGIobalInterfaceTable *g_pGIT; assert(g_pGIT != 0); IPoint *pPoint = 0; // create instance of class Point // создаем экземпляр класса Point HRESULT hr = CoCreateInstance(CLSID_Point, 0, CLSCTX_INPROC, IID_Ipoint, (void**)&pPoint); assert (SUCCEEDED (hr)); // register interface pointer in GIT // регистрируем интерфейсный указатель в GIT hr = g_pGIT->RegisterInterfaceInGlobal(pPoint, IID_Ipoint, &m_dwTopLeft); assert(SUCCEEDED(hr)); pPoint->Release(); // reference is now held in GIT // ссылка теперь содержится в GIT
// create instance of class Point // создаем экземпляр класса Point hr = CoCreateInstance(CLSID_Point, 0, CLSCTX_INPROC, IID_Ipoint, (void**)&pPoint); assert(SUCCEEDED(hr)); // register interface pointer in GIT // регистрируем интерфейсный указатель в GIT hr = g_pGIT->RegisterInterfaceInGlobal(pPoint, IID_Ipoint, &m_dwBottomRight); assert(SUCCEEDED(hr)); pPoint->Release(); // reference is now held in GIT // ссылка теперь содержится в GIT }
Отметим, что все то время, пока интерфейсный указатель зарегистрирован в GIT, пользователь интерфейсного указателя не должен хранить никаких дополнительных ссылок. Поскольку класс был преобразован для использования GIT вместо исходных интерфейсных указателей, он должен демаршалировать новый заместитель в каждом вызове метода, которому требуется доступ к зарегистрированным интерфейсам:
STDMETHODIMP SafeRect::get_Area(long *pn) { extern IGlobalInterfaceTable *g_pGIT; assert(g_pGIT != 0); // unmarshal the two interface pointers from the GIT // демаршалируем дВа интерфейсных указателя из GIT IPoint *ptl = 0, *pbr = 0; HRESULT hr = g_pGIT->GetInterfaceFromGlobal(m_dwPtTopLeft, IID_Ipoint, (void**)&ptl); assert (SUCCEEDED(hr)); hr = g_pGIT->GetInterfaceFromGlobal(m_dwPtBottomRight, IID_Ipoint, (void**)&pbr); // use temp ptrs to implement method // дпя реализации метода используем временные указатели long top, left, bottom, right; hr = ptl->GetCoords(&left, &top); assert (SUCCEEDED(hr)); hr = pbr->GetCoords(&right, &bottom); assert (SUCCEEDED (hr)); *pn = (right - left) * (bottom - top); // release temp ptrs. // освобождаем временные указатели ptl->Release(); pbr->Release(); return S_OK; }
Поскольку реализация SafeRect использует FTM, то нецелесообразно пытаться сохранить немаршалированные интерфейсные указатели между вызовами метода, так как неизвестно, произойдет ли следующий вызов метода в том же самом апартаменте. Все зарегистрированные интерфейсные указатели будут храниться в таблице GIT до тех пор, пока они не будут явно удалены нз GIT.
Это означает, что класс SafeRect должен явно аннулировать элементы GIT для двух своих элементов данных:
SafeRect::~SafeRect(void) { extern IGlobalInterfaceTable *g_pGIT; assert(g_pGIT != 0); HRESULT hr = g_pGIT->RevokeInterfaceFromGlobal(m_dwTopLeft); assert(SUCCEEDED(hr)); hr = g_pGIT->RevokeInterfaceFromGlobal(m_dwBottomRight); assert(SUCCEEDED(hr)); }
Удаление интерфейсного указателя из GIT освобождает все хранящиеся ссылки на объект.
Отметим, что совместное использование GIT и FTM влечет за собой очень много обращений к GIT, которые будут сделаны для создания временных интерфейсных указателей, необходимых для использования в каждом отдельном методе. Хотя GIT оптимизирована именно для поддержки такой схемы использования, код остается однообразным. Следующий простой класс C++ скрывает использование "закладки" GIT за удобным интерфейсом, обеспечивающим безопасность типа:
template <class Itf, const IID* piid> class GlobalInterfacePointer { DWORD m_dwCookie; // the GIT cookie // "закладка" GIT // prevent misuse // предотвращаем неправильное использование GlobalInterfacePointer(const GlobalInterfacePointer&); void operator =(const GlobalInterfacePointer&); public:
// start as invalid cookie // начинаем как неправильная "закладка" GlobalInterfacePointer(void) : m_dwCookie(0) { }
// start with auto-globalized local pointer // начинаем с автоматически глобализованным локальным указателем GlobalInterfacePointer(Itf *pItf, HRESULT& hr) : m_dwCookie(0) { hr = Globalize(pItf); }
// auto-unglobalize // осуществляем автоматическую деглобапизацию ~GlobalInterfacePointer(void) { if(m_dwСооkiе) Unglobalize() ; }
// register an interface pointer in GIT // регистрируем интерфейсный указатель в GIT HRESULT Globalize(Itf *pItf) { assert (g_pGIT != 0 && m_dwCookie == 0); return g_pGIT->RegisterInterfaceInGlobal(pItf, * piid, &m_dwCookie); }
// revoke an interface pointer in GIT // аннулируем интерфейсный указатель в GIT HRESULT Unglobalize(void) { assert(g_pGIT != 0 && m_dwCookie != 0); HRESULT hr = g_pGIT->RevokeInterfaceFromGlobal(m_dwCookie); m_dwCookie = 0; return hr; }
// get а local interface pointer from GIT // получаем локальный интерфейсный указатель из GIT HRESULT Localize(Itf **ppItf) const { assert(g_pGIT != 0 && m_dwCookie != 0); return g_pGIT->GetInteгfaceFromGlobal(m_dwCookie, *piid, (void**)ppItf); }
// convenience methods // методы для удобства bool IsOK(void) const { return m_dwCookie != 0; } DWORD GetCookie(void) const { return m_dwCookie; } };
#define GIP(Itf) GlobalInterfacePointer<Itf, &IID_##Itf>
Имея данное определение класса и макрос, класс SafeRect теперь вместо исходных DWORD сохраняет GlobalInterfacePointers:
class SafeRect : public IRect { LONG m_cRef: // СОM reference count // счетчик ссылок СОМ IUnknown *m_pUnkFTM; // cache for FTM lazy aggregate // кэш дпя отложенного агрегирования FTM GIP(IPoint) m_gipTopLeft; // GIT cookie - top/left // "закладка" GIT для верхнего/левого элемента GIP(IPoint) m_gipBottomRight; // GIT cookie - bottom/right // "закладка" GIT для нижнего/правого элемента : : : }
Для инициализации элемента GlobalInterfacePointer разработчик (который выполняется в апартаменте объекта) просто регистрирует обрабатываемые указатели, вызывая метод Globalize на каждый GlobalInterfacePointer:
SafeRect::SafeRect(void) : m_cRef (0), m_pUnkFTM(0) { IPoint *pPoint = 0; // create instance of class Point // создаем экземпляр класса Point HRESULT hr = CoCreateInstance(CLSID_Point, 0, CLSCTX_INPROC, IID_Ipoint, (void**)&pPoint); assert (SUCCEEDED(hr)); // register interface pointer in GIT // регистрируем интерфейсный указатель в GIT hr = m_gipTopLeft.Globalize(pPoint); assert (SUCCEEDED(hr)); pPoint->Release(); // reference is now held in GIT // теперь ссыпка хранится в GIT // create instance of class Point // создаем экземпляр класса Point hr = CoCreateInstance(CLSID_Point, 0, CLSCTX_INPROC, IID_Iроint, (void**) &рРоint); assert(SUCCEEDED(hr)); // register interface pointer in GIT // регистрируем интерфейсный указатель в GIT hr = m_gipBottomRight.Globalize(pPoint); assert (SUCCEEDED (hr)); pPoint->Release(); // reference is now held in GIT // теперь ссылка хранится в GIT }
Те методы, которым нужен доступ к глобализованным указателям, могут импортировать локальную копию посредством метода Localize из GlobalInterfaсePointer:
STDMETHODIMP SafeRect::get_Top(long *pVal) { IPoint *pPoint = 0; // local imported pointer // локальный импортированный указатель HRESULT hr = m_gipTopLeft.Localize(&pPoint); if (SUCCEEDED(hr)){ long x; hr = pPoint->get_Coords(&x, pVal); pPoint->Release(); } return hr; }
Отметим, что в силу применения маршалера свободной поточной обработки (FreeThreaded Marshaler) исходный интерфейсный указатель не может быть кэширован, а должен импортироваться при каждом вызове метода, чтобы предотвратить попытку доступа из неверного апартамента.
Предыдущий фрагмент кода может быть автоматизирован еще больше. Поскольку большинство вызовов методов в классе GlobalInterfacePointer должны будут локализовать временный указатель в самом вызове метода, то приводимый ниже класс автоматизирует импорт временного указателя и его последующее освобождение, что очень напоминает интеллектуальный указатель (smart pointer):
template <class Itf, const IID* piid> class LocalInterfacePointer { Itf *m_pItf; // temp imported pointer // временный импортированный указатель // prevent misuse // предотвращаем неверное использование LocalInterfacePointer(const LocalInterfacePointer&); operator = (const LocalInterfacePointer&); public: LocalInterfacePointer(const GlobalInterfacePointer<Itf, piid>& rhs, HRESULT& hr) { hr = rhs.Loca1ize(&m_pItf) ; }
LocalInterfacePointer(DWORD dwCookie, HRESULT& hr) { assert(g_pGIT != 0); hr = g_pGIT->GetInterfaceFromGlobal(dwCookie, *piid, (void**)&m_pItf); }
~LocalInterfacePointer(void) { if (m_pItf) m_pItf->Release(); }
class SafeItf : public Itf { STDMETHOD_(ULONG, AddRef) (void) = 0; // hide // скрытый STDMETHOD_(ULONG, Release)(void) = 0; // hide // скрытый };
SafeItf *GetInterface(void) const { return (SafeItf*) m_pItf; }
SafeItf *operator ->(void) const { assert(m_pItf != 0); return GetInterface(); }
};
#def1ne LIP(Itf) LocalInterfacePointer<Itf, &IID_##Itf>
С получением этого второго класса C++ обработка импортированных указателей становится намного проще:
STDMETHODIMP SafeRect::get_Area(long *pn) { long top, left, bottom, right; HRESULT hr, hr2; // import pointers // импортируем указатели LIP(IPoint) lipTopLeft(m_gipTopLeft, hr); LIP(IPoint) lipBottomRight(m_gipBottomRight, hr2); assert(SUCCEEDED(hr) && SUCCEEDED(hr2)); // use temp tocal pointers // используем временные локальные указатели hr = lipTopLeft->GetCoords(&left, &top); hr2 = lipBottomRight->GetCoords(&right, &bottom); assert(SUCCEEDED(hr) && SUCCEEDED(hr2)); *pn = (right - left) * (bottom - top); return S_OK; // LocalInterfacePointer auto-releases temp ptrs. // LocalInterfacePointer сам освобождает // временные указатели }
Макросы GIP и LIP делают совместное использование GIT и FTM намного менее громоздким. До появления GIT использование FTM в классе с интерфейсными указателями было значительно более трудным, чем теперь обеспечивает любой из кодов, приведенных в данном разделе.
Межапартаментный доступ
Для того чтобы объекты могли находиться в апартаментах, отличных от апартаментов клиента, в СОМ предусмотрена возможность экспорта интерфейсов из одного апартамента и импорта их в другой. Чтобы сделать интерфейс объекта видимым вне апартамента этого объекта, нужно экспортировать этот интерфейс. Чтобы сделать внешний интерфейс видимым внутри апартамента, нужно импортировать этот интерфейс. Когда интерфейс импортирован, то результирующий интерфейсный указатель ссылается на заместитель, доступ к которому разрешен для любого потока в импортирующем апартаменте. Обязанностью заместителя является передача управления обратно в апартамент объекта для того, чтобы удостовериться, что все вызовы метода выполняются в нужном апартаменте. Эта передача управления от одного апартамента к другому называется удaленным вызовом метода (method remoting) и является механизмом действия всех межпотоковых, межпроцессных и межмашинных связей в СОМ.
По умолчанию удаленный вызов метода использует имеющийся в СОМ протокол передачи ORPC (Object Remote Procedure Call - вызов объектом удаленной процедуры). СОМ ORPC является упрощенным протоколом MS-RPC (протокол вызова удаленной процедуры Microsoft), производным от DCE (Distributed Computing Environment - распределенная вычислительная среда). MS-RPC является независимым от протокола механизмом связи, который можно расширять с целью поддержки новых транспортных протоколов (посредством динамически загружаемых транспортных DLL) и новых пакетов аутентификации (посредством динамически загружаемых библиотек поставщика поддержки безопасности Security Support Provider DLL). СОМ использует наиболее эффективный на доступных транспортных протоколов в зависимости от подобия и типов импортирующего и экспортирующего апартаментов. При связи вне хост-машины СОМ предпочитает UDP (User Datagram Protocol - протокол передачи дейтаграмм пользователя), хотя и поддерживает большинство общеупотребительных сетевых протоколов. При локальной связи СОМ использует один из нескольких транспортных протоколов, каждый из которых оптимален для определенного типа апартаментов.
СОМ осуществляет передачу интерфейсных указателей через границы апартаментов с помощью особой технологии, именуемой маршалингом (marshaling), тo есть расположением в определенном порядке, выстраиванием. Маршалинг интерфейсного указателя — это преобразование его в передающийся байтовый поток, содержимое которого единственным образом идентифицирует объект и его собственный апартамент. Этот байтовый поток является маршалированным состоянием (marshaled state) интерфейсного указателя и дает возможность любому апартаменту импортировать интерфейсный указатель и осуществлять вызовы метода на объект. Отметим, что поскольку СОМ имеет дело исключительно с интерфейсными указателями, а не с самими объектами, это состояние маршалинга не представляет собой состояние объекта, а скорее преобразованное в последовательную форму (serialized) состояние не зависящей от апартаментов ссылки на объект. Такие маршалированные объектные ссылки просто содержат информацию об установлении связи, которая совершенно не зависит от состояния объекта.
Обычно указатели интерфейса маршалируются неявно как часть стандартной операции СОМ. Когда запрос на внутрипроцессную активацию сделан для класса с несовместимой моделью поточной обработки, СОМ неявно маршалирует интерфейс из апартамента объекта и демаршалирует заместитель в апартаменте клиента. Если сделан запрос на внепроцессную или внехостовую активацию, то СОМ также маршалирует результирующий указатель из апартамента объекта и демаршалирует заместитель для клиента. Если вызовы метода выполняются на заместители, то любые интерфейсные указатели, проходящие в качестве параметров метода, будут маршалированы с целью сделать объектные ссылки доступными в апартаментах и клиента, и объекта. Иногда необходимо маршалировать интерфейсы явным образом из одного апартамента в другой вне контекста запроса на активацию или вызова метода. Для поддержки этого режима в СОМ предусмотрена API-функция низкого уровня CoMarshalInterface, предназначенная для явного маршалинга интерфейсных указателей.
CoMarshalInterface принимает на входе интерфейсный указатель и записывает преобразованное в последовательную форму представление указателя в предоставленный вызывающим объектом байтовый поток. Этот байтовый поток может затем быть передан в другой апартамент, где API-функция CoUnmarshalInterface использует байтовый поток для возвращения интерфейсного указателя, который семантически эквивалентен исходному объекту, и к которому можно легально обращаться в апартаменте, выполняющем вызов функции CoUnmarshalInterface. При вызове CoMarshalInterface вызывающий объект должен указать, насколько далеко может располагаться импортирующий апартамент. В СОМ определен список рекомендуемых расстояний:
typedef enum tagMSHCTX { MSHCTX_INPROC = 4, // in-process/same host // внутрипроцессныи/тот же хост MSHCTX_LOCAL = 0, // out-of-process/same host // внепроцессный/тот же хост MSHCTX_NOSHAREDMEM = 1, // 16/32 bit/same host // 16/32-битный/тот же хост MSHCTX_DIFFERENTMACHINE = 2 // off-host // внехостовый } MSHCTX;
Допускается указывать большую дистанцию, чем необходимо, но для большей эффективности следует использовать по мере возможности корректное значение MSHCTX. CoMarshalInterface также позволяет вызывающему объекту специфицировать семантику маршалинга с помощью следующих специфических флагов:
typedef enum tagMSHLFLAGS { MSHLFLAGS_NORMAL, // marshal once, unmarshal once // маршалируем один раз, демаршалируем один раз MSHLFLAGS_TABLESTRONG, // marshal опсе, unmarshal many // маршалируем один раз. демаршалируем много раз MSHLFLAGS_TABLEWEAK, // marshal once, unmarshal many // маршалируем один раз, демаршалируем много раз MSHLFLAGS_NOPING = 4, // suppress dist. garbage collection // подавляем ненужный набор дистанций } MSHLFLAGS;
Нормальный (normal) маршалинг, иногда его называют еще маршалингом вызовов (call marshaling), означает, что маршалированная объектная ссылка должна быть демаршалирована только один раз, а если нужны дополнительные заместители, то требуются дополнительные вызовы CoMarshalInterface.
Табличный (table) маршалинг означает, что маршалированная объектная ссылка может быть демаршалирована нуль и более раз без требования дополнительных вызовов CoMarshalInterface. Подробности табличного маршалинга будут описаны далее в этой главе.
Чтобы разрешить маршалинг интерфейсных указателей на различные носители, функция CoMarshalInterface преобразует интерфейсный указатель в последовательную форму через интерфейс типа IStream, предоставляемый вызывающим объектом. Интерфейс IStream моделирует произвольное устройство ввода-вывода и выставляет методы Read и Write. Функция CoMarshalInterface просто вызывает метод Write на предоставленный вызывающим объектом интерфейс типа IStream, не интересуясь тем, куда эти фактические байты будут записаны. Вызывающие объекты могут получить обертку IStream на необработанную (raw) память, вызвав API-функцию CreateStreamOnHGlobal:
HRESULT CreateStreamOnHGlobal( [in] HGLOBAL hglobal, // pass null to autoalloc // передаем нуль для автовыдепения памяти [in] BOOL bFreeMemoryOnRelease, [out] IStream **ppStm);
С использованием семантики IStream следующий фрагмент кода:
void UseRawMemoryToPrintString(void) { void *pv = 0; // alloc memory // выделяем память pv = malloc(13); if (pv != 0) { // write a string to the underlying memory // пишем строку в основную память memcpy(pv, "Hello, World", 13); printf((const char*)pv); // free all resources // освобождаем все ресурсы free (pv); } }
эквивалентен такому фрагменту кода, использующему интерфейс IStream вместо memcpy:
void UseStreamToPrintString(void) { IStream *pStm = 0; // alloc memory and wrap behind an IStream interface // выделяем память и затем заворачиваем ее в интерфейс IStream HRESULT hr = CreateStreamOnHGlobal(0, TRUE, &pStm); if (SUCCEEDED(hr)) { // write a string to the underlying memory // записываем строку в память hr = pStm->Write("Hello. World", 13, 0); assert (SUCCEEDED (hr)); // suck out the memory // извлекаем память HGLOBAL hglobal = 0; hr == GetHglobalFromStream(pStm, &hglobal); assert(SUCCEEDED(hr)); printf((const char*)GlobalLock(hglobal)); // free all resources // освобождаем все ресурсы GlobalUnlock(hglobal); pStm->Release(); } }
API- функция GetHGlobalFromStream позволяет вызывающему объекту получать дескриптор (handle) памяти, выделенной функцией CreateStreamOnHGlobal. Использование HGLOBAL сложилось исторически и никоим образом не означает использование разделяемой памяти.
После осмысления всех типов параметров API-функции CoMarshalInterface она выглядит достаточно просто:
HRESULT CoMarshalInterface( [in] IStream *pStm, // where to write marshaled state // куда записывать маршалированное состояние [in] REFIID riid, // type of ptr being marshaled // тип маршалируемого указателя [in, iid_is(riid)] IUnknown *pItf, // pointer being marshaled // маршалируемый указатепь [in] DWORD dwDestCtx, // MSHCTX for destination apt. // MSHCTX для апартамента адресата [in] void *pvDestCtx, // reserved, must be zero // зарезервирован, должен равняться нулю [in] DWORD dwMshlFlags // normal, vs. table marshal // нормальный маршалинг против табличного );
Следующий код маршалирует интерфейсный указатель в блок памяти, пригодный для передачи по сети в любой апартамент:
HRESULT WritePtr(IRacer *pRacer, HGLOBAL& rhglobal) { IStream *pStm = 0; гhglobal = 0; // alloc and wrap block of memory // выделяем и заворачиваем блок памяти HRESULT hr = CreateStreamOnHGlobal(0, FALSE, &pStm); if (SUCCEEDED(hr)) { // write marshaled object reference to memory // записываем в память маршалированную объектную ссылку hr = CoMarshalInterface(pStm, IID_Iracer, pRacer, MSHCTX_DIFFERENTMACHINE, 0, MSHLFLAGS_NORMAL); // extract handle to underlying memory // извлекаем дескриптор памяти if (SUCCEEDED(hr)) hr = GetHGlobalFromStream(pStm, &rhglobal); pStm->Release(); } return hr; }
Рисунок 5.1 иллюстрирует взаимоотношения между интерфейсным указателем и памятью, содержащей маршалированную объектную ссылку. После вызова CoMarshalInterface апартамент объекта готов получить от другого апартамента запрос на соединение. Поскольку был использован флаг MSHCTX_DIFFERENTMACHINE, то импортирующий апартамент может находиться на другой хост-машине.
Для того чтобы декодировать маршалированную объектную ссылку, созданную в предыдущем фрагменте кода, в нормальный интерфейсный указатель, импортирующему апартаменту необходимо вызвать API-функцию CoUnmarshalInterface:
HRESULT CoUnmarshalInterface( [in] IStream *pStm, // where to read marshaled state // откуда читать маршалированное состояние [in] REFIID riid, // type of ptr being unmaгshaled // тип демаршалируемого указателя [out, iid_is(riid)] void **ppv // where to put unmarshaled ptr // куда поместить демаршалированный указатель );
CoUnmarshalInterface просто читает преобразованную в последовательную форму объектную ссылку и возвращает указатель на исходный объект, к которому есть легальный доступ в апартаменте вызывающего потока. Если импортирующий апартамент отличается от того апартамента, который изначально экспортировал интерфейс, то результирующий указатель будет указателем на заместитель. Если по какой-то причине вызов CoUnmarshalInterface осуществлен из исходного апартамента, где располагается объект, то в этом случае будет возвращен указатель на сам объект и не будет создано никакого заместителя. Следующий код переводит маршалированную объектную ссылку в нормальный указатель интерфейса:
HRESULT ReadPtr(HGLOBAL hglobal, IRacer * &rpRacer) { IStream *pStm = 0; rpRacer = 0; // wrap block of existing memory passed on input // заключаем в оболочку блок существующей памяти, // переданный на вход HRESULT hr = CreateStreamOnHGlobal(hglobal, FALSE, &pStm); if (SUCCEEDED(hr)) { // get a pointer to the object that is legal in this apt. // получаем указатель на объект, легальный в этом апартаменте hr = CoUnmarshalInterface(pStm, IID_Iracer, (void**)&rpRacer); pStm->Release(); } return hr; }
Результирующий заместитель будет реализовывать каждый из экспортируемых объектом интерфейсов путем переадресации запросов методов в апартамент объекта.
До появления выпуска СОМ под Windows NT 4.0 формат маршалированной объектной ссылки не был документирован.
Для того чтобы позволить сторонним участникам создавать сетевые продукты, совместимые с СОМ, этот формат был документирован для открытого использования в 1996 году и представлен на рассмотрение как проект стандарта для Интернета. На рис. 5.2 показан формат маршалированной объектной ссылки. Заголовок маршалированной объектной ссылки начинается с изысканной сигнатуры "MEOW" (мяу), а поле флагов указывает на выбранную технологию маршалинга (например, стандартный (standard), специальный (custom)), а также IID интерфейса, содержащегося в ссылке. В случае стандартного маршалинга подзаголовок объектной ссылки показывает, сколько внешних ссылок представляет данная маршалированная ссылка.
Этот счетчик внешних ссылок является частью распределенного протокола "сборки мусора" (garbage collection) СОМ и не полностью совпадает со счетчиком ссылок AddRef/Release, который может быть реализован объектом. Интересным элементом объектной ссылки является кортеж (tuple) OXID/OID/IPID, который единственным образом идентифицируют интерфейсный указатель. Каждому апартаменту в сети во время его создания присваивается уникальный Идентификатор Экспортера Объектов (Object Exporter Identifier — OXID). Этот OXID используется для нахождения сетевой или IPC-адресной информации при первом соединении заместителя с объектом. Идентификатор Объекта (Object Identifier — OID) единственным образом определяет идентификационную единицу СОМ в сети и использует CoUnmarshalInterface для поддержания правил идентификации СОМ для заместителей. Идентификатор Интерфейсного Указателя (Interface Pointer Identifier - IPID) единственным образом определяет интерфейсный указатель в апартаменте и помещается в заголовок каждого последующего запроса метода. IPID используется для эффективной диспетчеризации запросов ORPC на нужный интерфейсный указатель в апартаменте объекта.
Хотя OXID представляют интерес как логические идентификаторы, сами по себе они бесполезны, так как заместителям нужен какой-нибудь механизм IРС или сетевой протокол для связи с апартаментом объекта.
Для перевода OXID в полностью квалифицированные адреса сети или IPC в каждой хост-машине, поддерживающей СОМ, существует специальная служба распознавателя OXID (OXID Resolver - OR). Под Windows NT 4.0 OR является частью службы RPCSS. Когда апартамент инициализируется впервые, СОМ назначает OXID и регистрирует его с помощью локального OR. Это означает, что каждый OR знает обо всех работающих апартаментах локальной системы. Кроме того, OR следит за каналом локального IPC-порта для каждого апартамента. Когда CoUnmarshalInterface требуется соединить новый заместитель с апартаментом объекта, то для преобразования OXID в действительный адрес сети или IРС используется локальный OR. Если демаршалированный указатель встречается на той же машине, где находится апартамент объекта, то OR просто ищет OXID в своей локальной OXID-таблице и возвращает локальный IPC-адрес. Если же демаршалированный указатель встречается не на той машине, на которой находится объект, то локальный OR сначала проверяет, встречался ли данный OXID ранее, для чего смотрит в специальном кэше, где хранятся недавно распознанные OXID удаленного доступа. Если OXID ранее не встречался, то он передает запрос OR на хост-машину объекта, используя RPC. Отметим, что маршалированная объектная ссылка содержит адрес хост-машины объекта в формате, подходящем для целого ряда сетевых протоколов, так что OR с демаршалированной стороны знает, куда направлять запрос.
Чтобы распознавать распределенные OXID, служба OR на каждой хост-машине ждет удаленные запросы на распознавание OXID на известном адресе (порт 135 для TCP и UDP) для каждого поддерживаемого сетевого протокола. Когда запрос на распознавание OXID получен из сети, опрашивается локальная таблица OXID. Запрос на распознавание покажет, какие сетевые протоколы поддерживает машина клиента. Если запрошенный процесс из апартамента еще не начал использовать один из запрошенных протоколов, то OR свяжется с СОМ-библиотекой в апартаменте объекта, используя локальный IРС, и инициирует использование запрошенного протокола в процессе объекта.
Как только это произойдет, OR занесет новый сетевой адрес апартамента в локальную таблицу OXID. После записи новый сетевой адрес возвращается к OR демаршалирующей стороны, где он кэшируется, чтобы предотвратить дополнительные сетевые запросы на часто используемые апартаменты. Может показаться странным, что контрольное считывание полностью квалифицированных сетевых адресов в ссылке маршалировапного объекта не выполняется сразу. Но уровень косвенности, который допускают не зависящие от протокола идентификаторы апартаментов (OXID), разрешает процессу на основе СОМ откладывать использование сетевых протоколов до тех пор, пока они не потребуются. Это особенно важно, поскольку СОМ может иметь дело с множеством различных протоколов (не только TCP), и требовать от каждого процесса слежения за запросами с использованием всех поддерживаемых протоколов было бы чрезвычайно неэффективно. Фактически, если СОМ-процесс никогда не экспортирует указатели внехостовым клиентам, он никогда не потратит вообще никаких сетевых ресурсов.
1
Если импортирующий апартамент — тот, к которому принадлежит объект, то заместители не используются и импортированный указатель будут указывать прямо на объект.
2
Предпочтение UDP перед TCP (Transmission Control Protocol) - протоколом управления передачей - отдается из-за чрезмерных непроизводительных издержек при установке связи, обесчечиваемой TCP. СОМ, подобно DCE RPC, располагает информацию о своей безопасности и синхронизации протоколов ярусами в заголовках пакетов, используемых для передачи первого запроса RPC. Поскольку основанные на СОМ системы имеют тенденцию устанавливать и прерывать множество временных связей, то UDP является наилучшим выбором. При использовании передачи дейтаграмм, таких как UDP, динимическая библиотека RPC каждый раз выполняет коррекцию ошибок и алгоритмы контроля над потоком/перегрузкой.ТСР.
3
Один из администраторов программ фирмы Microsoft (Microsoft Program Manager), пожелавший остаться неизвестным, утверждает, что MEOW означает Microsoft Extended Object Wire (расширенная передачи объектов Microsoft).Но автор, несмотря на свою доверчивость, относится к этому скептически и желал бы выразить в адрес вышеупомянутого источника свои сомнения.
Объекты, интерфейсы и апартаменты
Клиенты хотят вызывать методы объектов. Объекты просто хотят выставлять свои методы для клиентов. Тот факт, что объект может иметь ограничения на параллелизм (concurrency constraints), отличные от тех, которые привносятся клиентским апартаментом, является элементом реализации, о котором клиент не должен знать. Кроме того, если разработчик объекта сочтет нужным развернуть реализацию объекта только на малом количестве хост-машин, которое не содержит той хост-машины, где находится программа клиента, то это также является деталью реализации, о которой клиент не должен знать. В любом случае, однако, объект должен находиться в апартаменте, отличном от апартамента клиента.
С точки зрения программирования, членство в апартаменте является атрибутом интерфейсного указателя, а не атрибутом объекта. Когда интерфейсный указатель возвращается после вызова API-функции СОМ или после вызова метода, то поток, осуществивший вызов API-функции или метода, определяет, к какому апартаменту принадлежит результирующий интерфейсный указатель. Если вызов возвращает указатель на текущий объект, то объект сам расположен в апартаменте вызывающего потока. Часто объект не может находиться в вызывающем апартаменте: или потому, что объект уже существует и другом процессе или на другой хост-машине, или потому, что требования параллелизма, присущие этому объекту, несовместимы с клиентским апартаментом. В этих случаях клиент получает указатель на заместитель (proxy).
В СОМ заместителем называется объект, семантически идентичный объекту в другом апартаменте. По смыслу заместитель представляет собой точную копию объекта в другом апартаменте. Заместитель выставляет тот же набор интерфейсов, что и представляемый им объект, однако реализация заместителем каждого из интерфейсных методов просто переадресовывает вызовы на объект, обеспечивая тем самым то, что методы объекта всегда выполняются в его апартаменте. Любой интерфейсный указатель, получаемый клиентом от вызова API-функции или вызова метода, является легальным для всех потоков в апартаменте вызывающего объекта независимо от того, указывает он на объект или на заместитель.
Разработчики объектов выбирают типы апартаментов, в которых могут выполняться их объекты. В главе 6 будет рассмотрено, что внепроцессные серверы явно задают тип своих апартаментов посредством вызова CoInitializeEx с соответствующим параметром. Для внутрипроцессных серверов необходим другой подход, так как CoInitializeEx уже вызывалась клиентом во время создания объекта. Для того чтобы внутрипроцессные серверы могли контролировать тип своих апартаментов, в СОМ каждому CLSID разрешается задавать свою собственную потоковую модель (threading model), которая объявляется в локальном реестре с использованием переменной под названием ThreadingModel:
[HKCR\CLSID\ {96556310-D779-11d0-8C4F-0080C73925BA}\InprocServer32] @="C:\racer.dll" ThreadingModel="Free"
Каждый CLSID в DLL может иметь индивидуальную ThreadingModel. Под Windows NT 4.0 СОМ допускает четыре возможных значения ThreadingModel для CLSID. Значение ThreadingModel="Both" указывает на то, что класс может выполняться как в МТА, так и в STA. Значение ThreadingModel="Free" указывает, что класс может выполняться только в МТА. Значение ThreadingModel="Apartment" указывает, что класс может выполняться только в STA. Отсутствие ThreadingModel означает, что класс может выполняться только в главном STA. Главный STA определяется как первый STA, который должен быть инициализирован в процессе.
Если апартамент клиента совместим с моделью организации поточной обработки идентификатора класса CLSID, то все запросы на внутрипроцессную активацию для этого CLSID будут обрабатывать объект непосредственно в апартаменте клиента. Это, безусловно, наиболее эффективный сценарий, так как не требуется никаких промежуточных заместителей. Если же апартамент клиента несовместим с моделью организации поточной обработки, указанной в CLSID, то запросы на внутрипроцесспую активацию для таких CLSID будут приводить к скрытому созданию объекта в отдельном апартаменте, а клиенту будет возвращен заместитель.
В случае, когда STA-клиенты активируют классы с ThreadingModel="Free", объект класса ( и последующие его экземпляры) будут выполняться в МТА. В случае, когда MTA-клиенты активируют классы с ThreadingModel="Apartment", объект класса (и последующие его экземпляры) будут выполняться в STA, созданном СОМ. В случае, когда клиенты любого типа активируют классы на основе главного STA, объект класса (и последующие его экземпляры) будут выполняться в главном STA процесса. Если же клиент окажется потоком главного STA, то будет осуществлен прямой доступ к объекту. В противном случае клиенту будет возвращен заместитель. Если в процессе нет ни одного STA (то есть если ни один поток не вызвал CoInitiаlizeEx с флагом COINIT_APARTMENTTHREADED), тогда СОМ создаст новый STA с тем, чтобы он стал главным STA для процесса.
Разработчики классов, не предусматривающие модель организации поточной обработки для своих классов, могут большей частью игнорировать проблемы, связанные с потоками, так как доступ к их библиотекам DLL будет осуществляться только из одного потока, а именно из главного STA-потока. Те разработчики, которые предусматривают для своих классов поддержку любой явной модели организации поточной обработки, косвенно свидетельствуют, что каждый из множественных апартаментов в процессе (который может быть многопоточным) может содержать экземпляры класса. Поэтому разработчик должен защитить любые ресурсы, которые совместно используются более чем одним экземпляром класса, от параллельного доступа. Это означает, что все глобальные и статические переменные должны быть защищены с помощью соответствующего примитива синхронизации потоков. Для внутрипроцессного сервера СОМ глобальный счетчик блокировок, отслеживающий время жизни сервера, должен быть защищен с помощью InterlockedIncrement / InterlockedDecrement, как показано в главе 3. Любое другое специфическое для сервера состояние также должно быть защищено.
Разработчики, обозначающие свои классы ThreadingModel= "Apartment", указывают на то, что экземпляры этих классов могут быть доступны только из одного потока в течение всей жизни объекта.
Следонательно, нет необходимости защищать режим экземпляра, а нужно защищать только режим, общий для нескольких экземпляров класса, о чем упоминалось ранее. Разработчики, обозначающие свои классы ThreadingModel="Free" или ThreadingModel="Both" устанавливают для экземпляров своего класса режим работы в МТА, что означает, что единовременно возможен доступ только к одному экземпляру класса. Поэтому разработчики должны защищать все ресурсы, используемые одним экземпляром, от параллельного доступа. Это относится не только к общим статическим переменным, но также к элементам данных экземпляра. Для объектов, расположенных в динамически распределяемой области памяти, это означает, что элемент данных, отвечающий за счетчик ссылок, должен быть защищен с помощью InterlockedIncrement/InterlockedDecrement, как было показано в главе 2. Любое другое состояние экземпляра класса также должно быть защищено.
На первый взгляд, совершенно не понятно, зачем существует ThreadingModel="Free", поскольку требования для работы в МТА выглядят как расширенный набор требований соответствия STA. Если разработчик объекта планирует рабочие потоки, которым необходим доступ к объекту, то очень полезно предотвратить создание объекта в каком-либо STA. Дело в том, что рабочие потоки не могут входить в STA, где живет объект, и поэтому вынуждены работать в другом апартаменте. Если класс обозначен ThreadingModel="Both" и запрос на активацию исходит от STA-потока, то объект будет существовать в STA. Это означает, что рабочие потоки (которые будут работать в МТА) должны обращаться к объекту через межапартаментные вызовы методов, значительно менее эффективные, нежели внутриапартаментные вызовы. Тем не менее, если класс помечен как ThreadingModel="Free", то любые запросы на активацию со стороны STA вызовут создание нового экземпляра в МТА, где любые рабочие потоки смогут иметь прямой доступ к объекту. Это означает, что при вызове клиентом, размещенным в STA, методов такого объекта, эффективность будет сниженной, в то время как рабочие потоки будут обрабатываться с большей эффективностью.Это является приемлемым компромиссом, если рабочие потоки будут обращаться к объекту чаще, чем действующий клиент из STA. Было бы весьма соблазнительно смягчить правила СОМ и записать, что не будет ошибкой прямо обращаться к некоторым объектам из более чем одного апартамента. Однако в общем случае это неверно, особенно для объектов, которые используют другие объекты для своей работы.
1
Стоимость выполнения вызова метода из другого апартамента из-за непроизводительных издержек по переключению потоков может быть в тысячи раз выше, чем в случае, когда метод вызывается внутри апартамента.
Реализация интерфейсных маршалеров
В предыдущем разделе было показано четыре интерфейса, используемых архитектурой стандартного маршалинга. Хотя и допустимо реализовать интерфейсные маршалеры с помощью ручного кодирования на C++, на практике это осуществляется редко. Дело в том, что компилятор IDL может автоматически генерировать исходный С-код для интерфейсного маршалера на основе IDL-определения интерфейса. Созданные MIDL интерфейсные маршалеры преобразуют параметры метода в последовательную форму, используя протокол Сетевого Представления Данных (Network Data Representation — NDR), который допускает демаршалинг этих параметров при различных архитектурах хост-машин. NDR учитывает различия в порядке следования байтов, в формате с плавающей точкой, в наборе символов и в расположении результатов. NDR поддерживает фактически все совместимые с C типы данных. Для того чтобы обеспечить передачу интерфейсных указателей как параметров, MIDL генерирует вызовы CoMarshalInterface / CoUnmarshalInterface для маршалинга любых параметров интерфейсных указателей. Если параметр является статически типизированным интерфейсным указателем:
HRESULT Method([out] IRacer **ppRacer);
то сгенерированный код маршалера будет маршалировать параметр ppRacer путем передачи IID IRacer (IID_IRacer) вызовам CoMarshalInterface / CoUnmarshalInterface. Если же интерфейсный указатель типизирован динамически:
HRESULT Method([in] REFIID riid, [out, iid_is(riid)] void **ppv);
то сгенерированный код маршалера будет маршалировать интерфейс, используя IID, переданный динамически в первый параметр метода.
MIDL генерирует исходный код интерфейсного маршалера для каждого нелокального интерфейса, определенного вне области действия оператора library. В следующем псевдо-IDL коде
// sports.idl // виды спорта. Язык описания интерфейсов [local, object] interface IBoxer : IUnknown { ... } [object] interface IRacer : IUnknown { ... } [object] interface ISwimmer : IUnknown { ... } [helpstring("Sports Lib")] library SportsLibrary { interface IRacer; // include def.
of IRacer in TLB // включаем определение IRacer в библиотеку типов TLB [object] interface IWrestler : IUnknown { ... } }
только интерфейсы IRacer и ISwimmer будут иметь исходный код интерфейсного маршалера. MIDL не будет генерировать маршалирующий код для IBoxer, поскольку атрибут [local] подавляет маршалинг. MIDL также не будет генерировать маршалер для IWrestler, поскольку он определен внутри области действия библиотечного оператора.
Если MIDL представлен в IDL такого типа, он будет генерировать пять файлов. Файл sports.h будет содержать С/С++-определения интерфейсов, sports_i.с - IID и LIBID, a sports.tlb — разобранный (tokenized) IDL для IRacer и IWrestler, который можно использовать в средах разработки, поддерживающих СОМ. Файл sports_p.c будет содержать фактические реализации методов интерфейсных заместителей и заглушек, которые осуществляют преобразования вызова методов в NDR. Этот файл также может содержать С-определения виртуальной таблицы (vtable) для интерфейсных заместителей и заглушек наряду со специальным управляющим кодом MIDL. Поскольку интерфейсные маршалеры являются внутрипроцессными серверами СОМ, то четыре стандартные точки входа (DllGetClassObject и другие) должны быть также определены. Эти четыре метода определены в пятом файле dlldata.c.
Для того чтобы построить интерфейсный маршалер из этих сгенерированных файлов, необходимо лишь создать сборочный файл (makefile), который скомпилирует три исходных С-файла (sports_i.с, sports_p.c, dlldata.c) и скомпонует их имеете для создания библиотеки DLL. Четыре стандартные точки входа СОМ должны быть явно экспортированы с помощью либо файла определения модуля, либо переключателей компоновщика. Отметим, что по умолчанию dlldata.c содержит только определения DllGetClassObject и DllCanUnloadNow. Это происходит потому, что поддерживающая RPC динамическая библиотека под Windows NT 3.50 поддерживала только эти две подпрограммы. Если интерфейсный маршалер будет использоваться только под Windows NT 3.51 или под более поздние версии (а также под Windows 95), то символ С-препроцсссора REGISTER_PROXY_DLL должен быть определен при компиляции файла dlldata.c, чтобы стандартные входные точки саморегистрации также были скомпилированы.
Интерфейсный маршалер после создания должен быть установлен в локальном реестре и/или в хранилище классов.
В реализацию библиотеки СОМ под Windows NT 4.0 введена поддержка полностью интерпретируемого (interpretive) маршалинга. В зависимости от интерфейса использование интерпретируемого маршалера может значительно увеличить эффективность приложения путем сокращения объема рабочей памяти (working set). Предварительно установленные интерфейсные маршалеры для всех стандартных интерфейсов СОМ используют интерпретируемый маршалер. Microsoft Transaction Server (MTS) обязывает интерфейсные маршалеры использовать интерпретируемый маршалер. Чтобы задействовать интерпретируемый маршалер, просто запустите компилятор MIDL с переключателем /Oicf в командной строке:
midl.exe /0icf sports.idl
В то время, когда пишется этот текст, компилятор MIDL не перезаписывает существующий файл _p.c, так что при изменении данной установки этот файл должен быть удален. Поскольку интерфейсные маршалеры, основанные на /Oicf, не будут работать на версиях СОМ до Windows NT 4.0, то при компиляции исходного кода маршалера символу С-препроцессора _WIN32_WINNT нужно присвоить целое значение, большее или равное 0х400. С-компилятор сделает это во время компиляции.
Третья методика для генерирования интерфейсных маршалеров поддерживается ограниченным числом интерфейсных классов. Если интерфейс использует только простые типы данных, которые поддерживаются VARIANT, то можно использовать универсальный маршалер. Использование универсального маршалера разрешается путем добавления атрибута [oleautomation] к определению интерфейса:
[ uuid(F99D19A3-D8BA-11d0-8C4F-0080C73925BA), version(1.0)] library SportsLib { importlib("stdole32.tlb"); [ uuid(F99D1907-D8BA-11D0-8C4F-0080C73925BA), object, oleautomation ] interface IWrestler : IUnknown { import "oaidl.idl"; HRESULT HalfNelson([in] double nmsec); } }
Наличие атрибута [oleautomation] информирует функцию RegisterTypeLib, что при регистрации библиотеки типов ей следует добавить следующие дополнительные элементы реестра:
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}] @="IWrestler"
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}\ProxyStubClsid32] @="{O0020424-0000-0000-C000-000000000046}"
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}\ProxyStubClsid] @="{O0020424-0000-0000-C000-000000000046}"
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}\TypeLib] @="{F99D19AЗ-08BA-11d0-8C4F-0080C73925BA}" Version="1.0"
CLSID {O0020424-0000-0000-C000-000000000046} соответствует универсальному маршалеру, который предварительно устанавливается на всех платформах, поддерживающих СОМ, в том числе в 16-разрядных Windows.
Основное преимущество использования универсального маршалера заключается в том, что это — единственная поддерживаемая методика осуществления стандартного маршалинга между 16- и 32-разрядными приложениями. Кроме того, универсальный маршалер совместим с Microsoft Transaction Server. Другое преимущество универсального марщалера заключается в следующем: если библиотека типов установлена на хост-машинах и клиента, и объекта, то не потребуется никакой дополнительной DLL интерфейсного марщалера. Основной же недостаток использования универсального маршалера — ограниченная поддержка типов данных параметров. Это то же самое ограничение, которое устанавливают динамический вызов и среды выполнения сценариев, но является серьезным ограничением при разработке интерфейсов системного программирования низкого уровня. Под Windows NT 4.0 начальные затраты на вызов CoMarshalInterface / CoUnmarshalInterface несколько повысятся при использовании универсального маршалера. Однако после обработки интерфейсных заместителя и заглушки затраты на вызов метода становятся эквивалентными затратам на /0icf-маршалер.
1
MTS также требует, чтобы при создании маршалеров использовалась специальная динамическая библиотека. Интерпретируемый формат маршалера позволяет MTS получать информацию об интерфейсе.
2
Variants - это тип данных, который используется в средах подготовки сценариев (scripting environments) и обсуждался в главе 2.
3
Вероятно, в будущих реализациях библиотеки СОМ это ограничение будет снято. О деталях можете справиться в своей локальной документации.
Снова интерфейс и реализация
Некоторые разработчики всесторонне используют многопоточные программные технологии и способны написать удивительно изощренный программный продукт с использованием примитивов синхронизации потоков, доступных из операционной системы. Другие разработчики больше сконцентрированы на решении вопросов, специфических для их области, и не утруждают себя написанием нудного потоко-безопасного кода. Третьи разработчики имеют особые ограничения по организации потоков, связанные с тем, что многие системы с управлением окнами (включая Windows) имеют очень строгие правила взаимодействия потоков и оконных примитивов. Еще один класс разработчиков может широко использовать старую библиотеку классов, которая неприязненно относится к потокам и не может допустить какого бы то ни было многопоточного обращения. У всех четырех типов разработчиков должна быть возможность использования объектов друг друга без перестраивания их потоковой стратегии, чтобы приспособиться ко всем возможным сценариям. Чтобы облегчить прозрачное использование объекта безотносительно к его осведомленности о потоках, СОМ рассматривает ограничения на параллелизм объектов как еще одну деталь реализации, о которой клиенту не нужно беспокоиться. Чтобы освободить клиента от ограничений параллелизма и реентерабельности (повторной входимости), в СОМ имеется весьма формальная абстракция, которая моделирует связь объектов с процессами и потоками. Эта абстракция носит название апартамент (apartment). Апартаменты определяют логическое группирование объектов, имеющих общий набор ограничений по параллелизму и реентерабельности. Каждый объект СОМ принадлежит к ровно одному апартаменту: один апартамент, однако, может быть общим для многих объектов. Апартамент, к которому относится объект, безоговорочно является частью идентификационной уникальности объекта.
Апартамент не является ни процессом, ни потоком; но в то же время апартаменты обладают некоторыми свойствами обоих этих понятий. Каждый процесс, использующий СОМ, имеет один апартамент или более; тем не менее, апартамент содержится ровно в одном процессе.
Это означает, что каждый процесс, использующий СОМ, имеет как минимум одну группу объектов, которая удовлетворяет требованиям параллелизма и реентерабельности; однако два объекта, находящихся в одном и том же процессе, могут принадлежать к двум различным апартаментам и поэтому иметь разные ограничения по параллелизму и реентерабельности. Этот принцип позволяет библиотекам с совершенно различными понятиями о потоках мирно соучаствовать и одном общем процессе.
Поток одновременно выполняется в одном и только одном апартаменте. Для того чтобы поток мог использовать СОМ, он должен сначала войти в апартамент. Когда поток входит в апартамент, СОМ размещает информацию об апартаменте в локальной записи потока (TLS - thread local storage), и эта информация связана с потоком до тех пор, пока поток не покинет апартамент. СОМ обеспечивает доступ к объектам только для тех потоков, которые выполняются в апартаменте данного объекта. Это означает, что если поток выполняется в том же процессе, что и объект, то потоку может быть запрещено обращаться к объекту, даже если память, которую занимает объект, полностью видима и доступна. В СОМ определен HRESULT (RPC_E_WRONG_THREAD), который возвращают некоторые системные объекты при прямом обращении из других апартаментов. Объекты, определенные пользователем, также могут возвращать этот HRESULT; однако лишь немногие разработчики желают пройти столь долгий путь для обеспечения правильного использования своих объектов.
В версии СОМ под Windows NT 4.0 определяется два типа апартаментов: многопоточные апартаменты (МТА - multithreaded apartments) и однопоточные апартаменты (STA - singlethreaded apartments). В каждом процессе может быть не больше одного МТА; однако процесс может содержать несколько STA. Как следует из этих названий, в МТА может выполняться одновременно несколько потоков, а в STA — только один поток. Точнее, только один поток может когда-либо выполняться в данном STA; что означает не только то, что к объектам, находящимся в STA, никогда нельзя будет обратиться одновременно, но также и то, что только один определенный поток может когда-либо выполнять методы объекта.
Эта привязка (affinity) к потоку позволяет разработчикам объекта надежно сохранять промежуточное состояние между вызовами метода в локальной памяти потока TLS (thread local storage), а также сохранять блокировки (locks), требующие поточной привязки (например, критические секции Win32 и семафоры (mutexes)).
Подобная практика приведет к катастрофе, если применять ее в случае объектов из МТА, так как в этом случае неизвестно, какой поток будет выполнять данный вызов метода. Неудобство STA заключается в том, что он позволяет одновременно выполняться только одному вызову метода, независимо от того, сколько объектов принадлежат к данному апартаменту. В случае МТА потоки могут быть распределены динамически на основании текущих запросов, вне зависимости от количества объектов в апартаменте. Для того чтобы создать параллельные серверные процессы с использованием только однопоточных апартаментов, потребуется много апартаментов, и если не соблюдать осторожность, то может возникнуть непомерное количество потоков. Кроме того, уровень параллелизма в основанных на STA обслуживающих процессах не может превышать общее число объектов в процессе. Если процесс сервера содержит только малое число крупномодульных (coarse-grained) объектов, то удастся обойтись малым числом потоков, даже если каждый объект существует в своем отдельном STA.
В будущей реализации СОМ будет, возможно, введен третий тип апартамента — апартамент, арендованный потоками (RTA - rentalthreaded apartment). Подобно МТА, RTA позволяет входить в апартамент более чем одному потоку. Но, в отличие от МТА, когда поток входит в RTA, он вызывает блокировку всего апартамента (apartment-wide lock) (то есть он как бы арендует апартамент), которая запрещает другим потокам одновременно входить в апартамент. Эта блокировка апартамента снимается, когда поток покидает RTA, что позволяет войти следующему потоку. В этом отношении RTA подобен МТА, за исключением того, что все вызовы методов осуществляются последовательно. Это обстоятельство делает RTA значительно удобнее для классов, относительно которых неизвестно, являются ли они потокобезопасными.
Хотя все вызовы в STA также осуществляются сериями, объекты на основе RTA отличаются тем, что в них нет привязки потоков; то есть внутри RTA могут выполняться любые потоки, а не только тот исходный поток, который создал апартамент. Эта свобода от привязки к потоку делает объекты на базе RTA более гибкими и эффективными, чем объекты на основе STA, так как любой поток потенциально может сделать вызов объекта, просто войдя в RTA объекта. На момент написания этого текста еще окончательно не определились детали создания апартаментов RTA и входа в них. За подробностями вы можете обратиться к документации по SDK.
Когда поток впервые создается операционной системой как результат вызова CreateProcess или CreateThread, этому новообразованному потоку не сопоставлен ни один апартамент. Перед тем как использовать СОМ, новый поток должен войти в какой-либо апартамент путем вызова одной из приведенных далее API-функций.
В Windows NT 5.0 это будет изменено. За подробностями обращайтесь к документации по SDK.
HRESULT CoinitializeEx(void *pvReserved, DWORD dwFlags); HRESULT Coinitialize(void *pvReserved); HRESULT OleInitialize(vo1d *pvReserved);
Для всех трех только что описанных API-функций первый параметр зарезервирован и должен равняться нулю.
CoInitializeEx является API-функцией самого низкого уровня и позволяет вызывающему объекту определять, в какой тип апартамента нужно войти. Для того чтобы войти в МТА всего процесса, вызывающий объект должен использовать флаг COINIT_MULTITHREADED:
HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);
Для входа во вновь соаданный STA вызывающий объект должен выставить флаг COINIT_APARTMENTTHREADED:
HRESULT hr = CoInitializeEx(0, COINIT_APARTMENTTHREADED);
Каждый поток в процессе, который вызывает CoInitializeEx с применением COINIT_MULTITHREADED, выполняется в том же самом апартаменте. Каждый поток, который вызывает CoInitiаlizeEx с применением COINIT_APARTMENTTHREADED, выполняется в отдельном апартаменте, в который не могут входить никакие другие потоки.
CoInitialize — это традиционная подпрограмма, которая просто вызывает CoInitializeEx с флагом COINIT_APARTMENTTHREADED. Olelnitialize сначала вызывает CoInitialize, а затем инициализирует несколько подсистем из OLE-приложений, таких как OLE Drag and Drop и OLE Clipboard. Если не предполагается использовать эти службы более высокого уровня, то в общем случае предпочтительнее использовать CoInitialize или CoInitializeEx.
Каждая из этих трех API-функций может быть вызвана больше одного раза на поток. Первый вызов каждого потока возвратит S_ОК. Последующие вызовы просто повторно войдут в тот же апартамент и возвратят S_FALSE. На каждый успешный вызов CoInitialize или CoInitializeEx из того же потока нужно вызвать CoUninitialize. На каждый успешный вызов OleInitialize из того же потока нужно вызвать OleUninitialize. Эти подпрограммы деинициализации (uninitialization) имеют очень простые сигнатуры:
void CoUninitialize(void); void OleUninitialize(void);
Если все эти подпрограммы перед прекращением потока или процесса не вызваны, это может задержать восстановление ресурсов. Когда поток входит в апартамент, недопустимо изменять типы апартамента с использованием CoInitiаlizeEx. При попытках сделать это HRESULT будет содержать RPC_E_CHANGED_MODE. Однако, если поток полностью покинул апартамент посредством CoUninitialize, он может войти в другой апартамент путем повторного вызова CoInitializeEx.
1
Апартаменты являются более современным названием того, что в спецификации СОМ изначально именовалось контекстом исполнения (execution context).
Специальный маршалинг
До сих пор внимание в данной главе было сосредоточено на стандартном маршалинге и вызове методов на основе ORPC. Для большого класса объектов этого было достаточно, чтобы достичь нужного баланса между производительностью, семантической корректностью и простотой реализации. В то же время существуют объекты, для которых ORPC-вызов по умолчанию является неэффективным и даже непригодным. Для таких объектов в СОМ предусмотрен специальный маршалинг (custom marshaling). Как уже упоминалось в этой главе, специальный маршалинг позволяет разработчику объекта обеспечить реализацию специальных заместителей (custom proxies), которые будут созданы в импортирующих апартаментах. Объекты сообщают о своем желании поддерживать специальный маршалинг путем экспорта интерфейса IMarshal:
[uuid(00000003-0000-0000-C000-000000000046), local, object] interface IMarshal : IUnknown {
// get CLSID for custom proxy (CoMarshalInterface) // получаем CLSID для специального заместителя (CoMarshalInterface) HRESULT GetUnmarshalClass( [in] REFIID riid, [in, iid_is(riid) ] void *pv, [in] DWORD dwDestCtx, [in] void *pvDestCtx, [in] DWORD mshlflags, [out] CLSID *pclsid);
// get size of custom marshaled objref (CoGetMarshalSizeMax) // получаем размер специально маршалированной объектной ссылки (CoGetMarshalSizeMax) HRESULT GetMarshalSizeMax( [in] REFIID riid, [in, iid_is(riid)] void *pv, [in] DWORD dwDestCtx, [in] void *pvDestCtx, [in] DWORD mshlflags, [out] DWORD *pSize);
// write out custom marshaled objref (CoMarshalInterface) // выполняем контрольное считывание специально маршалированной объектной ссылки (CoMarshalInterface) HRESULT MarshalInterface([in] IStream *pStm, [in] REFIID riid, [in, iid_is(riid)] void *pv, [in] DWORD dwDestCtx, [in] void *pvDestCtx, [in] DWORD mshlflags);
// read objref and return proxy (CoUnmarshalInterface) // читаем объектную ссылку и возвращаем заместитель
// ( CoUnmarshalInterface) HRESULT UnmarshalInterface([in] IStream *pStm, [in] REFIID riid, [out, iid_is(riid)] void **ppv);
// revoke a marshal (CoReleaseMarshalData) // аннулируем маршалер (CoReleaseMarshalData) HRESULT ReleaseMarshalData([in] IStream *pStm);
// tear down connection-state (CoDisconnectObject) // разрываем связь между объектами (CoDisconnectObject) HRESULT DisconnectObject([in] DWORD dwReserved); }
Комментарии, предваряющие определения методов, показывают, какие именно API-функции вызывает каждый из них.
Когда метод CoMarshalInterface вызывается на объект, поддерживающий специальный маршалинг, маршалированная объектная ссылка имеет несколько другой (формат, как показано на рис. 5.7. Заметим, что после стандартного заголовка MEOW маршалированная объектная ссылка просто содержит CLSID, используемый для создания специального заместителя и непрозрачного байтового массива, предназначенного для инициализации этого специального заместителя. CoMarshalInterface находит CLSID специального заместителя посредством вызова на объект метода IMarshal::GetUnmarshalСlass. CoMarshalInterface заполняет непрозрачный байтовый массив, вызывая реализацию метода IMarshal::MarshalInterface объекта. Именно в MarshalInterface объект получает свой первый и единственный шанс послать инициализационное сообщение новому специальному заместителю, просто записав его в подаваемый байтовый поток.
При вызове CoUnmarshalInterface это сообщение будет передано вновь созданному специальному заместителю через его метод IMarshal::UnmarshalInterface. Это означает, что и объект, и специальный заместитель должны реализовать IMarshal. Метод объекта MarshalInterface записывает инициализационное сообщение. Метод заместителя UnmarshalInterface читает инициализационное сообщение. Когда метод UnmarshalInterface возвращается, СОМ больше не участвует ни в каких связях заместитель/объект. Реализация интерфейсных методов семантически корректным способом является делом специального заместителя. Если нужно произвести удаленный вызов метода на объект, то сделать это — задача заместителя.
Если же метод может быть реализован в апартаменте клиента, то заместитель может сделать и это.
Преимуществом специального маршалинга является то, что клиент не имеет понятия о его использовании. Фактически клиент не может достоверно определить, является ли интерфейс стандартным заместителем, специальным заместителем или настоящим объектом. Специальный маршалинг является решением на уровне объект-объект. Два экземпляра одного и того же класса могут независимо друг от друга избрать стандартный или специальный маршалинг. Если объект выбирает реализацию специального маршалинга, то он должен делать это для всех интерфейсов. Если объект желает специально маршалировать только для части всех возможных контекстов, подлежащих маршалингу — например, внутрипроцессный, локальный, с другой машины, — то он может получить экземпляр стандартного маршалера и направить его методы IMarshal для маршалинга неподдерживаемых контекстов, так чтобы могли поддерживаться все контексты. Если бы объект мог безоговорочно направить все методы IMarshal к стандартному маршалеру, то он практически всегда использовал бы стандартный маршалинг.
Для получения указателя на стандартный маршалер объекты могут вызывать метод CoGetStandardMarshal:
HRESULT CoGetStandardMarshal( [in] REFIID riid, // type of itf marshaled? // тип, которым маршалирован интерфейс? [in, iid_is(riid)] IUnknown *pUnk, // the itf to marshal // интерфейс для маршалинга [in] DWORD dwDestCtx, // MSHCTX [in] void *pvDestCtx, // reserved // зарезервировано [in] DWORD mshlflags, // normal vs. table // нормальный или табличный маршалинг [out] IMarshal **ppMarshal); // ptr to std. Marshal // указатель на стандартный маршалер
Предположим, что объект использует технологию специального маршалинга, которая работает только на локальном хосте, но не при связи с внехостовыми апартаментами. Реализация объектом метода GetMarshalSizeMax могла бы выглядеть примерно так:
STDMETHODIMP CustStd::GetMarshalSizeMax( ULONG *pcb, REFIID riid, void *pv, DWORD dwDestCtx, void *pvDestCtx, DWORD mshlflags) { // if context is supported, do work! // если контекст поддерживается, то действуем!
if (dwDestCtx == MSHCTX_LOCAL dwDestCtx == MSHCTX_INPROC) return this->MyCustomMarshalingRoutine(pcb);
// unsupported context, delegate to std marshal // контекст не поддерживается, обращаемся к стандартному маршапингу IMarshal *pMsh = 0; HRESULT hr = CoGetStandardMarshal (riid, pv, dwDestCtx, pvDestCtx, mshlflags, &pMsh); if (SUCCEEDED(hr)) { hr = pMsh->GetMarshalSizeMax(pcb, riid, pv, dwDestCtx, pvDestCtx, mshlflags); pMsh->Retease(); } return hr; }
В этом фрагменте кода не показано, как писать инициализационное сообщение для случая, когда действительно желателен специальный маршалинг. Дело в том, что не существует стандартной реализации каждого из методов IMarshal (отсюда и термин специальный (custom) маршалинг). Существует, однако, несколько общих сценариев, в которых специальный маршалинг чрезвычайно выигрышен и реализация IMarshal в этих сценариях — довольно обычное явление. Безусловно, наиболее общим приложением IMarshal является реализация маршалинга по значению (marshal-by-value).
Маршалинг по значению наиболее удобен для таких объектов, которые после инициализации никогда не изменяют своего состояния. Обертки СОМ для структур — вот типичный пример объекта, который просто инициализирован, передан другому объекту для запроса и затем уничтожен. Такой объект является первым кандидатом для специального маршалинга. При реализации маршалинга по значению реализация объекта почти всегда является внутрипроцессным сервером. Это позволяет объекту и заместителю разделять один и тот же класс реализации. Идея маршалинга по значению состоит в том, что специальный заместитель становится клоном исходного объекта. Из этого следует, что маршалированная объектная ссылка должна содержать все состояние исходного объекта, а также (для простоты) то, что CLSID специального заместителя должен быть тем же, что и у исходного объекта.
Представим следующее определение класса СОМ-обертки вокруг простой двумерной точки:
class Point : public IPoint, public IMarshal { long m_x; long m_y; public: Point(void) : m_x(0), m_y(0) {} IMPLEMENT_UNKNOWN (Point) BEGIN_INTERFACE_TABLE(Point) IMPLEMENTS_INTERFACE(IPoint) IMPLEMENTS_INTERFACE(IMarshal) END_INTERFACE_TABLE() // IPoint methods // методы IPoint // IMarshal methods // методы IMarshal };
Для поддержки маршалинга по значению метод MarshalInterface класса должен преобразовать состояние объекта в последовательную форму в качестве инициализационного сообщения для заместителя:
STOMETHODIMP Point::MarshalInterface(IStream *pStm, REFIID, void *, DWORD, void *, DWORD) { // write out endian header // переписываем завершающий заголовок DWORD dw = OxFF669900; HRESULT hr = pStm->Write(&dw, sizeof(DWORD), 0); if (FAILED(hr)) return hr; dw = m_x; hr = pStm->Write(&dw, sizeof(DWORD), 0); if (FAILED(hr)) return hr; dw = m_y; return pStm->Write(&dw, sizeof (DWORD), 0); }
Если допустить, что класс объекта реализован как внутрипроцессный сервер, то специальный заместитель может стать просто вторым экземпляром того же класса, из чего вытекает следующая реализация GetUnmarshalClass:
STDMETHODIMP Point::GetUnmarshalClass(REFIID, void *, DWORD, void *, DWORD, CLSID *pclsid) { *pclsid = CLSID_Point; // this class's CLSID // CLSID этого класса return hr; }
Для обеспечения того, чтобы для инициализационного сообщения было выделено достаточно места, методу объекта GetMarshalSizeMax требуется возвратить правильное количество байт:
STDMETHODIMP Point::GetMarshalSizeMax(REFIID, void *, DWORD, void *, DWORD, DWORD *pcb) { *pcb = 3 * sizeof (DWORD); // m_x + m_y + header return hr; }
Когда маршалированная объектная ссылка демаршалируется с помощью CoUnmarshalInterface, тот факт, что она была маршалирована специальным образом, вызовет создание нового специального заместителя. Объектная ссылка содержит CLSID специального заместителя, возвращенный исходным объектом в своем методе GetUnmarshalClass. Когда создан новый специальный заместитель, его метод UnmarshalInterface получает инициализационное сообщение, которое объект записал в своей реализации MarshalInterface:
STDMETHODIMP Point::UnmarshalInterface(IStream *pStm, REFIID riid, void ** ppv) { *ppv = 0; // read endian header // читаем заключительный заголовок DWORD dw; ULONG cbRead; HRESULT hr = pStm->Read(&dw, sizeof (DWORD), &cbRead); if (FAILED(hr) cbRead != sizeof(DWORD)) return RPC_E_INVALID_DATA; bool bSwapEndian = dw == 0x009966FF; // read m_x and m_y // читаем m_x и m_y hr = pStm->Read(&dw, sizeof(DWORD), &cbRead); m_x = dw; if (FAILED(hr) cbRead != sizeof(DWORD)) return RPC_E_INVALID_DATA; hr = pStm->Read(&dw, sizeof(DWORD), &cbRead); m_y = dw; if (FAILED(hr)) cbRead != sizeof(DWORD)) return RPC_E_INVALID_DATA; // byte swap members if necessary // байт переставляет свои биты, если необходимо if (bSwapEndian) byteswapdata(&m_x, &m_y); // return pointer to this object // возвращаем указатель на этот объект return this->QueryInterface(riid, ppv); }
Отметим, что реализация MarshalInterface и UnmarshalInterface должна позаботиться о том, чтобы маршалированное состояние могло читаться на любой платформе. Это означает ручную работу по выравниванию, расстановке байтов и учету различий в размерах типов данных.
Приведенная здесь реализация UnmarshalInterface просто возвращает указатель вновь созданному специальному заместителю. Для простого объекта, маршалированного по значению, это может быть приемлемо. Однако более типичные реализации UnmarshalInterface могут захотеть найти несколько демаршалированных указателей, соответствующих одной и той же идентификационной единице СОМ, и возвратить указатель на заместитель той же единицы, чтобы установить отношение идентичности заместителя объекту. Это может не только сэкономить ресурсы, но также повысить чистоту программы.
Стандартный маршалинг, потоки и протоколы
Подробности того. как СОМ на самом деле преобразует запросы ORPC в потоки, нигде не документированы и подлежат изменениям но мере развития реализации библиотеки СОМ. Описания, содержащиеся в данном разделе, относятся только ко времени написания этого текста, но, конечно, некоторые детали реализации могут измениться в последующих выпусках СОМ.
Когда в процессе инициализируется первый апартамент, СОМ включает RPC-слой времени выполнения, переводя процесс в RPC-сервер. Если апартамент имеет тип МТА или RTA, то используется RPC-протокол ncalrpc, который является оберткой вокруг портов LPC (local procedure call - локальный вызов процедуры) под Windows NT. Если тип апартамента - STA, то используется последовательность закрытого протокола, основанная на очередях сообщений MSG Windows. При первом обращении внехостовых клиентов к объектам, постоянно находящимся в процессе, в процессе регистрируются дополнительные сетевые протоколы. Когда процесс впервые начинает использовать протоколы, отличные от протокола MSG Windows, запускается кэш потоков RPC. Этот кэш потоков начинается с одного потока, который ожидает приходящих запросов на установление соединения, запросов RPC или других действий, специфических для протоколов. Как только произойдет любое из этих событий, кэш потоков RPC переключит поток на обслуживание запроса и будет ждать следующих действий. Для предотвращения излишних затрат на создание/уничтожение потоков эти потоки возвращаются в потоковый кэш, где они будут ждать дальнейшей работы. Если работы нет, то потоки самоуничтожатся по истечении заранее определенного периода бездействия. Теневая сторона этого заключается в том, что кэш потоков RPC растет или сжимается в зависимости от занятости объектов, экспортированных из апартаментов процессов. С точки зрения программирования важно заметить, что кэш потоков RPC динамически размещает потоки, основанные на ORPC-запросах, приходящие по любым протоколам, кроме протокола Windows MSG, который будет обсуждаться позднее в этом разделе.
Когда поступающий ORPC- запрос переадресуется потоку из кэша. поток выделяет из заголовка ORPC-вызова IPID (идентификатор указателя интерфейса) и находит соответствующий администратор заглушек и интерфейсную заглушку. Поток определяет тип того апартамента, в котором хранится объект, и если объект находится в апартаменте типа МТА или RTA, поток входит в апартамент объекта и вызывает метод IRpcStubBuffer::Invoke на интерфейсную заглушку. Если апартамент имеет тип RTA, то в течение вызова метода последующие потоки не будут допускаться к объекту. Если апартамент имеет тип МТА, то последующие потоки могут обращаться к объекту одновременно. В случае внутрипроцессных RTA/MTA-связей канал может сократить кэш потоков RPC и использовать поток клиента повторно, временно входя в апартамент объекта. Если бы МТА и RTA были единственными типами апартаментов, то этого было бы достаточно.
Диспетчеризация вызовов в STA более сложна, так как в существующий STA не могут войти никакие другие потоки. К сожалению, когда ORPC-запросы поступают от внехостовых клиентов, они координируются с использованием потоков из RPC кэша потоков, которые по определению не могут выполняться в STA объекта. Для того чтобы войти в STA и направить вызов потока STA, RPC-поток использует АРI-функцию PostMessage, которая ставит сообщение в специальную MSG-очередь сообщений STA-потоков, как показано на рис. 5.5. Эта очередь представляет собой ту же очередь FIFO (first-in, first-out), которую применяет оконная система. Это означает, что для завершения диспетчеризации вызова STA-поток должен обслуживать очередь с помощью одной из вариаций следующего кода:
MSG msg; while (GetMessage(&msg, 0, О, 0)) DispatchMessage(&msg);
Этот код означает, что STA-поток имеет по меньшей мере одно окно, которое может принимать сообщения. Когда поток входит в новый STA посредством вызова CoInitializeEx, СОМ создает новое невидимое окно, вызывая CreateWindowEx. Это окно связано с зарегистрированным в СОМ оконным классом, элемент которого WndProc ищет определенные заранее оконные сообщения и обслуживает соответствующий ORPC-запрос посредством вызова метода IRpcStubBuffer::Invoke на интерфейсную заглушку.
Отметим, что поскольку окна, подобно объектам на основе STA, обладают потоковой привязкой, WndProc будет выполняться в апартаменте объекта. Чтобы избежать чрезмерного количества переключения потоков, в версии СОМ для Windows 95 предусмотрен транспортный RPC-механизм, который обходит RPC-кэш потоков и вызывает PostMessage из потока вызывающего объекта. Этот перенос возможен только в том случае, если клиент находится на том же хосте, что и объект, поскольку API-функция PostMessage не работает в сети.
Для предотвращения взаимоблокировки все типы апартаментов СОМ поддерживают реентерабельность (повторную входимость). Когда поток в апартаменте делает запрос на объект вне апартамента вызывающего объекта посредством заместителя, то могут обслуживаться и другие поступающие запросы методов, пока поток вызывающего объекта находится в ожидании ORPC-ответа па первый запрос. Без этой поддержки было бы невозможно создавать системы, основанные на совместно работающих объектах. При написании следующего кода предполагалось, что CLSID_Callback является внутрипроцессным сервером, поддерживающим модель вызывающего потока, и что CLSID_Object является классом, сконфигурированным для активации на удаленной машине:
ICallback *pcb = 0; HRESULT hr = CoCreateInstance(CLSID_Callback, 0, CLSCTX_ALL, IID_ICallback, (void**)&pcb); assert(SUCCEEDED(hr)); // callback object lives in this apt. // объект обратного вызова живет в этом апартаменте I0bject "po = 0; hr = CoCreateInstance(CLSID_Object, 0, CLSCTX_REMOTE_SERVER, IID_Iobject, (void **)&po); assert(SUCCEEDED(hr)); // object lives in different apt. // объект живет в другом апартаменте // make a call to remote object, marshaling a reference to // the callback object as an [in] parameter // делаем вызов удаленного объекта, маршалируя ссылку // на объект обратного вызова как на [in]-параметр hr = po->UseCallback(pcb); // clean up resources // очищаем ресурсы pcb->Release(); pco->Release();
На рис. 5.6 показано, что если апартамент вызывающего объекта не поддерживает реентерабельность, то следующая реализация метода UseCallback вызовет взаимоблокировку:
STDMETHODIMP Object::UseCallback(ICallback *pcb) { HRESULT hr = pcb->GetBackToCallersApartment(); assert(SUCCEEDED(hr)); return S_OK;
Напомним, что когда [in]- параметр передается через метод заместителя UseCallback, то заместитель вызывает CoMarshalInterface для маршалинга интерфейсного указателя ICallback. Поскольку указатель ссылается на объект, находящийся в апартаменте вызывающего объекта, то этот апартамент становится экспортером объектов и поэтому любые межапартаментные вызовы объекта должны обслуживаться в апартаменте вызывающего объекта. Когда заглушка интерфейса IObject демаршалирует интерфейс ICallback, она создает заместитель для передачи его реализации метода UseCallback. Этот заместитель представляет объект при промежуточном соединении с объектом обратного вызова, которое продолжается на протяжении всего времени вызова. Время существования этого заместителя/соединения может превысить время вызова, если реализация метода просто вызовет AddRef на заместитель:
STDMETHODIMP Object::UseCallback(ICallback *pcb) { if (!pcb) return E_INVALIDARG; // hold onto proxy for later use // сохраняем в заместителе для дальнейшего использования (m_pcbMyCaller = pcb)->AddRef(); return S_OK; }
Обратное соединение с апартаментом клиента будет длиться до тех пор, пока заместитель не будет освобожден объектом. Поскольку все апартаменты СОМ могут получать ORPC-запросы, объект может послать обратный вызов в апартамент клиента в любое время.
Реентерабельность реализуется для каждого типа апартаментов по-разному. Наиболее проста реализация в случае МТА, так как МТА-апартаменты не гарантируют параллелизма и не указывают, какой поток будет обслуживать заданный вызов метода. Повторный вызов может прийти в то время, когда МТА-поток заблокирован в канале в ожидании ORPC-ответа. Тогда RPC-поток, получающий повторный запрос, просто входит в МТА и обслуживает вызов своими ресурсами. Тот факт, что другой поток апартамента заблокирован и ожидании ORPC-ответа, не влияет на диспетчеризацию вызова.
В случае реализации RTA — когда поток, выполняющийся в RTA, делает межапартаментный вызов посредством заместителя, — канал уступает контроль над апартаментом, снимая блокировку всего RTA и разрешая тем самым обработку поступивших вызовов. И снова, но причине отсутствия привязки объектов к потокам в RTA, RPC-поток, получающий ORPC-запрос, может просто войти в апартамент RTA и обслужить вызов сразу после блокирования всего RTA.
Реализация реентерабельности для апартаментов STA более сложна. Поскольку STA-объекты обладают привязкой к потоку, то когда поток делает межапартаментный вызов из STA, СОМ не может разрешить потоку сделать блокирующий вызов, который предотвратил бы обработку входящих ORPC-запросов. Когда поток вызывающего объекта входит в метод канала SendReceive, чтобы послать ORPC-запрос и получить ORPC-ответ, этот канал захватывает поток вызывающего объекта и помещает его во внутренний оконный MSG-цикл. Это аналогично тому, что происходит при создании потоком модальных диалоговых окон. В обоих случаях поток вызывающего объекта вынужден обслуживать определенные классы оконных сообщений во время выполнения этой операции. В случае модальных диалоговых окон поток должен обслуживать основные оконные сообщения, чтобы разморозить основной пользовательский интерфейс. В случае межапартаментного вызова метода в СОМ поток должен обслуживать не только обычные оконные сообщения пользовательского интерфейса, но и оконные сообщения, относящиеся к поступающим ORPC-запросам. По умолчанию канал будет разрешать обслуживание всех поступающих ORPC-вызовов, пока клиентский поток ожидает ORPC-ответа. Такой режим можно настроить с помощью установки в потоке специального фильтра сообщений.
Фильтры сообщений являются уникальными для STA. Фильтр сообщений — это объект СОМ для каждого STA, который используется для решения вопроса, организовать диспетчеризацию поступающих ORPC-запросов или нет. Кроме того, фильтры сообщений используются для размещения задержанных сообщений пользовательского интерфейса, пока поток STA ожидает ORPC-ответа внутри канала.
Фильтры сообщений выставляют интерфейс IMessageFilter:
[ uuid(00000016-0000-0000-C000-000000000046),local, object ] interface IMessageFilter : IUnknown { typedef struct tagINTERFACEINFO { IUnknown *pUnk; // which object? // чей объект? IID iid; // which interface? // чей интерфейс? WORD wMethod; // which method? // чей метод? } INTERFACEINFO;
// called when an incoming ORPC request arrives in an STA // вызывается, когда входящий ORPC-запрос поступает в STA DWORD HandleInComingCall( [in] DWORD dwCallType, [in] HTA5K dwThreadIdCaller, [in] DWORD dwTickCount, [in] INTERFACEINFO *pInterfaceInfo );
// called when another STA rejects or postpones an ORPC request // вызывается, когда другой STA отклоняет или откладывает ORPC-запрос
DWORD RetryRejectedCall( [in] HTASK dwThreadIdCallee, [in] DWORD dwTickCount, [in] DWORD dwRejectType );
// called when a non-COM MSG arrives while the thread is // awaiting an ORPC response // вызывается, когда поступает не СОМ'овское MSG, пока // поток ожидает ORPC-ответа DWORD MessagePending( [in] HTASK dwThreadIdCallee, [in] DWORD dwTickCount, [in] DWORD dwPendingType ); }
Для установки специального фильтра сообщений в СОМ существует API-функция CoRegisterMessageFilter:
HRESULT CoRegisterMessageFilter([in] IMessageFilter *pmfNew, [out] IMessageFilter **ppmfOld);
CoRegisterMessageFilter связывает указанный фильтр сообщений с текущим STA. Предыдущий фильтр сообщений возвращается для того, чтобы вызывающий объект мог восстановить его в дальнейшем.
Когда бы входящий ORPC-запрос ни пришел в STA-поток, вызывается метод фильтра сообщений HandleIncomingCall, который дает апартаменту возможность принять, отклонить или отложить вызов. HandleIncomingCall используется как реентерабельными, так и нереентерабельными вызовами. Параметр dwCallType показывает, какой тип вызова был получен:
typedef enum tagCALLTYPE { CALLTYPE_TOPLEVEL, // STA not in outbound call // STA не в исходящем вызове CALLTYPE_NESTED, // callback on behalf of outbound call // обратный вызов от имени исходящего вызова CALLTYPE_ASYNC, // asynchronous call // асинхронный вызов CALLTYPE_TOPLEVEL_CALLPENDING, // new call while waiting // новый вызов во время ожидания CALLTYPE_ASYNC_CALLPENDING // async call while waiting // асинхронный вызов во время ожидания } CALLTYPE;
Вложенный (реентерабельный) вызов и незаконченный (нереентерабельный) вызов верхнего уровня происходят, пока поток ожидает ORPC-ответа в канале. Вызовы верхнего уровня происходят в тех случаях, когда в апартаменте нет активных вызовом.
В СОМ определено перечисление, которое должна возвратить реализация HandleIncomingCall, чтобы указать, что произошло с вызовом:
typedef enum tagSERVERCALL { SERVERCALL_ISHANDLED, // accept call and forward to stub // принимаем вызов и направляем его заглушке SERVERCALL_REJECTED, // tell caller that call is rejected // сообщаем вызывающему объекту, что вызов отклонен SERVERCALL RETRYLATER // tell caller that call is postponed // сообщаем вызывающему объекту, что вызов отложен } SERVERCALL;
Если функция HandleIncomingCall фильтра сообщений возвращает SERVERCALL_ISHANDLED, то вызов будет направлен в интерфейсную заглушку для демаршалинга. Фильтр сообщений, принятый по умолчанию, всегда возвращает SERVERCALL_ISHANDLED. Если HandleIncomingCall возвращает SERVERCALL_REJECTED или SERVERCALL_RETRYLATER, то фильтр сообщений вызывающего объекта будет информирован о положении вызова и ORPC-запрос будет отклонен.
Когда фильтр сообщений отвергает или откладывает вызов, то фильтр сообщений вызывающего объекта информируется об этом с помощью метода RetryRejectedCall. Этот вызов происходит в контексте апартамента вызывающего объекта, и реализация метода RetryRejectedCall фильтра сообщений может решать, повторять ли отложенный вызов. Параметр dwRejectType указывает, был ли вызов отклонен или отложен. Реализация канала вызывающего объекта будет решать, какое действие предпринять, в зависимости от значения, возвращенного RetryRejectedCall. Если RetryRejectedCall возвращает -1, то канал предположит, что повторных попыток не требуется, и немедленно заставит заместитель возвратить HRESULT, равный RPC_E_CALL_REJECTED. По умолчанию фильтр сообщений всегда возвращает -1. Любое другое значение, возвращаемое методом RetryRejectedCall, интерпретируется как число миллисекунд, через которое следует повторить вызов.
Поскольку это согласование осуществляется внутри канала, то не требуется повторного ORPC-запроса со стороны заместителя. В сущности, интерфейсные маршалеры не имеют ни малейшего понятия о процессах в фильтре сообщений.
Когда размещенный в STA поток блокирован в канале в ожидании ORPC-ответа, то не связанные с СОМ оконные сообщения могут поступать в MSG-очередь потока. Когда это происходит, то фильтр сообщений STA уведомляется об этом посредством метода MessagePending. Фильтр сообщений, принятый по умолчанию, разрешает диспетчеризацию некоторых оконных сообщений, чтобы предотвратить замораживание всей оконной системы. Тем не менее, действия ввода (например, щелчки мышью, нажатие клавиш) не учитываются, чтобы конечный пользователь не начал новое взаимодействие с системой. Как уже отмечалось ранее, фильтры сообщений существуют только в апартаментах STA и не поддерживаются в RTA или МТА. Фильтры сообщений лишь обеспечивают лучшую интеграцию СОМ с потоками, обрабатывающими события от пользовательского интерфейса. Из этого следует, что все эти потоки должны выполняться в однопотоковых апартаментах. Большинство потоков, обрабатывающих события от пользовательского интерфейса, захотят установить специальный фильтр сообщений, чтобы убедиться в том, что входящие запросы не обслуживаются, пока приложение находится в такой критической фазе, в которой реентерабельность может привести к семантическим ошибкам. Фильтры сообщений не следует применять в качестве универсального механизма для управления потоками. Реализация фильтров сообщений печально известна своей неэффективностью в тех случаях, когда вызовы отклоняются или откладываются. Это делает фильтры сообщений малоприспособленными в качестве механизма для управления потоками в высокопроизводительных приложениях.
1
Во время написания этого текста СОМ не поддерживал ни одного нереентерабельного типа апартаментов. Возможно, что будущие версии СОМ смогут предусмотреть новые типы апартаментов, не поддерживающие реентерабельность.
2
По недоразумению широко распространено мнение, что для обеспечения двухсторонней связи или обратных вызовов требуются точки стыковки (Connection Points). Как описывается в главе 7, точки стыковки необходимы только для поддержки программ обработки событий в Visual Basic и для сред подготовки сценариев.
Управление жизненным циклом и маршалинг
Ранее в этой главе обсуждались взаимоотношения между администратором заглушек и объектом. Администратор заглушек создается при первом вызове CoMarshalInterface для определенного идентифицированного объекта. Администратор заглушек хранит неосвобожденные ссылки на тот объект, который он предсиавляет, и существует до тех пор, пока остается хотя бы одна неосвобожденная внешняя ссылка на заглушку. Эти внешние ссылки обычно являются заместителями, хотя учитываются и маршалированные объектные ссылки, так как они могут представлять заместители. Когда все внешние ссылки на администратор заглушек уничтожены, он самоуничтожается и освобождает все хранящиеся в нем ссылки на текущий объект. Такое поведение по умолчанию в точности имитирует обычную внутрипроцессную семантику AddRef и Release. Многие объекты не имеют никаких специальных требований относительно жизненного цикла и целиком удовлетворяются этой схемой. Некоторые объекты предпочитают дифференцировать взаимоотношения между внешними ссылками, администратором заглушек и объектом. К счастью, СОМ предоставляет администратору заглушек на время жизненного цикла достаточно приемов работы, которые позволяют реализовывать различные стратегии. Для того чтобы понять, как организован жизненный цикл заглушки, необходимо в первую очередь исследовать алгоритм распределенной сборки мусора (garbage collection) СОМ.
Когда администратор заглушек создан, то идентификатор объекта (OID) регистрируется в распределенном сборщике мусора СОМ, который в настоящее время реализован в службе распознавателя идентификаторов экспортера объектов (OXID Resolver - OR). OR отслеживает, какие идентификаторы объектов экспортируются из каких апартаментов локальной хост-машины. Когда создается администратор заместителей, то CoUnmarshalInterface информирует локальный OR о том, что в апартамент импортируется объектная ссылка. Это означает, что локальный OR также знает, какие OID импортированы в каждый апартамент локальной хост-машины. Если определенный OID импортирован на хост-машину впервые, OR импортирующего хоста устанавливает отношения тестового опроса (ping relationship) с экспортирующим хостом.
Тогда OR импортирующей стороны будет передавать периодические тестовые сообщения через RPC, подтверждая тем самым, что импортирующая хост-машина все еще функционирует и доступна в сети. Текущая реализация посылает такое сообщение один раз в две минуты. Если за последний тестовый интервал (ping interval) не было импортировано никаких дополнительных OID, то посылается простое уведомление. Если же были импортированы новые ссылки или освобождены уже существующие, то посылается более сложное сообщение, показывающее разницу между прошлым и нынешним наборами хранимых ссылок.
В рамках реализации СОМ для Windows NT 4.0 установлено, что если три последовательных тестовых интервала (шесть минут) пройдут без получения уведомления от определенного хоста, то OR будет считать, что хост либо сам вышел из строя, либо недоступен из-за сбоя и сети. В этом случае OR проинформирует всех администраторов заглушек, импортированных ныне отказавшим хостом, что все неосвобожденные ссылки теперь неверны и подлежат освобождению. Если какой-то определенный объект использовался исключительно клиентами ныне мертвого хоста, то в администраторе заглушек более не останется неосвобожденных ссылок и он самоликвидируется, что, в свою очередь, освободит ссылки СОМ на данный объект.
В предыдущем сценарии описывалось, что происходит в том случае, когда хост-машина становится недоступной в сети. Больший интерес представляет сценарий, когда происходит преждевременный выход процесса, в котором остались неосвобожденные заместители. Если процесс закрывается, не вызвав CoUninitialize нужное число раз (например, процесс аварийно завершился), то у библиотеки СОМ нет возможности восстановить утерянные ссылки. Когда это происходит, локальный OR обнаружит гибель процесса и удалит импортированные им ссылки из последующих передаваемых сообщений, что в конце концов заставит OR-экспортера освободить хранящиеся там ссылки. Если в процессе хранились импортированные ссылки на объекты локальной машины, то они могут быть освобождены вскоре после установления смерти клиента.
Распределенный сборщик мусора СОМ иногда критикуют за неэффективность. На самом деле, если объектам нужно надежно установить жизнеспособность клиента, то СОМ сделает это значительно более эффективно, чем модели, специфические для приложения. Дело в том, что сборщик мусора СОМ может агрегировать сохраненные сообщения для всех ссылок на определенной машине в единое периодическое сообщение. Модели же, специфические для приложений, не имеют столь полной информации, и от них можно ожидать единого сообщения для каждого приложения, но не для каждой хост-машины. Для тех сценариев, когда сборщик мусора СОМ действительно влияет на производительность, тестовый опрос для конкретной заглушки может быть отключен с помощью флага MSHLFLAGS_NOPING. Тем не менее, стандартное поведение сборщика мусора пригодно для большинства приложений и превосходит множество специальных моделей, специфических для приложений.
Администратор заглушек следит за тем, сколько внешних ссылок еще не выполнено. Когда заглушка создана, этот счетчик устанавливается в нуль. Если сделан вызов CoMarshalInterface с флагом MSHLFLAGS_NORMAL, этот счетчик увеличивается на некоторое число n, которое записано в маршалированной объектной ссылке. Администратор заместителей, демаршалируя ссылку, добавляет n к своему счетчику хранимых ссылок. Если CoMarshalInterface вызван для администратора заместителей для передачи копии ссылки в другой апартамент, то администратор заместителей может выделить некоторое количество ссылок для инициализации второго заместителя. Если в заместителе осталась только одна ссылка, он должен вернуться в администратор заглушек для запроса дополнительных ссылок.
Часто бывает полезно сохранить маршалированные интерфейсные ссылки в центральной области, доступной для одного или более клиентов. Классическим примером этого является Таблица Исполняемых Объектов (Running Object Table), используемая некоторыми реализациями моникеров. Если бы маршалированные интерфейсные указатели должны были создаваться с использованием флага MSHLFLAGS_NORMAL, то только один клиент смог бы когда-либо демаршалировать объектную ссылку.
Если предполагается, что объектную ссылку будут демаршалировать несколько клиентов, то ссылка должна маршалироваться с применением либо MSHLFLAGS_TABLESTRONG, либо MSHLFLAGS_TABLEWEAK. В обоих случаях маршалированная объектная ссылка может быть демаршалирована несколько раз.
Разница между сильным (strong) и слабым (weak) табличными маршалингами заключается во взаимоотношениях между маршалированой объектной ссылкой и администратором заглушек. Когда маршалированная объектная ссылка создается с флагом MSHLFLAGS_TABLEWEAK, то внешний счетчик ссылок в администраторе заглушек не увеличивается. Это означает, что маршалированная объектная ссылка будет содержать нуль ссылок, и каждому администратору заместителей для получения одной или более внешних ссылок придется связываться с администратором заглушек. Маршалированная с помощью слабой таблицы ссылка не представляет сосчитанную внешнюю ссылку на администратор заглушек. Поэтому, когда последний администратор заместителей отсоединится от администратора заглушек, администратор заглушек самоуничтожится и, конечно, освободит все хранившиеся ссылки СОМ на объект. Если ни один администратор заместителей ни разу не свяжется с администратором заглушек, то последний останется жить в течение неопределенного времени. Отрицательной стороной является то, что неосвобожденная маршалированная объектная ссылка не заставляет оставаться в живых администратор заглушек или объект. Напротив, когда маршалированная объектная ссылка создана с применением флага MSHLFLAGS_TABLESTRONG, то есть с помощью сильной таблицы, то внешний счетчик ссылок увеличивается на единицу. Это означает, что маршалированная объектная ссылка представляет сосчитанную внешнюю ссылку на администратор заглушек. Как и в случае маршалинга по слабой таблице, каждому администратору заместителей понадобится связаться с администратором заглушек, чтобы получить одну или более дополнительных внешних ссылок. Поскольку маршалированная по сильной таблице ссылка представляет внешний счетчик ссылок на администратор заглушек, то при отсоединении последнего администратора заместителей от администратора заглушек он не будет самоуничтожаться и фактически продолжит хранение ссылок СОМ на объект.
Отрицательная сторона маршалинга по сильной таблице заключается в том, что неосвобожденная маршалированная объектная ссылка влияет на жизненный цикл администратора заглушек или объекта. Это означает, что должен существовать какой-нибудь механизм для освобождения ссылок, хранящихся в объектной ссылке, маршалированной по сильной таблице. В СОМ предусмотрена API-функция CoReleaseMarshalData, которая информирует администратор заглушек о том, что маршалированная объектная ссылка уничтожается:
HRESULT CoReleaseMarshalData([in] IStream *pStm);
Подобно CoUnmarshalInterface, CoReleaseMarshalData принимает интерфейсный указатель IStream на маршалированную объектную ссылку. Если таблица маршалинга более не нужна, для ее аннулирования следует вызвать функцию CoReleaseMarshalData. Если по некоторым причинам нормально маршалированная объектная ссылка не будет демаршалироваться с помощью CoUnmarshalInterface, то должна вызываться также функция CoReleaseMarshalData.
Разработчики объектов могут обратиться к счетчику внешних ссылок администратора заглушек вручную, чтобы убедиться в том, что администратор заглушек продолжает жить во время критических фаз жизненного цикла объекта. В СОМ предусмотрена функция CoLockObjectExternal, которая увеличивает или уменьшает на единицу счетчик внешних ссылок администратора заглушек:
HRESULT CoLockObjectExternal([in] IUnknown *pUnkObject, [in] BOOL bLock, [in] BOOL bLastUnlockKillsStub);
Первый параметр CoLockObjectExternal должен указывать на действительный объект, он не может указывать на заместитель. Второй параметр, bLock, показывает, увеличивать или уменьшать на единицу счетчик внешних ссылок администратора заглушек. Третий параметр показывает, нужно или нет уничтожать администратор заглушек, если этот вызов удаляет последнюю внешнюю ссылку. Чтобы понять, для чего необходима функция CoLockObjectExternal, рассмотрим объект, который контролирует некоторое аппаратное устройство и зарегистрирован в таблице исполняемых объектов (Running Object Table) с использованием маршалинга по слабой таблице.
Пока объект осуществляет активный контроль, он хочет быть уверенным, что его администратор заглушек действительно существует, чтобы новые клиенты могли соединяться с объектом для проверки состояния устройства. Если же, однако, объект не осуществляет активный контроль, то он мог бы пожелать, чтобы администратор заглушек исчез, если нет соединенных с ним неосвобожденных заместителей. Для реализации такой функциональной возможности объект должен иметь метод, который начинает осуществлять контроль:
STDMETHODIMP Monitor::StartMonitoring(void) { // ensure that stub manager/object stays alive // убеждаемся, что администратор заглушек/объект остается жив
HRESULT hr = CoLockObjectExternal(this, TRUE, FALSE);
// start hardware monitoring // начинаем контроль за аппаратным устройством
if (SUCCEEDED(hr)) hr = this->EnableHardwareProbe();
return hr; }
а также другой метод, который предписывает объекту закончить активный контроль:
STDMETHODIMP Monitor::StopMonitoring(void) { // stop hardware monitoring // прекращаем контроль за устройством
this->DisableHardwareProbe();
// allow stub manager/object to die when no clients exist // разрешаем администратору заглушек/объекту прекратить // существование, когда нет клиентов
hr = CoLockObjectExternal(this, FALSE, TRUE);
return hr; }
Если принять, что объект был изначально маршалирован с помощью слабой таблицы маршалинга, то данный код обеспечивает жизнь администратора заглушек и объекта до тех пор, пока хотя бы один неосвобожденный заместитель или объект активно контролируют основное устройство.
Кроме предоставления разработчикам объектов возможности устанавливать счетчик внешних ссылок в администраторе заглушек, СОМ также позволяет разработчикам явно уничтожать администратор заглушек, независимо от числа неосвобожденных объектных ссылок. В СОМ предусмотрена API-функция CoDisconnectObject, которая находит администратор заглушек объекта и уничтожает его, отсоединяя все существующие в данный момент заместители:
HRESULT CoDisconnectObject( [in] Unknown * pUnkObject, // ptr to object // указатель на объект [in] DWORD dwReserved // reserved, must be zero // зарезервировано, должно равняться нулю );
Подобно CoLockObjectExternal, функция CoDisconnectObject должна вызываться из процесса действующего объекта и не может быть вызвана на объект. Для того чтобы применить CoDisconnectObject к показанному выше объекту контроля за устройством, рассмотрим, что произошло бы, если бы состояние объекта было испорчено. Для предотвращения дополнительных вызовов методов объекта, которые могут возвращать ошибочные результаты, объект мог бы вызвать CoDisconnectObject, чтобы резко отсоединить все существующие заместители:
STDMETHODIMP Monitor::GetSample(/*[out]*/ SAMPLEDATA *ps) { HRESULT hr = this->GetSampleFromProbe(ps); if (FAILED(hr)) // probe or object may be corrupted // образец или объект могут быть испорчены CoDisconnectObject(this, 0); return hr; }
Функция CoDisconnectObject используется также в случаях, когда процесс хочет отключиться, хотя один или более его объектов могут иметь неосвобожденные заместители. При явном вызове CoDisconnectObject до уничтожения любых объектов, которые могут иметь оставшиеся заместители, нет риска, что исходящие ORPC-запросы будут обслуживаться после того, как объект уже уничтожен. Если бы входящий ORPC-запрос должен был бы поступить после того, как объект уже уничтожен, но администратор заглушек еще жив, то небрежность привела бы к вызову интерфейсной заглушкой соответствующего метода из участка памяти, ранее известного как данный объект. Это вызвало бы лишние неприятности, связанные с тщетными усилиями по отладке.
Обе функции - CoLockObjectExternal и CoDisconnectObject — могут быть использованы разработчиком объектов для манипулирования администратором заглушек. Часто бывает полезно знать, есть ли в администраторе заглушек в наличии какие-либо заместители или объектные ссылки, маршалированные по сильной таблице (strong marshals). Для информирования объектов о том, что имеются неосвобожденные внешние ссылки на администратор заглушек, в СОМ определен интерфейс IExternalConnection, который может быть экспортирован объектами:
[ uuid(00000019-0000-0000-C000-000000000046), object, local ] interface IExternalConnection : IUnknown { DWORD AddConnection( [in] DWORD extconn, // type of reference // тип ссылки [in] DWORD reserved // reserved, must be zero // зарезервировано, должно быть равно нулю );
DWORD ReleaseConnection( [in] DWORD extconn, // type of reference // тип ссылки [in] DWORD reserved, // reserved, must be zero // зарезервировано, должно быть равно нулю [in] BOOL fLastReleaseCloses // should kill stub? // нужно ли убить заглушку? ); }
При первом подсоединении администратора заглушек к объекту он спрашивает объект, желает ли тот, чтобы его уведомляли о создании или уничтожении внешних ссылок. Он делает это посредством запроса интерфейса IExternalConnection для QueryInterface. Если объект не реализует IExternalConnection, то администратор заглушек будет использовать свой собственный счетчик ссылок при решении вопроса, когда уничтожать администратор заглушек. Если же объект предпочтет реализовать IExternalConnection, то в этом случае администратор заглушек будет жить до тех пор, пока объект явно не уничтожит его путем вызова CoDisconnectObject.
Ожидается, что в объектах, которые реализуют IExternalConnection, поддерживается счетчик блокировок, записывающий число вызовов функций AddConnection и ReleaseConnection. Для большей эффективности СОМ не вызывает AddConnection всякий раз, когда создается заместитель. Это означает, что если объект поддерживает счетчик блокировок, основанный на вызовах функций AddConnection и ReleaseConnection, то этот счетчик блокировок объекта не будет точно отражать число существующих в данный момент объектных ссылок. В то же время СОМ гарантирует, что в том случае, когда счетчик блокировок не равен пулю, существует хотя бы одна неосвобожденная ссылка, а если счетчик блокировок равен нулю, то не существует ни одной неосвобожденной ссылки. Вызовы функции CoLockObjectExternal также будут влиять на этот счетчик. Эта информация особенно полезна для тех объектов, которые заботятся о существовании внешних клиентов. Предположим для примера, что представленный ранее объект для контроля над аппаратным устройством порождает подпроцесс для выполнения фоновой регистрации выборочных данных. Также допустим, что если регистрация произойдет в тот момент, когда объект осуществляет активный контроль данных или, напротив, находится под контролем внешних клиентов, то может возникнуть ошибка выборки.
Для предотвращения этой ситуации регистрационный поток мог бы проверять счетчик блокировок, поддерживаемый объектной реализацией IExternalConnection и осуществлять операцию регистрации только тогда, когда не существует внешних ссылок. Это предполагает, что объект реализует IExternalConnection следующим образом:
class Monitor : public IExternalConnection, public IMonitor { LONG m_cRef; // normal COM reference count // обычный счетчик ссылок СОМ LONG m_cExtRef; // external reference count // счетчик внешних ссылок Monitor(void) : m_cRef(0), m_cExtRef(0) { ... }
STDMETHODIMP_(DWORD) AddConnection(DWORD extconn, DWORD) { if (extconn & EXTCONN_STRONG) // must check for this bit // нужно проверить этот бит return InterlockedIncrement(&m_cExtRef); }
STDMETHODIMP_(DWORD) ReleaseConnection(DWORD extconn, DWORD, BOOL bLastUnlockKillsStub) { DWORD res = 0; if (extconn & EXTCONN_STRONG) { // must check for this bit // нужно проверить этот бит res = InterlockedDecrement(&m_cExtRef); if (res == 0 && bLastUnlockKillsStub) CoDisconnectObject(this, 0); } return res; } } : : : : : : }
Получив эту реализацию, подпрограмма потока могла бы проверить состояние объекта и решить, выполнять или нет операцию регистрации, основываясь на уровне активности объекта:
DWORD WINAPI ThreadProc(void *pv) { // assume ptr to real object is passed to CreateThread // пусть указатель на действительный объект передается в CreateThread
Monitor *pm = (Monitor*)pv; while (1) { // sleep for 10 seconds // ожидаем 10 секунд Sleep(1OOOO); // if object is not in use, perform a log operation // если объект не используется, то выполняем операцию регистрации if (pm->m_cExtRef == 0) pm->TryToLogSampleData(); } return 0; }
Если принять, что метод объекта TryToLogSampleData корректно поддерживает параллелизм, то данная поточная процедура будет регистрировать данные только при условии, что объект не используется внешними клиентами или не осуществляет активный контроль (напомним, что при контроле объект увеличивает счетчик внешних ссылок посредством CoLockObjectExternal).Хотя данный пример может показаться несколько запутанным, имеются случаи, когда отслеживание внешних ссылок является решающим для обеспечения правильности операции. Один классический пример описан в главе 6 и относится к регистрации объектов класса на внепроцессных серверах.
1
В действительности локальный OR ждет в течение короткого промежутка времени, чтобы предоставить шанс демаршализоваться любым оставшимся маршализованным объектным ссылкам, созданным покойным клиентом.
Вспомогательные средства для внутрипроцессного маршалинга
Хотя фрагменты кода для WritePtr и ReadPtr из предыдущего раздела достаточно просто реализовать, большинство явных вызовов CoMarshalInterface будут использоваться для передачи интерфейсного указателя от одного потока к другому в том же самом процессе. Для упрощения этой задачи в СОМ предусмотрены две оберточные функции (wrapper functions), которые реализуют нужный стандартный код вокруг CoMarshalInterface и CoUnmarshalInterface. API-функция СОМ CoMarshalInterThreadInterfaceInStream
HRESULT CoMarshalInterThreadInterfaceInStream( [in] REFIID riid, [in, iid_is(riid)] IUnknown *pItf, [out] IStream **ppStm );
обеспечивает простую обертку вокруг CreateStreamOnHGlobal и CoMarshalInterface, как показано ниже:
// from OLE32.DLL (approx.) // из OLE32.DLL (приблизительно) HRESULT CoMarshalInterThreadInterfaceInStream( REFIID riid, IUnknown *pItf, IStream **ppStm) { HRESULT hr = CreateStreamOnHGlobal(0, TRUE, ppStm); if (SUCCEEDED(hr)) hr = CoMarshalInterface(*ppStm, riid, pItf, MSHCTX_INPROC, 0, MSHLFLAGS_NORMAL); return hr; }
В СОМ предусмотрена также обертка вокруг CoUnmarshalInterface:
HRESULT CoGetInterfaceAndReleaseStream( [in] IStream *pStm, [in] REFIID riid, [out, iid_is(riid)] void **ppv );
которая является очень тонкой оберткой вокруг CoUnmarshalInterface:
// from OLE32.DLL (approx.) // из OLE32.DLL (приблизительно) HRESULT CoGetInterfaceAndReleaseStream( IStream *pStm, REFIID riid, void **ppv) { HRESULT hr = CoUnmarshalInterface(pStm, riid, ppv); pStm->Release(); return hr; }
Ни одна из этих двух функций не обеспечивает каких-либо особых возможностей, но в некоторых случаях удобнее использовать их, а не низкоуровневые аналоги.
Следующий фрагмент кода мог бы быть применен для передачи интерфейсного указателя в другой апартамент того же процесса с использованием глобальной переменной для хранения промежуточной маршалированной объектной ссылки:
HRESULT WritePtrToGlobalVarable(IRacer *pRacer) { // where to write the marshaled ptr // куда записывать маршалированный указатель extern IStream *g_pStmPtr; // thread synchronization for read/write // синхронизация потока для чтения/записи extern HANDLE g_heventWritten; // write marshaled object reference to global variable // записываем маршалированную объектную ссыпку // в глобальную переменную HRESULT hr = CoMarshalInterThreadInterfaceInStream( IID_IRacer, pRacer, &g_pStmPtr); // signal other thread that ptr is now available // подаем сигнал другому процессу о том, что указатель // теперь доступен SetEvent (g_heventWritten); return hr; }
Соответствующий код будет корректно демаршалировать объектную ссылку в апартамент вызывающего объекта при условии, что он находится и том же самом процессе:
HRESULT ReadPtrFromGlobalVariable(IRacer * &rpRacer) { // where to write the marshaled ptr // куда записывать маршалированный указатель extern IStream *g_pStmPtr; // thread synchronization for read/write // синхронизация потока для чтения/записи extern HANDLE g_heventWritten; // wait for other thread to signal that ptr is available // ожидаем другой поток, чтобы дать сигнал о том. что // указатель доступен WaitForSingleObject(g_heventWritten, INFINITE); // read marshaled object reference from global variable // читаем маршалированную объектную ссылку из глобальной переменной HRESULT hr = CoGetInterfaceAndReleaseStream( g_pStmPtr, IID_IRacer. (void**) &rpRacer); // MSHLFLAGS_NORMAL means no more unmarshals are legal // MSHLFLAGS_NORMAL означает, что больше не может быть // извлечено никаких демаршалированных указателей g_pStmPtr = 0; return hr; }
Данный код требуется при передаче указателя от одного апартамента к другому. Отметим, что при передаче указателя от потока, выполняющегося в МТА или RTA, к другому потоку, выполняющемуся в том же апартаменте, не требуется никаких вызовов маршалинга. Тем не менее, обычной практикой для программы записи (writer) интерфейсного указателя является вызов AddRef до передачи копии в поток программы считывания (reader). Когда поток читающей программы выполняется с использованием указателя, ему, конечно, необходимо будет вызвать Release.
Отметим, что в приведенном коде программа считывания устанавливает в нуль глобальную переменную g_pStmPtr после демаршалинга. Это сделано потому, что объектная ссылка была маршалирована с флагом MSHLFLAGS_NORMAL и может быть демаршалирована только один раз. Во многих сценариях это не является проблемой. В некоторых других сценариях, однако, желательно маршалировать указатель из одного потока и иметь несколько рабочих потоков, в которых можно демаршалировать интерфейсный указатель по мере необходимости.
Если все рабочие потоки выполняются в МТА, то это не является проблемой, поскольку нужно выполнить демаршалинг только в одном потоке — от имени всех потоков, выполняющихся в МТА. Если, однако, рабочие потоки выполняются в произвольных апартаментах, то этот подход не сработает, поскольку тогда придется независимо демаршалировать объектную ссылку в каждом рабочем потоке. Большинство разработчиков в этом случае обращаются к флагу MSHLFLAGS_TABLESTRONG, надеясь на однократный маршалинг и столько демаршалингов, сколько необходимо (по одному разу на апартамент). К сожалению, табличный маршалинг (в отличие от обычного маршалинга) не поддерживается в случае, если исходный указатель является заместителем, что случается часто, особенно в распределенных приложениях. Для разрешения этой проблемы в выпуске СОМ Service Pack 3 под Windows NT 4.0 вводится Глобальная интерфейсная таблипа (Global Interface Table - GIT).
GIT является оптимизацией CoMarshalInterface / CoUnmarshalInterface, которая допускает обращение к интерфейсным указателям из всех апартаментов процесса. Внутри СОМ реализуется одна GIT на процесс. GIT содержит маршалированные интерфейсные указатели, которые могут быть эффективно демаршалированы несколько раз внутри одного и того же процесса. Это семантически эквивалентно использованию табличного маршалинга, однако GIT можно использовать как для объектов, так и для заместителей. GIT выставляет интерфейс IGlobalInterfaceTable:
[uuid(00000146-0000-0000-C000-000000000046), object, local ] interface IGlobalInterfaceTable : IUnknown { // marshal an Interface into the GIT // маршалируем интерфейс в GIT HRESULT RegisterInterfaceInGlobal ( [in, iid_is(riid)] IUnknown *pItf, [in] REFIID riid, [out] DWORD *pdwCookie); // destroy the marshaled object reference // уничтожаем маршалированную объектную ссылку HRESULT RevokeInterfaceFromGlobal ( [in] DWORD dwCookle); // unmarshal an interface from the GIT // демаршалируем интерфейс из GIT HRESULT GetInterfaceFromGlobal ( [in] DWORD dwCookie, [in] REFIID riid, [out, iid_is(riid)] void **ppv); }
Клиенты получают доступ к GIT для своего процесса, вызывая CocreateInstance с использованием класса CLSID_StdGlobalInterfaceTable. Каждый вызов CoCreateInstance с применением этого CLSID возвращает указатель на одну и ту же GIT в процессе. Так же как к интерфейсу IStream, возвращенному CoMarshalInterThreadInterfaceInStream, к интерфейсным указателям на GIT можно обратиться из любого апартамента без обязательного маршалинга.
Для того чтобы сделать интерфейсный указатель доступным для всех апартаментов процесса, апартамент, содержащий этот интерфейсный указатель, должен зарегистрировать его в GIT путем вызова метода RegisterInterfaceInGlobal. GIT вернет вызывающей программе DWORD, который представляет глобальный указатель для всех апартаментов процесса. Этот DWORD может быть использован из любого апартамента процесса для демаршалинга нового заместителя путем вызова метода GetInterfaceFromGlobal. Этот же DWORD можно использовать для повторного демаршалинга заместителей до тех пор, пока вызов RevokeInterfaceFromGlobal не объявит глобальный интерфейсный указатель недействительным. Приложения, использующие эту глобальную интерфейсную таблицу (GIT), обычно связывают один интерфейсный указатель на весь процесс при запуске:
IGlobalInterfaceTable *g_pGIT = 0; HRESULT Init0nce(void) { assert(g_pGIT == 0); return CoCreateInstance(CLSID_StdGlobalInterfaceTable, 0, CLSCDX_INPROC_5ERVER, IID_IGlobalInterfaceTable, (void**)&g_pGIT); }
Когда глобальная интерфейсная таблица является доступной, передача интерфейсного указателя в другой апартамент сводится к простой регистрации указателя в глобальной интерфейсной таблице:
HRESULT WritePtrToGlobalVariable(IRacer *pRacer) { // where to write the marshaled ptr // куда записывать маршалированный указатель extern DWORD g_dwCookie; // thread synchronization // синхронизация потока extern HANDLE g_heventWritten; // write marshaled object reference to global variable // записываем маршалированную объектную ссыпку в глобальную переменную HRESULT hr = g_pGIT->RegisterInterfaceInGlobal( pRacer, IID_IRacer, &g_dwCookie); // signal other thread that ptr is now available // сообщаем другому потоку о доступности указателя SetEvent(g_heventWritten); return hr; }
Следующий код корректно демаршалирует объектную ссылку и может вызываться из любого апартамента одного и того же процесса:
HRESULT ReadPtrFromGlobalVariable(IRacer * &rpRacer, bool bLastUnmarshal) { // where to write the marshaled ptr // куда записывать маршалированный указатель extern DWORD g_dwCookie; // thread synchronization // синхронизация потока extern HANDLE g_heventWritten; // wait for other thread to signal that ptr is available // ожидаем другой поток, чтобы сигнализировать о доступности указателя WaitForSingleObject(g_heventWritten, INFINITE); // read marshaled object reference from global variable // читаем маршалированную объектную ссылку из глобальной переменной HRESULT hr = g_pGIT->GetInterfaceFromGlobal( g_dwCookie, IID_IRacer, (void**)&rpRacer); // if we are the last to unmarshal, revoke the pointer // если мы поспедние в очереди на демаршапинг, то // аннулируем указатель if (bLastUnmarshal) g_pGIT->RevokeInterfaceFromGlobal(g_dwCookie); return hr; }
Отметим принципиальную разницу между этими фрагментами кода и примерами с применением CoMarshalInterThreadInterfaceInStream. Она состоит в том, что код, основанный на GIT, способен демаршалировать более чем один заместитель.
1 Может показаться странным, что глобальная переменная является интерфейсным указателем, который инициализирован в апартаменте программы записи, а используется из апартамента программы считывания. Об этой противоречивости упоминается в документации по CoMarshalInterThreadInterfaceInStream, где формулируется, что к результирующему интерфейсному указателю IStream можно обратиться из любого апартамента в процессе.
Активация и SCM
Диспетчер управления COM-сервисами (Service Control Manager — SCM) обеспечивает связь между CLSID и серверными процессами в реестре. Это позволяет SCM запускать серверный процесс при поступлении клиентских запросов на активацию. Если предположить, что код для класса будет упакован как образ процесса (ЕХЕ), а не как DLL, то достаточно использовать ключ реестра LocalServer32 вместо InprocServer32, как показано в следующем примере:
[HKCR\CLSID\{27EE6A26-DF65-11d0-8C5F-0080C73925BA}] @="Gorillaquot;
[HKCR\CLSID\{27EE6A26-DF65-11d0-8C5F-0080C73925BA}\LocalServer32] @="C:\ServerOfTheApes.exe"
Ожидается, что внепроцессный сервер установит эти ключи во время самоинсталляции. В отличие от своих внутрипроцессных аналогов, внепроцессные серверы не экспортируют известные библиотеки DllRegisterServer и DllUnregisterServer. Вместо этого внепроцессный сервер должен проверить командную строку на наличие известных ключей /RegServer и /UnregServer
. Имея вышеуказанные элементы реестра, SCM начнет новый серверный процесс с использованием файла ServerOfTheApes.ехе, при первом запросе на активацию класса Gorilla. После этого извещение SCM о том, какие классы фактически являются доступными из нового процесса, будет обязанностью серверного процесса.
Как уже рассматривалось в главе 3, процессы могут контактировать с SCM для связывания ссылок на объекты класса, экземпляров класса и постоянных экземпляров. Для осуществления этого в COM предусмотрены три функции активации (CoGetClassObject, CoCreateInstanceEx и CoGetInstanceFromFile). Они, как и высокоуровневые моникеры, предназначены для того, чтобы скрыть детали реализации каждой стратегии связывания. В каждой из этих трех стратегий активации для вызова объекта к жизни используется объект класса. Как уже рассматривалось в главе 3, когда активация объекта осуществляется внутри процесса, DLL класса загружается самой COM, а для выборки соответствующих объектов класса используется известная точка входа DllGetClassObject.
Однако пока не рассматривалось, как объекты могут быть активированы через границы процессов.
Процесс становится серверным процессом для определенного класса после явной саморегистрации с помощью SCM. После такой регистрации любые активационные запросы класса, для которых необходима внепроцессная активация, будут отосланы к зарегистрированному серверному процессу . Серверные процессы саморегистрируются с помощью SCM API-функции CoRegisterClassObject:
HRESULT CoRegisterClassObject( [in] REFCLSID rclsid, // which class? // какой класс? [in] IUnknown *pUnkClassObject, // ptr to class object // указатель на объект класса [in] DWORD dwClsCtx, // locality // локализация [in] DWORD dwRegCls, // activation flags // флаги активации [out] DWORD *pdwReg); // association ID // ID связи
При вызове CoRegisterClassObject библиотека COM сохраняет ссылку на объект класса, указанную в качестве второго параметра, и связывает объект класса с его CLSID в организованной внутри библиотеки таблице. В зависимости от флагов активации, использованных при вызове, библиотека COM может также сообщать локальному SCM, что вызывающий процесс является теперь серверным процессом для указанного класса. CoRegisterClassObject возвращает двойное слово (DWORD), которое представляет связь между CLSID и объектом класса. Это двойное слово можно использовать для завершения связи (а также для извещения SCM о том, что вызывающий процесс более не является серверным процессом для данного CLSID) путем вызова API-функции CoRevokeClassObject:
HRESULT CoRevokeClassObject([in] DWORD dwReg); // association ID // ID связи
Два параметра типа DWORD являются примером тонкого устройства CoRegisterClassObject. Эти параметры дают вызывающему объекту контроль над тем, как и когда объект класса является доступным.
А каким образом и на какой срок сделать доступным объект класса, вызывающему объекту позволяют решить флаги активации, передающиеся CoRegisterСlassObject в качестве четвертого параметра. COM предусматривает следующие константы для использования в этом параметре:
typedef enum tagREGCLS { REGCLS_SINGLEUSE = 0, // give out class object once // выделяем объект класса однократно REGCLS_MULTIPLEUSE = 1, // give out class object many // выделяем объект класса многократно REGCLS_MULTI_SEPARATE = 2, // give out class object many // выделяем объект класса многократно REGCLS_SUSPENDED = 4, // do not notify SCM (flag) // не извещаем SCM (флаг) REGCLS_SURROGATE = 8 // used with DLL Surrogates // используется с суррогатами DLL } REGCLS;
Значение REGCLS_SURROGATE используется в реализациях суррогатов DLL, которые будут рассматриваться позднее в данной главе. Двумя основными значениями являются REGCLS_SINGLEUSE и REGCLS_MULTIPLEUSE. Первое предписывает библиотеке COM использовать объект класса для обслуживания только одного активационного запроса. Когда происходит первый активационный запрос, COM удаляет зарегистрированный объект класса из области открытой видимости (public view). Если придет второй активационный запрос, COM должна использовать другой зарегистрированный объект класса. Если больше не доступен ни один объект класса с тем же CLSID, то для удовлетворения этого запроса COM создаст другой серверный процесс.
Напротив, флаг REGCLS_MULTIPLEUSE показывает, что объект класса может быть использован многократно, до тех пор, пока вызов функции CoRevokeСlassObject не удалит его элемент из таблицы класса библиотеки COM. Флаг REGCLS_MULTI_SEPARATE адресует последующие внутрипроцессные активационные запросы, которые могут произойти в процессе вызывающего объекта. Если вызывающий объект регистрирует объект класса с флагом REGCLS_MULTIPLEUSE, то COM допускает, что любые внутрипроцессные активационные запросы от процесса вызывающего объекта не будут загружать отдельный внутрипроцессный сервер, а будут вместо этого использовать зарегистрированный объект класса. Это означает, что даже если вызывающая программа только зарегистрировала объект класса с флагом CLSCTX_LOCAL_SERVER, то для удовлетворения внутрипроцессных запросов от того же процесса будет использован зарегистрированный объект класса.
Если такое поведение неприемлемо, вызывающая программа может зарегистрировать объект класса, используя флаг REGCLS_MULTI_SEPARATE. Флаг инструктирует COM использовать зарегистрированный объект класса для внутрипроцессных запросов только в случае, если для регистрации этого класса был использован флаг CLSCTX_INPROC_SERVER. Это означает, что следующий вызов CoRegisterClassObject:
hr = CoRegisterClassObject(CLSID_Me, &g_coMe, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &dw);
эквивалентен следующему:
hr = CoRegisterClassObject(CLSID_Me, &g_coMe, CLSCTX_LOCAL_SERVER | CLSCTX_INPROC, REGCLS_MULTI_SEPARATE, &dw);
В любом случае, если бы из процесса вызывающего объекта был осуществлен такой вызов:
hr = CoGetClassObject(CLSID_Me, CLSCTX_INPROC, 0, IID_IUnknown, (void**)&pUnkCO);
то никакая DLL не была бы загружена. Вместо этого COM удовлетворила бы запрос, используя объект класса, зарегистрированный посредством CoRegisterClassObject. Если, однако, серверный процесс вызвал CoRegisterClassObject таким образом:
hr = CoRegisterClassObject(CLSID_Me, &g_coMe, CLSCTX_LOCAL_SERVER, REGCLS_MULTI_SEPARATE, &dw);
то любые внутрипроцессные запросы на активацию CLSID_Me, исходящие изнутри серверного процесса, заставят DLL загрузиться.
CoRegisterClassObject связывает зарегистрированный объект класса с апартаментом вызывающего объекта. Это означает, что все поступающие запросы методов будут выполняться в апартаменте вызывающей программы. В частности, это означает, что если объект класса экспортирует интерфейс IClassFactory, то метод CreateInstance будет выполняться в апартаменте вызывающей программы. Результаты метода CreateInstance будут маршалированы из апартамента объекта класса, а это, в свою очередь, означает, что экземпляры класса будут принадлежать к тому же апартаменту, что и объект класса.
Серверные процессы могут регистрировать объекты класса для более чем одного класса. Если объекты класса зарегистрированы для выполнения в МТА процесса, то это означает, что поступающие запросы на активацию могут быть обслужены, как только будет завершен первый вызов CoRegisterClassObject.
Во многих серверных процессах, основанных на МТА, это может вызвать проблемы, так как бывает, что процесс должен выполнить дальнейшую инициализацию. Чтобы избежать этой проблемы, в реализации COM под Windows NT 4.0 введен флаг REGCLS_SUSPENDED. При добавлении этого флага в вызов CoRegisterСlassObject библиотека COM не извещает SCM о том, что класс доступен. Это предотвращает поступление в серверный процесс входящих активационных запросов. Библиотека COM связывает CLSID с объектом класса; однако она помечает этот элемент в таблице библиотеки класса как отложенный. Для снятия этой пометки в COM предусмотрена API-функция CoResumeClassObjects:
HRESULT CoResumeClassObjects(void);
CoResumeClassObjects делает следующее. Во-первых, она помечает все отложенные объекты класса как легальные для использования. Во-вторых, она посылает SCM единственное сообщение, информируя его, что все ранее отложенные объекты класса теперь являются доступными в серверном процессе. Это сообщение обладает огромной силой, так как при его получении обновляется таблица класса SCM по всей машине и сразу для всех классов, зарегистрированных вызывающим объектом.
Получив три только что описанные API-функции, легко создать серверный процесс, экспортирующий один или более классов. Ниже приводится простая программа, которая экспортирует три класса из МТА сервера:
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { // define a singleton class object for each class // определяем синглетон для каждого класса static GorillaClass s_gorillaClass; static OrangutanClass s_orangutanClass; static ChimpClass s_chimpClass; DWORD rgdwReg[3]; const DWORD dwRegCls = REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED; const DWORD dwClsCtx = CLSCTX_LOCAL_SERVER;
// enter the MTA // входим в МТА HRESULT hr = GoInitializeEx(0, COINIT_MULTITHREADED); assert(SUCCEEDED(hr)) ; // register class objects with СОM library's class table // регистрируем объекты класса с помощью // таблицы класса библиотеки COM hr = CoRegisterClassObject(CLSID_Gorilla, &s_gorillaClass, dwClsCtx, dwRegCls, rgdwReg); assert(SUCCEEDED(hr)); hr = CoRegisterClassObject(CLSID_Orangutan, &s_orangutanClass, dwClsCtx, dwRegCls, rgdwReg + 1); assert(SUCCEEDED(hr)) ; hr = CoRegisterClassObject(CLSID_Chimp, &s_chimpClass, dwClsCtx, dwRegCls, rgdwReg + 2); assert(SUCCEEDED(hr));
// notify the SCM // извещаем SCM hr = CoResumeClassObjects(); assert(SUCCEEDED(hr)); // keep process alive until event is signaled // сохраняем процессу жизнь, пока событие не наступило extern HANDLE g_heventShutdown; WaitForSingleObject(g_heventShutdown, INFINITE); // remove entries from COM library's class table // удаляем элементы из таблицы класса библиотеки COM for (int n = 0; n < 3; n++) CoRevokeClassObject(rgdwReg[n]); // leave the MTA // покидаем MTA CoUninitialize(); return 0; }
В данном фрагменте кода предполагается, что событие (Win32 Event object) будет инициализировано где-нибудь еще внутри процесса таким образом:
HANDLE g_heventShutdown = CreateEvent(0, TRUE, FALSE, 0);
Имея данное событие, сервер может быть мирно остановлен с помощью вызова API-функции SetEvent:
SetEvent(g_heventShutdown);
которая запустит последовательность выключения в главном потоке. Если бы сервер был реализован как сервер на основе STA, то главный поток должен был бы вместо ожидания события Win32 Event запустить конвейер обработки оконных сообщений (windows message pump). Это необходимо для того, чтобы позволить поступающим ORPC-запросам входить в апартамент главного потока.
1
Хорошо реализованные серверы проверяют также наличие -RegServer и -UnregServer. Все четыре ключа не зависят от регистра (case).
2
В зависимости от того, как класс сконфигурирован в локальном регистре, регистрация серверного процесса может обратиться ко всем клиентским процессам или только к тем клиентским процессам, которые выполняются с тем же мандатом защиты и/или с той же windows-станцией.
3
Для метода CreateInstance технически осуществимо обеспечить принудительное создание объекта в определенном апартаменте с использованием стандартных технологии мультиапартаментного программирования. Однако фактическая реализация CreateInstance просто обрабатывает новый объект во время выполнения в текущем апартаменте.
COM и защита
Исходная версия COM не занималась проблемой защиты. Это можно рассматривать как упущение, так как многие примитивы NT без удаленного доступа (nonremotable) (например, процессы, потоки) могут быть защищены, несмотря на то, что ими нельзя управлять дистанционно. Версия Windows NT 4.0 вынудила добавить к COM обеспечение безопасности, так как стало возможным осуществлять доступ к серверным процессам практически от любой машины в сети. К счастью, поскольку COM использует в качестве средства сообщения RPC, то защита COM просто применяет существующую инфраструктуру защиты RPC.
Защита COM может быть разделена на три категории: аутентификация (authentication), контроль доступа (access control) и управление маркерами (token management). Аутентификация заключается в обеспечении подлинности сообщения, а именно: отправитель действительно тот, за кого он себя выдает, а данное сообщение действительно пришло от отправителя. Контроль за доступом проверяет, кто имеет допуск к объектам сервера и кто имеет право запуска серверного процесса. Управление маркерами осуществляет контроль за тем, чьи полномочия использованы при запуске серверных процессов и при выполнении самого вызова метода. В COM предусмотрены до некоторой степени разумные установки по умолчанию по всем трем аспектам защиты, что делает теоретически возможным писать приложения COM, не учитывая вопросов безопасности. Эти установки по умолчанию основаны на принципе наименьших сюрпризов; то есть если программист не делает ничего явного по защите, то маловероятно, что этим будут внесены какие-либо "дыры" в систему безопасности NT. В то же время построение даже простейшего рассредоточенного приложения на базе COM требует, чтобы каждому аспекту обеспечения безопасности было уделено определенное внимание.
Большинство аспектов защиты COM может быть сконфигурировано путем помещения в реестр нужной информации. Программа DCOMCNFG.ЕХЕ позволяет администраторам настраивать большинство установок (но не все), относящихся к защите COM.
Для большинства этих установок ( но не для всех) программист приложения может предпочесть употребление явных API-функций вместо установок в реестре. В целом большинство приложений используют комбинацию установок DCOMCNFG.EXE с явными API-функциями. Первый вариант проще для отладки системными администраторами, в то время как второй предполагает большую гибкость без риска неправильного обращения с DCOMCNFG.EXE.
Защита COM использует основные средства RPC для аутентификации и заимствования прав (impersonation). Напомним, что RPC использует загружаемые транспортные модули для того, чтобы после их загрузки в систему были добавлены новые сетевые протоколы. Транспортные модули именуются при помощи последовательностей протоколов (protocol sequences) (например, "ncadg_ip_udp"), которые преобразуются в реестре в специальную транспортную библиотеку DLL. Это позволяет третьим участникам устанавливать поддержку новых транспортных протоколов без модифицирования библиотеки COM. Подобным же образом RPC поддерживает загружаемые модули защиты, позволяя добавлять в систему новые протоколы защиты. Модули защиты именуются при помощи целых чисел, которые преобразуются в реестре в специальные DLL модулей защиты. Эти DLL должны соответствовать требованиям SSPI (Security Support Provider Interface — интерфейс провайдера поддержки безопасности), который является производным от Internet Draft Standard GSSAPI.
В системных заголовочных файлах определено несколько констант для известных модулей защиты. Ниже приведен текущий список известных на момент написания этого текста модулей защиты:
enum { RPC_C_AUTHN_NONE = 0, // no authentication package // модуля аутентификации нет RPC_C_AUTHN_DCE_PRIVATE = 1, // DCE private key (not used) // закрытый ключ DCE (не используется) RPC_C_AUTHN_DCE_PUBLIC = 2, // DCE public key (not used) // открытый ключ DCE (не используется) RPC_C_AUTHN_DEC_PUBLIC = 4, // Digital Equip, (not used) // цифровое оборудование (не используется) RPC_C_AUTHN_WINNT = 10, // NT Lan Manager // администратор локальной сети NT RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_MQ = 100, // MS Message Queue package // модуль MS Message Queue (очереди сообщений Microsoft) RPC_C_AUTHN_DEFAULT = 0xFFFFFFFFL };
RPC_C_AUTHN_WINNT показывает, что должен использоваться протокол аутентификации Администратора локальной сети (NT LAN (local area network) Manager — NTLM). RPC_C_AUTHN_GSS_KERBEROS показывает, что будет использован протокол аутентификации Kerberos. Под Windows NT 4.0 поддерживается только протокол NTLM, если не установлена SSP третьего участника. Windows NT 5.0 будет выпущена с поддержкой как минимум NTLM и Kerberos. По вопросам получения других модулей защиты обращайтесь к соответствующей документации.
Каждый интерфейсный заместитель может быть сконфигурирован независимо, что дает возможность использовать различные модули защиты. Если интерфейсный заместитель сконфигурирован для использования протокола защиты, в клиентский процесс загружается соответствующая SSP DLL. Для того чтобы запрос на соединение с защитой был принят, серверный процесс должен зарегистрировать и загрузить соответствующую библиотеку SSP DLL до получения от клиента первого вызова ORPC. Если соединение сконфигурировано для использования модуля защиты, то соответствующая SSP DLL работает в связке с динамическим уровнем иерархии RPC и имеет возможность видеть каждый пакет, передаваемый или получаемый в рамках конкретного соединения. Библиотеки SSP DLL могут посылать в каждом пакете дополнительную информацию, специфическую для защиты, а также модифицировать маршалированное состояние параметра в целях шифрования. DCE RPC (и COM) предусматривают шесть уровней аутентификационной защиты, которые варьируются от отсутствия защиты до полного шифрования состояния всех параметров:
enum { RPC_C_AUTHN_LEVEL_DEFAULT, // use default level for pkg // используем для модуля уровень, принятый по умолчанию RPC_C_AUTHN_LEVEL_NONE, // по authentication // без аутентификации RPC_C_AUTHN_LEVEL_CONNECT, // only authenticate credentials // только аутентификация мандата RPC_C_AUTHN_LEVEL_CALL, // protect message headers // защищаем заголовки сообщений RPC_C_AUTHN_LEVEL_PKT, // protect packet headers // защищаем заголовки пакетов RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, // protect parameter state // защищаем состояние параметров RPC_C_AUTHN_LEVEL_PKT_PRIVACY, // encrypt parameter state // зашифровываем состояние параметров };
Каждый последующий уровень аутентификации включает в себя функциональные возможности предыдущего уровня. Уровень RPC_C_AUTHN_LEVEL_NONE показывает, что не будет проведено никакой аутентификации. Уровень RPC_C_AUTHN_LEVEL_CONNECT показывает, что при первом вызове метода полномочия клиента должны быть аутентифицированы на сервере. Если у клиента нет необходимых полномочий, то вызов ORPC будет прерван с ошибкой E_ACCESSDENIED. Как именно проверяется достоверность этих полномочий, зависит от того, какая SSP используется. Под NTML серверный процесс выдает запрос пароля (challenge) клиентскому процессу. Этот запрос представляет собой просто непредсказуемое большое случайное число. Клиент использует закодированную версию пароля вызывающего объекта для шифрования этого запроса, который затем пересылается обратно серверу в качестве отклика (response). Затем сервер шифрует исходный запрос пароля с помощью того, что он считает закодированным паролем, и сравнивает результат с тем откликом, который он получил от клиента. Если отклик клиента совпадает с зашифрованным запросом сервера, то "личность" клиента считается установленной. NTLMSSP последовательно располагает пары квитирования (установления связи) запрос-отклик в отправных пакетах, которые посылаются исполняемой программой RPC для синхронизации порядковых номеров. Поэтому между клиентом и сервером не производится никакого дополнительного сетевого обмена данными. В зависимости от типа учетной записи (доменная, локальная) дополнительный обмен данными с контроллером домена для поддержки опосредованной аутентификации (pass-through authentication) может производиться или не производиться.
При использовании уровня аутентификации RPC_AUTHN_LEVEL_CONNECT никакого дополнительного обмена информацией, касающейся защиты, после проверки начальных полномочий не осуществляется. Это означает, что программы-злоумышленники могут перехватывать сообщения в сети и воспроизводить RPC-запросы путем простого изменения порядковых номеров DCE (среды распределенных вычислений) в заголовках пакетов.
Для дополнительной защиты от воспроизведения вызова следовало бы использовать уровень аутентификации RPC_C_AUTHN_LEVEL_CALL. Он информирует библиотеки SSP DLL о необходимости защиты RPC-заголовка первого пакета каждого запроса или отклика RPC путем привязывания к передаваемому пакету однонаправленного хэш-ключа (на базе случайных чисел). Поскольку запрос или отклик RPC может быть помещен частями в более чем один сетевой пакет, то RPC API поддерживает также уровень аутентификации RPC_C_AUTHN_LEVEL_PKT. Этот уровень защищает от воспроизведения на уровне сетевых пакетов, что является большей защитой, чем уровень RPC_C_AUTHN_LEVEL_CALL, поскольку RPC-сообщение может занимать два пакета или более.
До уровня аутентификации RPC_C_AUTHN_LEVEL_PKT включительно SSP DLL в большей или меньшей мере игнорирует фактическую полезную нагрузку RPC-пакетов и защищает только целостность RPC-заголовков. Для того чтобы убедиться, что состояние маршалированного параметра не изменено вражеским агентом в сети, в RPC предусмотрен уровень аутентификации RPC_C_AUTHN_LEVEL_PKT_INTEGRITY. Этот уровень предписывает SSP DLL вычислять контрольную сумму состояния маршалированного параметра и проверять, что содержимое пакета не было изменено в процессе передачи. Поскольку при этом уровне аутентификации каждый передаваемый байт должен обрабатываться с помощью SSP DLL, она проходит значительно медленнее, чем при уровне RPC_C_AUTHN_LEVEL_PKT, и его следует использовать только в ситуациях, требующих особой защиты.
До уровня аутентификации RPC_C_AUTHN_LEVEL_PKT_INTEGRITY включительно фактическое содержание RPC-пакетов пересылается как открытый текст (например, незашифрованный). Для обеспечения невидимости состояния маршалированного параметра для вражеских агентов сети в RPC предусмотрен уровень аутентификации RPC_C_AUTHN_LEVEL_PKT_PRIVACY. Данный уровень аутентификации предписывает SSP DLL зашифровывать состояние маршалированного параметра до передачи. Подобно всем прочим уровням аутентификации уровень RPC_C_AUTHN_LEVEL_PKT_PRIVACY включает в себя защиту всех уровней ниже себя.
Как и в случае RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, каждый передаваемый байт должен обрабатываться SSP DLL, поэтому во избежание излишних издержек этот уровень аутентификации следует использовать только в особых с точки зрения безопасности ситуациях.
Наиболее важной API-функцией в службе безопасности COM является CoInitializeSecurity. Каждый процесс, использующий COM, вызывает CoInitializeSecurity ровно один раз, явно или неявно. Функция CoInitializeSecurity вводит автоматические установки по защите. Эти установки применяются ко всем импортируемым и экспортируемым ссылкам на объекты, за исключением явно переопределенных с использованием дополнительных вызовов API-функций. Чтобы использовать один или нескольких модулей защиты, CoInitializeSecurity конфигурирует используемый исполняемый слой RPC, а также устанавливает уровень аутентификации, принимаемый по умолчанию для процесса. Кроме того, CoInitializeSecurity позволяет вызывающей программе указать, каким пользователям разрешено делать ORPC-запросы на объекты, экспортируемые из текущего процесса. CoInitia1izeSecurity имеет довольно большое число параметров:
HRESULT CoInitializeSecurity( [in] PSECURITY_DESCRIPTOR pSecDesc, // access control // контроль за доступом [in] LONG cAuthSvc, // # of sec pkgs (-1 == use defaults) // количество модулей защиты (-1 == используем по умолчанию) [in] SOLE_AUTHENTICATION_SERVICE *rgsAuthSvc, // SSP array // массив SSP [in] void *pReserved1, // reserved MBZ // зарезервировано, должен быть О [in] DWORD dwAuthnLevel, // auto, AUTHN_LEVEL // аутентификация AUTHN_LEVEL [in] DWORD dwImpLevel, // auto. IMP_LEVEL // аутентификация IMP_LEVEL [in] void *pReserved2, // reserved MBZ // зарезервировано, должен быть О [in] DWORD dwCapabilities, // misc flags // различные флаги [in] void *pReserved3 // reserved MBZ // зарезервировано, должен быть О );
Некоторые из этих параметров применяются только в тех случаях, когда процесс выступает как экспортер/сервер. Другие — только если процесс действует как импортер/клиент.
Остальные применяются в обоих случаях.
Первый параметр функции CoInitializeSecurity, pSecDesc, применим только в случае, когда процесс выступает как экспортер. Этот параметр используется для контроля того, каким принципалам — пользователям или процессам, имеющим учетную запись (principals) — разрешен доступ к объектам, экспортируемым из данного процесса. В деталях этот параметр будет обсужден позже в данной главе. Второй и третий параметры функции CoInitializeSecurity, соответственно cAuthSvc и rgsAuthSvc, используются при работе процесса в качестве экспортера для регистрации одного или нескольких модулей защиты с помощью библиотеки COM. Эти два параметра ссылаются на массив описаний модулей защиты:
typedef struct tagSOLE_AUTHENTICATION_SERVICE { DWORD dwAuthnSvc; // which authentication package? // какой модуль защиты? DWORD dwAuthzSvc; // which authorization service? // какая служба авторизации? OLECHAR *pPrincipalName; // server principal name? // имя серверного принципала? HRESULT hr; // result of registration // результат регистрации } SOLE_AUTHENTICATION_SERVICE;
В Windows NT 4.0 единственной установленной службой аутентификации является RPC_C_AUTHN_WINNT (NTLM). При использовании аутентификации NTLM служба авторизации (authorization service — сервис контроля доступа, определяющий права клиента) должна быть указана как RPC_C_AUTHZ_NONE, а имя серверного принципала не используется и должно быть нулевым. Для тех процессов, которые просто хотят использовать пакет (пакеты) защиты по умолчанию на отдельной машине, следует использовать значения: cAuthSvc, равное -1, и rgsAuthSvc, равное нулю.
Пятый параметр функции CoInitializeSecurity, dwAuthnLevel, применим как к экспортируемым, так и к импортируемым объектным ссылкам. Величина, заданная для этого параметра, устанавливает предельно низкий уровень аутентификации для объектных ссылок, экспортируемых из этого процесса. Это означает, что поступающие ORPC-запросы должны иметь по крайней мере такой уровень аутентификации; в противном случае этот вызов будет отклонен.
Эта величина определяет также минимальный уровень аутентификации, используемый новыми интерфейсными заместителями, которые возвращаются API-функциями или методами COM. Создавая новый интерфейсный заместитель во время демаршалинга, COM рассматривает число, обозначающее нижний уровень аутентификации, заданный экспортером, как часть разрешения OXID. Затем COM устанавливает уровень аутентификации нового заместителя равным или нижнему уровню экспортера, или нижнему уровню текущего процесса — в зависимости от того, какой из них выше. Если процесс, импортирующий объектную ссылку, имеет уровень аутентификации ниже, чем экспортирующий процесс, то для установки уровня аутентификации используется нижний уровень экспортера. Такой способ гарантирует, что любые ORPC-запросы, посылаемые интерфейсным заместителем, пройдут через нижний уровень экспортера. Далее в этой главе будет рассмотрено, как с целью более детального контроля можно явным образом изменить уровень аутентификации для отдельного интерфейсного заместителя.
Шестой параметр функции CoInitializeSecurity, dwImpLevel применяется для импортируемых объектных ссылок. Величина, определенная для этого параметра, устанавливает уровень заимствования прав (impersonation level), используемый для всех объектных ссылок, которые возвращаются функцией CoUnmarshalInterface. Уровень заимствования прав позволяет одному процессу заимствовать атрибуты защиты у другого процесса и отражает степень доверия, которое клиент испытывает к серверу. Этот параметр должен принимать одно из следующих четырех значений, характеризующих уровень заимствования прав:
enum { // hide credentials of caller from object // скрываем от объекта полномочия вызывающей программы RPC_C_IMP_LEVEL_ANONYMOUS = 1, // allow object to query credentials of caller // разрешаем объекту запрашивать полномочия вызывающей программы RPC_C_IMP_LEVEL_IDENTIFY = 2, // allow use of caller's credentials up to one-hop away // разрешаем использовать полномочия вызывающей // программы не далее одной сетевой передачи RPC_C_IMP_LEVEL_IMPERSONATE = 3, // allow use of caller's credentials across multiple hops // разрешаем использовать полномочия вызывающей // программы в течение нескольких сетевых передач RPC_C_IMP_LEVEL_DELEGATE = 4 };
Уровень RPC_C_IMP_LEVEL_ANONYMOUS не позволяет реализации объекта обнаружить идентификатор безопасности вызывающей программы. Уровень доверия RPC_C_IMP_LEVEL_IDENTIFY указывает, что реализация объекта может программно определить идентификатор защиты вызывающей программы. Уровень доверия RPC_C_IMP_LEVEL_IMPERSONATE указывает, что сервер может не только определить идентификатор защиты вызывающей программы, но также выполнить операции на уровне операционной системы с использованием полномочий вызывающей программы. На этом уровне доверия объекты могут использовать полномочия вызывающей программы, но имеют доступ только к локальным ресурсам. В противоположность этому, уровень доверия RPC_C_IMP_LEVEL_DELEGATE разрешает серверу доступ как к локальным, так и к удаленным ресурсам с использованием полномочий вызывающей программы. Этот уровень доверия не поддерживается протоколом аутентификации NTLM, но поддерживается протоколом аутентификации Kerberos.
Восьмой параметр функции CoInitializeSecurity, dwCapabilities применим к импортируемым и к экспортируемым объектным ссылкам. Этот параметр является битовой маской, которая может состоять из нуля или более следующих битов:
typedef enum tagEOLE_AUTHENTICATION_CAPABILITIES { EOAC_NONE = 0х0, EOAC_MUTUAL_AUTH = 0х1, // These are only valid for CoInitializeSecurity // только эти допустимы для CoInitializeSecurity EOAC_SECURE_REFS = 0х2, EOAC_ACCESS_CONTROL = 0х4, EOAC_APPID = 0х8 } EOLE_AUTHENTICATION_CAPABILITIES;
Взаимная аутентификация (EOAC_MUTUAL_AUTH) под NTLM не поддерживается. Она используется для проверки того, что сервер исполняется как ожидаемый принципал. Ссылки защиты (EOAC_MUTUAL_AUTH) указывают, что распределенные вызовы подсчета ссылок COM будут аутентифицироваться для гарантии того, что никакие вражеские агенты не могут испортить счетчик ссылок, используемый OR и администраторами заглушек для управления жизненным циклом. EOAC_ACCESS_CONTROL и EOAC_APPID используются для управления семантикой первого параметра функции CoInitializeSecurity и будут обсуждаться далее в этой главе.
Как было установлено ранее в этом разделе, CoInitializeSecurity вызывается один раз на процесс, явно или неявно. Приложения, желающие вызвать CoInitializeSecurity явно, должны делать это после первого вызова CoInitializeEx, но перед "первым интересным вызовом COM" (first interesting COM call). Фраза "первый интересный вызов COM" относится к любой API-функции, которой может понадобиться OXID. Сюда относятся CoMarshalInterface и CoUnmarshalInterface, а также любые API-вызовы, неявно вызывающие эти функции. Поскольку вызовы CoRegisterClassObject связывают объекты класса с апартаментами, то CoInitializeSecurity должна быть вызвана до регистрации объектов класса. API-функции активации (например, CoCreateInstanceEx) являются любопытным исключением. Активация API-функций для определенных внутренних классов, которые являются частью COM API (например, глобальная интерфейсная таблица, COM-объект контроля доступа по умолчанию) может быть произведена до вызова CoInitializeSecurity. Тем не менее, функция CoInitializeSecurity должна быть вызвана раньше, чем любые активационные вызовы, которые фактически консультируются с реестром и загружают другие DLL или контактируют с другими серверами. Если приложение не вызывает функцию CoInitializeSecurity явно, то COM вызовет ее неявно в качестве первого интересного вызова COM.
Когда COM вызывает функцию CoInitializeSecurity неявно, она читает значения большинства параметров из реестра. Некоторые из этих параметров содержатся в реестровом ключе, общем для всей машины, в то время как остальные записаны под специфическим AppID данного приложения. Чтобы извлечь AppID приложения, COM ищет имя файла процесса приложения под ключом реестра
HKEY_CLASSES_ROOT\AppID
Если COM находит здесь имя файла, она извлекает AppID из именованной величины AppID:
[HKCR\AppID\ServerOfTheApes.exe] AppID="{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}"
Если никакого соответствия не существует, то COM считает, что приложение не имеет в реестре специфических установок по защите.
Неявные вызовы CoInitializeSecurity находят первый параметр, pSecDesc, путем поиска сериализованного (приведенного в последовательную форму) дескриптора защиты NT SECURITY_DESCRIPTOR в следующей именованной величине:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}] AccessPermission=<serialized NT security descriptor>
Если эта именованная величина не найдена, то COM ищет общий для всей машины элемент:
[HKEY_LOCAL_MACHINE\Software\Microsoft\OLE] DefaultAccessPermission=<serialized NT security descriptor>
Оба этих элемента реестра могут быть легко изменены при помощи DCOMCNFG.ЕХЕ. Если не найден ни один из этих элементов реестра, то COM создаст дескриптор защиты (security descriptor), предоставляющий доступ принципалу только вызывающей программы и встроенной учетной записи SYSTEM. COM использует этот дескриптор защиты, чтобы при помощи Win32 API-функции AccessCheck предоставить или запретить доступ к объектам, экспортированным из данного процесса.
Неявные вызовы CoInitializeSecurity используют для второго и третьего параметров (cAuthSvc и rgsAuthSvc) значения -1 и нуль соответственно, указывая тем самым, что должны использоваться модули защиты, принятые по умолчанию. Неявные вызовы CoInitializeSecurity находят значения для пятого и шестого параметров (dwAuthnLevel и dwImpLevel) в следующем элементе реестра всей машины:
[HKEY_LOCAL_MACHINE\Software\Microsoft\OLE] LegacyAuthenticationLevel = 0x5 LegacyImpersonationLevel = 0x3
RPC_C_AUTHN_LEVEL_PKT_INTEGRITY и RPC_C_AUTHN_LEVEL_IMPERSONATE принимают числовые значения 5 и 3 соответственно. Если эти именованные величины отсутствуют, то используются значения RPC_C_AUTHN_LEVEL_CONNECT и RPC_C_IMP_LEVEL_IDENTIFY. Из флагов, используемых в восьмом параметре функций CoInitializeSecurity, dwCapabilities, в настоящее время читается из элемента реестра всей машины только флаг EOAC_SECURE_REFS:
[HKEY_LOCAL_MACHINE\Software\Microsoft\OLE] LegacySecureRefs = "Y"
Если эта именованная величина в наличии и содержит "Y" или "y", то COM будет использовать флаг EOAC_SECURE_REFS; в противном случае используется флаг EOAC_NONE.
Каждая из этих традиционных установок аутентификации может быть легко изменена при помощи DCOMCNFG.ЕХЕ.
1
Эти два параметра могут использоваться в других модулях аутентификации.
2
Отдельный модуль защиты может увеличить уровень, заданный для клиента/сервера, в зависимости от используемого транспортного протокола. В частности, NTML будет использовать уровень RPC_C_AUTHN_LEVEL_PRIVACY для всех вызовов с той же машины. Кроме того, NTLM будет повышать уровни RPC_AUTHN_LEVEL_CONNECT и RPC_C_AUTHN_LEVEL_CALL до RPC_AUTHN_LEVEL_PKT для транспортировки дэйтаграмм (datagram transports) (например, UDP). Для транспортировок, ориентированных на связь (connection-oriented transport) (например, TCP), NTLM будет повышать уровень с RPC_С_AUTHN_LEVEL_CALL дo RPC_C_AUTHN_LEVEL_PKT.
3
Во время написания данного текста оба SSP - NTLM и Kerberos - молча принимали это значение, но в действительности повышали его до RPC_C_IMP_LEVEL_IDENTIFY, если имело место соединение с удаленной машиной.
4
Формально уровень RPC_C_IMP_LEVEL_IMPERSONATE разрешает сохранять полномочия вызывающей программы не более чем в течение одной сетевой передачи. Это эффективно ограничивает доступ для удаленных объектов ресурсами только локальной машины объекта.
Где мы находимся?
В данной главе рассматривались вопросы, относящиеся к выделению классов в отдельные серверные процессы. COM поддерживает запуск серверных процессов на основе запросов на активацию. Эти серверные процессы должны саморегистрироваться с помощью библиотеки COM, используя CoRegisterClassObject для того, чтобы обеспечить доступ к объектам своего класса со стороны внешних клиентов. Архитектура системы безопасности COM тесно связана с собственной моделью безопасности операционной системы и основывается на трех различных понятиях. Целостность и аутентичность сообщений ORPC, которыми обмениваются клиент и объект, обеспечивается аутентификацией. Контроль доступа выявляет, какие принципалы защиты могут иметь доступ к объектам, экспортированным из данного процесса. Управление маркерами отслеживает, какие полномочия используются для запуска серверных процессов и выполнения методов объекта.
Идентификаторы приложений
В версии COM под Windows NT 4.0 введено понятие приложений COM (COM applications). Приложения COM идентифицируются с помощью GUID (называемых в этом контексте AppID — идентификаторы приложения) и представляют серверный процесс для одного или более классов. Каждый CLSID может быть связан с ровно одним идентификатором приложений. Эта связь фиксируется в локальном реестре, использующем именованное значение AppID:
[HKCR\CLSID\{27EE6A4E-DF65-11D0-8C5F-0080C73925BA}] @="Gorilla" AppID="{27EE6A4E-DF65-11D0-8C5F-0080C73925BA}"
Все классы, принадлежащие к одному и тому же приложению COM, будут иметь один и тот же AppID, а также будут использовать одни и те же установки удаленной активации и защиты. Эти установки записаны в локальном реестре под ключом
HKEY_CLASSES_ROOT\AppID
Подобно CLSID, AppID могут быть зарегистрированы для каждого пользователя под Windows NT версии 5.0 или выше. Поскольку серверы, реализованные до появления Windows NT 4.0, не регистрируют явно свои AppID, инструментальные средства конфигурирования COM (например, DCOMCNFG.EXE, OLEVIEW.EXE) автоматически создадут новый AppID для этих старых серверов. Чтобы синтезировать AppID для старых серверов, эти программы автоматически добавляют именованное значение AppID ко всем CLSID, экспортируемым определенным локальным сервером. При добавлении этих именованных значений DCOMCNFG или OLEVIEW просто использует в качестве AppID первый встреченный CLSID для данного сервера. Те приложения, которые были разработаны после выпуска Windows NT 4.0, могут (и будут) использовать для своих AppID особый GUID.
Большинством установок AppID можно управлять с помощью программы DCOMCNFG.EXE, которая является стандартным компонентом Windows NT 4.0 или выше. DCOMCNFG.EXE предоставляет администраторам удобный для использования интерфейс для контроля установок удаленного доступа и защиты. Более мощный инструмент, OLEVIEW.EXE, осуществляет большинство функциональных возможностей DCOMCNFG.EXE и, кроме того, обеспечивает очень "COM-центрический" (COM-centric) взгляд на реестр.
Обе эти программы интуитивно понятны при использовании и обе являются существенными для разработок с использованием COM.
Простейшим установочным параметром AppID является RemoteServerName. Эта именованная величина показывает, какую хост-машину следует использовать для удаленных запросов на активацию, если в них явно не указано с помощью COSERVERINFO удаленное хост-имя. Рассмотрим следующие установки реестра:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}] @="Аре Server" RemoteServerName="www.apes.com"
[HKCR\AppID\{27EE6A4E-DF65-11d0-8C5F-0080C73925BA}] @="Gorilla" AppID={27EE6A4D-DF65-11d0-8C5F-0080C73925BA}
[HKCR\AppID\{27EE6A4F-DF65-11d0-8C5F-0080C73925BA}] @="Chimp" AppID={27EE6A4D-DF65-11d0-8C5F-0080C73925BA}
Если клиент осуществляет такой запрос на активацию:
IApeClass *рас = 0; HRESULT hr = CoGetClassObject(CLSID_Chimp, CLSCTX_REMOTE_SERVER, 0, IID_IApeClass, (void**)&pac);
то SCM со стороны клиента направит этот запрос в SCM на www.apes.com, где этот запрос будет рассмотрен как локальный активационный запрос. Отметим, что если клиент предоставляет явное хост-имя:
IApeClass *рас = 0; COSERVERINFO csi; ZeroMemory(&csi, sizeof(csi)); csi.pwszName = OLESTR("www.dogs.com"); HRESULT hr = CoGetClassObject(CLSID_Chimp, CLSCTX_REMOTE_SERVER, &csi, IID_IApeClass, (void**)&pac);
то установка RemoteServerName игнорируется и запрос направляется в www.apes.com.
Чаще встречается ситуация, когда клиенты не указывают явно свои предпочтения относительно хост-имени и месторасположения. Рассмотрим следующий вызов CoGetClassObject
IApeClass *pac = 0; HRESULT hr = CoGetClassObject(CLSID_Chimp, CLSCTX_ALL, 0, IID_IApeClass, (void*)&pac);
Поскольку не указано никакого хост-имени, то SCM сначала будет искать в локальном реестре следующий ключ:
[HKCR\AppID\{27EE6A4F-DF65-11d0-8C5F-0080C7392SBA}]
Если этот ключ локально не доступен, COM обратится к хранилищу класса (class store) под Windows NT 5.0, если оно доступно.
Если с этой точки зрения ключ реестра доступен, то вслед за этим SCM будет искать подключ InpocServer32:
HKCR\CLSID\{27EE6A4F-DF65-11d0-8C5F-0080C73925BA}\InprocServer32] @="C:\somefile.dll"
Если этот ключ найден, класс будет активирован путем загрузки той DLL, которая указана в реестре. В противном случае SCM ищет подключ InprocHandler32:
HKCR\CLSID\{27EE6A4F-DF65-11d0-8C5F-0080C73925BA}\InprocHandler32] @="C:\somefile.dll"
Если класс имеет ключ дескриптора (handler), то и тогда класс будет активирован путем загрузки той DLL, которая указана в реестре. Если ни один из внутрипроцессных подключей не доступен, то SCM предполагает, что активационный запрос должен быть внепроцессным. В таком случае SCM проверяет, имеет ли серверный процесс объект класса, зарегистрированный в настоящее время для запрашиваемого CLSID. Если это так, то SCM входит в серверный процесс и маршалирует объектную ссылку из соответствующего объекта класса и возвращает ее в апартамент вызывающего объекта, где она демаршалируется до того, как управление возвращается к вызывающему объекту. Если объект класса был зарегистрирован серверным процессом с флагом REGCLS_SINGLEUSE, то SCM затем забывает, что класс доступен в серверном процессе и не будет использовать его для последующих запросов на активацию.
Только что описанный сценарий является корректным, если серверный процесс уже выполняется. Если, однако, SCM получает внепроцессный запрос на активацию, но под запрашиваемым CLSID не зарегистрировался ни один серверный процесс, то SCM запустит серверный процесс, который еще не запущен. COM поддерживает три модели для создания серверов: сервисы NT (NT Services), нормальные процессы и суррогатные процессы (surrogate processes). NT Services и нормальные процессы очень похожи, и причины, по которым один из них можно предпочесть другому, явятся предметом дальнейшего обсуждения в рамках этой главы. Суррогатные процессы используются в основном для возложения функции ведущего узла на старые внутрипроцессные серверы в отдельных серверных процессах.
Это дает преимущества удаленной активации и локализации ошибок для старых DLL или для классов, которые должны быть упакованы как DLL (например, виртуальная машина Java). Независимо от того, какая модель используется для создания серверного процесса, у серверного процесса есть 120 секунд (или 30 секунд под Windows NT Service Pack 2 и более ранних версий) для регистрации запрошенного объекта класса с применением CoRegisterClassObject. Если серверный процесс не может вовремя зарегистрировать сам себя, то SCM откажет вызывающему объекту в запросе на активацию.
При создании серверного процесса SCM вначале проверяет, имеет ли AppID, соответствующий запрашиваемому классу, именованную величину LocalService:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA} LocalService="apesvc"
Если это именованное значение имеется, то SCM использует NT Service Control Manager (диспетчер управления сервисами) для запуска той службы NT, которая указана в реестре (например, apesvc). Если же именованная величина LocalService отсутствует, то в этом случае SCM ищет в указанном CLSID ключе подключ LocalServer32:
[HKCR\CLSID\{27EE6A4F-DF65-11d0-8C5F-0080C73925BA}\LocalServer32] @="C:\somefile.exe"
Если этот ключ присутствует, то SCM применит для запуска серверного процесса API-функцию CreateProcess (или CreateProcessAsUser). В случае отсутствия и LocalService, и LocalServer32, SCM ищет, определен ли для AppID-класса суррогатный процесс:
[HKCR\AppID\{27EE6A4D-DF6S-11d0-8CSF-0080C73925BA}] DllSurrogate=""
Если величина, именованная DllSurrogate, существует, но пуста, то SCM запустит суррогатный процесс по умолчанию (dllhost.exe). Если именованная величина DllSurrogate существует, но ссылается на легальное имя файла:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C7392SBA}] DllSurrogate="С:\somefile.exe"
то SCM запустит указанный серверный процесс. В любом случае суррогатный процесс саморегистрируется библиотекой COM (и SCM) с помощью API-функции CoRegisterSurrogate в качестве суррогатного процесса:
HRESULT CoRegisterSurrogate([in] ISurrogate *psg);
Эта API- функция предполагает, что суррогатный процесс предоставляет реализацию интерфейса ISurrogate:
[uuid(00000022-0000-0000-C000-000000000046), object] interface ISurrogate : IUnknown { // SCM asking surrogate to load inprocess class object and // call CoRegisterClassObject using REGCLS_SUSPENDED // SCM просит суррогат загрузить внутрипроцессный // объект класса и вызвать CoRegisterClassObject // с флагом REGCLS_SUSPENDED HRESULT LoadDllServer([in] REFCLSID rclsid); // SCM asking surrogate to shut down // SCM просит суррогат прекратить работу HRESULT FreeSurrogate(); }
Интерфейс ISurrogate предоставляет COM механизм запроса суррогатного процесса для регистрации объектов класса с последующим его остановом. Суррогатный механизм существует в первую очередь для поддержки удаленной активации старых внутрипроцессных серверов. В общем случае суррогаты могли бы использоваться только в тех случаях, когда внутрипроцесные серверы не могут быть перестроены во внепроцессные.
Если, наконец, не существует ни одного из этих ключей реестра или именованных величин, то SCM будет искать элемент RemoteServerName под ключом AppID, соответствующим классу:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8CSF-0080C7392SBA}] RemoteServerName="www.apes.com"
При наличии этой величины активационный запрос будет переадресован SCM указанной хост-машины. Отметим, что даже если клиент указал в начальном запросе на активацию только флаг CLSCTX_LOCAL_SERVER, то запрос будет переадресован только в случае, если не зарегистрировано ни одного локального серверного процесса.
Еще один дополнительный фактор, способный изменить адресацию активационных запросов, относится только к запросам CoGetInstanceFromFile (включая вызовы BindToObject файлового моникера). По умолчанию, если имя файла, использованное для наименования постоянного объекта, относится к файлу из удаленной файловой системы, то COM будет использовать вышеописанный алгоритм для определения того, где активировать объект.Если, однако, AppID класса имеет именованную величину ActivateAtStorage и эта величина равна "Y" или "y", то COM направит активационный запрос к той машине, на которой располагается файл, при условии, что вызывающий объект не передавал явное хост-имя через структуру COSERVERINFO. Этот способ гарантирует, что во всей сети будет существовать только один экземпляр.
1
Формально должен быть объект класса, зарегистрированный как легальный в контексте защиты вызывающего объекта.
Контроль доступа
Как уже упоминалось ранее в этой главе, каждый процесс COM может защитить сам себя от несанкционированного доступа. COM рассматривает контроль доступа на двух уровнях: права запуска (launch permissions) и права доступа (access permissions). Права запуска используются, чтобы определить, какие пользователи могут запускать серверные процессы при осуществлении активационных вызовов к SCM. Права доступа определяют, какие пользователи могут обращаться к объектам процесса после того, как сервер уже запущен. Оба типа контроля доступа можно сконфигурировать при помощи DCOMCNFG.EXE, но только права доступа могут быть заданы программно на этапе выполнения (поскольку после того, как сервер запущен, уже слишком поздно отказывать пользователю в правах запуска). Вместо этого право запуска предоставляется диспетчеру управления сервнсами SCM во время активации.
Когда SCM решает, что должен быть запущен новый серверный процесс, он пытается получить дескриптор защиты NT SECURITY_DESCRIPTOR, описывающий, каким пользователям разрешено запускать серверный процесс. В первую очередь SCM проверяет AppID класса для явной установки прав запуска. Эта установка приходит в форме сериализованного дескриптора защиты NT, который хранится в именованной величине LaunchPermission AppID:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}] LaunchPermission=<serialized NT security descriptor>
Если эта именованная величина отсутствует, SCM пытается прочитать общие для всей машины права запуска из такой именованной величины:
[HKEY_LOCAL_MACHINE\Software\Microsoft\OLE] DefaultLaunchPermission=<serialized NT security descriptor>
Обе эти установки могут быть модифицированы с помощью DCOMCNFG.EXE. Если не найден ни один из этих ключей реестра, то COM запретит запуск кому бы то ни было. Если же SECURITY_DESCRIPTOR найден, SCM проверяет идентификатор защиты активизирующей вызывающей программы (формально называемой активизатором — actiuator) по списку разграничительного контроля доступа DACL (Discretionary Access Control List), имеющемуся в дескрипторе, чтобы определить, имеет ли активизатор полномочия на запуск сервера.
Если активизатор не имеет необходимых полномочий, то следует отказ на активационный вызов с HRESULT E_ACCESSDENIED, и никаких процессов не запускается. В случае успешной проверки SCM запускает серверный процесс и продолжает выполнение активационного запроса.
Права запуска определяют только, какие пользователи могут или не могут начинать серверные процессы во время активации. Эта проверка всегда выполняется SCM на основе информации, записанной в реестре. Права доступа определяют, какие пользователи могут действительно связываться с объектами серверного процесса. Эта проверка осуществляется библиотекой COM при каждом запросе на установку соединения, приходящем от клиента. Для контроля установок прав доступа к процессу разработчики могут использовать API-функцию CoIntializeSecurity.
Напомним, что процессы, не вызывающие явно функцию CoInitializeSecurity, автоматически используют список контроля доступа, записанный под ключом реестра AppID приложения:
[HKCR\AppIO\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}] AccessPermission=<serialized NT security descriptor>
Ранее объяснялось, что если этот элемент реестра отсутствует, то COM ищет установку по умолчанию для всей машины, а если она также отсутствует, то создается новый список контроля доступа, который включает только принципала серверных процессов и встроенной учетной записи SYSTEM.
Приложения, явно вызывающие CoInitializeSecurity, могут вручную контролировать, каким вызывающим программам разрешен доступ к объектам, экспортируемым данным процессом. По умолчанию первый параметр CoIntializeSecurity принимает указатель на SECURITY_DESCRIPTOR NT. Если вызывающая программа передает в качестве этого параметра нулевой указатель, то COM не будет осуществлять никакого контроля входящих вызовов. Это разрешает вызовы от любого аутентифицированного принципала защиты. Если и клиент, и сервер укажут RPC_C_AUTHN_LEVEL_NONE, то COM разрешит вызовы от кого угодно, независимо от его аутентификации. Если же в вызывающей программе имеется легальный указатель на дескриптор защиты, то COM с помощью DACL этого дескриптора защиты определит, каким вызывающим программам разрешен доступ к объектам процесса.
Заголовки SDK определяют так называемый флаг прав (COM_RIGHTS_EXECUTE), который используется при создании DACL для явного разрешения или запрета пользователям на связь с объектами процесса.
Хотя и допускается использовать API-функции Win32 для создания SECURITY_DESCRIPTOR с целью передачи его в CoInitializeSecurity, этот способ контроля доступа к объектам процесса не является предпочтительным, в основном по причине темной природы API-функций защиты Win32. Для упрощения программирования в COM контроля доступа в реализации COM для Windows NT 4.0 Service Pack 2 разработчикам разрешено указывать тот объект COM, который будет использоваться для выполнения проверки доступа при установке новых соединений. Этот объект регистрируется библиотекой COM во время выполнения CoInitializeSecurity и должен реализовать интерфейс IAccessControl:
[object, uuid(EEDD23EO-8410-11CE-A1C3-08002B2B8D8F)] interface IAccessControl : IUnknown { // add access allowed rights for a list of users // добавляем разрешенные права доступа для списка пользователей HRESULT GrantAccessRights([in] PACTRL_ACCESSW pAccessList);
// explicitly set the access rights for a list of users // явно устанавливаем права доступа для списка пользователей HRESULT SetAccessRights([in] PACTRL_ACCESSW pAccessList // users+rights // пользователи + Права );
// set the owner/group IDs of the descriptor // устанавливаем идентификаторы владельца/группы для дескриптора HRESULT Set0wner( [in] PTRUSTEEW pOwner, // owner ID // ID владельца [in] PTRUSTEEW pGroup // group ID // ID группы );
// remove access rights for a list of users // удаляем права доступа для списка пользователей HRESULT RevokeAccessRights( [in] LPWSTR lpProperty, // not used // не используется [in] ULONG cTrustees, // how many users // сколько имеется пользователей [in, size_is(cTrustees)] TRUSTEEW prgTrustees[] // users // пользователи );
// get list of users and their rights // получаем список пользователей и их прав HRESULT GetAllAccessRights( [in] LPWSTR lpProperty, // not used // не используется [out] PACTRL_ACCESSW *ppAccessList, // users+rights // пользователи + права [out] PTRUSTEEW *ppOwner, // owner ID // ID владельца [out] PTRUSTEEW *ppGroup // group ID // ID группы );
// called by COM to allow/ deny access to an object // вызывается COM для разрешения/запрета доступа к объекту HRESULT IsAccessAllowed( [in] PTRUSTEEW pTrustee, // caller's ID // ID вызывающей программы [in] LPWSTR lpProperty, // not used // не используется [in] ACCESS_RIGHTS Rights, // COM_RIGHTS_EXECUTE [out] BOOL *pbAllowed // yes/no! // да/нет! ); }
Этот интерфейс предназначен для того, чтобы разработчики могли создавать объекты контроля доступа на основе статических таблиц данных, преобразующих имена принципалов в права доступа. Интерфейс основывается на новом Windows NT 4.0 API защиты на базе опекуна (trustee), то есть пользователя, обладающего правами доступа к объекту. Основным типом данных, используемым этим API, является TRUSTEE:
typedef struct _TRUSTEE_W { struct _TRUSTEE_W *pMultipleTrustee; MULTIPLE_TRUSTEE_OPERATION MultipleTrusteeOperation; TRUSTEE_FORM TrusteeForm; TRUSTEE_TYPE TrusteeType; switch_is(TrusteeForm)] union { [case(TRUSTEE_IS_NAME)] LPWSTR ptstrName; [case(TRUSTEE_IS_SID)] SID *pSid; }; } TRUSTEE_W, *PTRUSTEE_W, TRUSTEEW, *PTRUSTEEW;
Этот тип данных используется для описания принципала защиты. Первые два параметра, pMultipleTrustee и MultipleTrusteeOperation, позволяют вызывающей программе отличать настоящие регистрационные имена (logins — логины) от попыток заимствования прав. Пятый параметр, ptstrName/pSid, содержит либо идентификатор защиты NT (security identifier — SID), либо текстовое имя учетной записи, подлежащее идентификации. При этом третий параметр, TrusteeForm, указывает, какой именно член объединения (union member) используется. Четвертый параметр, TrusteeType, указывает, является ли данный принципал учетной записью пользователя или группы.
Для связывания опекуна с полномочиями, которые ему даны или в которых ему отказано, в Win32 API предусмотрен тип данных ACTRL_ACCESS_ENTRY:
typedef struct _ACTRL_ACCESS_ENTRYW { TRUSTEE_W Trustee; // who? // кто? ULONG fAccessFlags; // allowed/denied? // разрешено/запрещено? ACCESSRIGHTS Access;// which rights? // какие права? ACCESSRIGHTS ProvSpecificAccess; // not used by COM // в COM не используется INHERIT_FLAGS Inheritance; // not used by COM // в COM не используется LPWSTR lpInheritProperty; // not used by COM // в COM не используется } ACTRL_ACCESS_ENTRYW, *PACTRL_ACCESS_ENTRYW;
а также тип данных для создания списков элементов для опекунов/полномочий:
typedef struct _ACTRL_ACCESS_ENTRY_LISTW { ULONG cEntries; [size_is(cEntries)] ACTRL_ACCESS_ENTRYW *pAccessList; } ACTRL_ACCESS_ENTRY_LISTW, *PACTRL_ACCESS_ENTRY_LISTW;
И наконец, в Win32 предусмотрено еще два дополнительных типа данных, которые позволяют связывать элементы списков доступа с именованными признаками.
typedef struct _ACTRL_PROPERTY_ENTRYW { LPWSTR lpProperty; // not used by COM // не используется в COM ACTRL_ACCESS_ENTRY_LISW *pAccessEntryList; ULONG fListFlags; // not used by COM // не используется в COM } ACTRL_PROPERTY_ENTRYW, *PACTRL_PROPERTY_ENTRYW;
typedef struct _ACTRL_ALISTW { ULONG cEntries; [size_is(cEntries)] ACTRL_PROPERTY_ENTRYW *pPropertyAccessList; } ACTRL_ACCESSW, *PACTRL_ACCESSW;
Хотя в настоящее время COM не использует возможности контроля по каждому признаку, заключенному в этих двух типах данных, тип данных ACTRL_ACCESSW все же используется в интерфейсе IAccessControl для представления списков контроля доступа. Дело в том, что этот интерфейс широко используется также в службе директорий Windows NT 5.0, где требуется контроль доступа по каждому признаку.
В COM предусмотрена реализация интерфейса IAccessControl (CLSID_DCOMAccessControl), которую вызывающие программы могут заполнять явными именами учетных записей и правами доступа, используя типы данных контроля доступа NT 4.0. Следующий фрагмент кода использует эту реализацию для создания объекта контроля доступа, разрешающего доступ для встроенной учетной записи SYSTEM и для пользователей в группе Sales\Managers, но запрещающего доступ для отдельного пользователя Sales\Bob:
HRESULT CreateAccessControl(IAccessControl * &rpac) { rpac = 0; // create default access control object // создаем объект контроля доступа по умолчанию HRESULT hr = CoCreateInstance(CLSID_DCOMAccessControl, 0, CLSCTX_ALL, IID_IaccessControl, (void**)&rpac); if (SUCCEEDED(hr)) { // build list of users/rights using NT4 security data types // создаем списов пользователей/прав, используя типы данных защиты из NT4 ACTRL_ACCESS_ENTRYW rgaae[] = { { { 0, NO_MULTIPLE_TRUSTEE, TRUSTEE_IS_NAME, TRUSTEE_IS_USER, L"Sales\\Bob" }, ACTRL_ACCESS_DENIED, COM_RIGHTS_EXECUTE, 0, NO_INHERITANCE, 0 }, { { 0, NO_MULTIPLE_TRUSTEE, TRUSTEE_IS_NAME, TRUSTEE_IS_GROUP, L"Sales\\Managers" }, ACTRL_ACCESS_ALLOWED, COM_RIGHTS_EXECUTE, 0, NO_INHERITANCE, 0 }, { { 0, NO_MULTIPLE_TRUSTEE, TRUSTEE_IS_NAME, TRUSTEE_IS_USER, L"NT AUTHORITY\\SYSTEM" }, ACTRL_ACCESS_ALLOWED, COM_RIGHTS_EXECUTE, 0, NO_INHERITANCE, 0 } }; ACTRL_ACCESS_ENTRY_LISTW aael = { sizeof(rgaae)/sizeof(*rgaae), rgaae }; ACTRL_PROPERTY_ENTRYW ape = { 0, &aael, 0 }; ACTRL_ACCESSW aa = { 1, &ape }; // present list of users+rights to Access Control object // представляем список пользователей + прав объекту контроля доступа hr = rpac->SetAccessRights(&aa); } return hr; }
Имея эту функцию, приложение может связать вновь созданный объект контроля доступа с его процессом следующим образом:
IAccessControl *pac = 0; HRESULT hr = CreateAccessControl(pac); assert(SUCCEEDED(hr)); hr = CoInitializeSecurity(pac, -1, 0, 0, RPC_C_AUTHN_LEVEL_PKT, RPC_C_IMP_LEVEL_IDENTIFY, 0, EOAC_ACCESS_CONTROL, // use IAccessControl // используем IAccessControl 0);
assert(SUCCEEDED(hr)); pac->Release(); // COM holds reference until last CoUninitialize // COM сохраняет ссылку до последнего CoUninitialize
Флаг EOAC_ACCESS_CONTROL показывает, что первый параметр в функции СоInitializeSecurity является указателем на интерфейс IAccessControl, а не указателем на SECURITY_DESCRIPTOR NT. При каждом поступающем запросе на связь COM будет использовать метод этого объекта IsAccessAllowed для определения того, разрешен или запрещен доступ к объектам процесса. Отметим, что хотя этот код должен исполняться до первого интересного вызова COM, вызов CoCreateInstance для получения реализации по умолчанию IAccessControl является допустимым, так как COM не рассматривает его как интересный.
Если список авторизованных пользователей не может быть известен во время запуска процесса, то можно зарегистрировать специальную (custom) реализацию IAccessControl, которая выполняет определенного рода проверку доступа во время выполнения в своей реализации метода IsAccessAllowed. Поскольку сама COM использует только метод IsAccessAllowed, то такая специальная реализация могла бы безошибочно возвращать E_NOTIMPL для всех других методов IAccessControl. Ниже приведена простая реализация IAccessControl, позволяющая получить доступ к объектам процесса только пользователям с символом "x" в именах своих учетных записей:
class XOnly : public IAccessControl { // Unknown methods // методы IUnknown STDMETHODIMP QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IAccessControl riid == IID_IUnknown) *ppv = static_cast<IAccessControl*>(this); else return (*ppv = 0), E_NOINTERFACE; ((IUnknown*)*ppv)->AddRef(); return S_OK; }
STDMETHODIMP_(ULONG) AddRef(void) { return 2; } STDMETHODIMP_(ULONG) Release(void) { return 1; }
// IAccessControl methods // методы IAccessControl
STDMETHODIMP GrantAccessRights(ACTRL_ACCESSW *) { return E_NOTIMPL; } STDMETHODIMP SetAccessRights(ACTRL_ACCESSW *) { return E_NOTIMPL; } STDMETHODIMP SetOwner(PTRUSTEEW, PTRUSTEEW) { return E_NOTIMPL; } STDMETHODIMP RevokeAccessRights(LPWSTR, ULONG, TRUSTEEW[]) { return E_NOTIMPL; } STDMETHODIMP GetAllAccessRights(LPWSTR, PACTRL_ACCESSW_ALLOCATE_ALL_NODES *, PTRUSTEEW *, PTRUSTEEW *) { return E_NOTIMPL; } // this is the only IAccessControl method called by COM // это единственный метод IAccessControl, вызванный COM STDMETHODIMP IsAccessAllowed( PTRUSTEEW pTrustee, LPWSTR lpProperty, ACCESS_RIGHTS AccessRights, BOOL *pbIsAllowed) { // verify that trustee contains a string // удостоверяемся, что опекун содержит строку if (pTrustee == 0 pTrustee->TrusteeForm != TRUSTEE_IS_NAME) return E_UNEXPECTED; // look for X or x and grant/deny based on presence // ищем "X" или "x" и в зависимости от его наличия // предоставляем или запрещаем *pbIsAllowed = wcsstr(pTrustee->ptstrName, L"x") != 0 wcsstr(pTrustee->ptstrName, L"X") != 0; return S_OK; } }
Если экземпляр вышеприведенного класса C++ зарегистрирован c CoInitializeSecurity:
XOnly xo; // declare an instance of the C++ class // объявляем экземпляр класса C++ hr = CoInitializeSecurity(static_cast<IAccessControl*>(&xo), -1, 0, 0, RPC_C_AUTHN_LEVEL_PKT, RPC_C_IMP_LEVEL_IDENTIFY, 0, EOAC_ACCESS_CONTROL, // use IAccessControl // используем IAccessControl 0); assert(SUCCEEDED(hr));
то от пользователей, не имеющих "x" в именах своих учетных записей, никакие поступающие вызовы не будут приняты. Поскольку имя опекуна содержит в качестве префикса имя домена, этот простой тест также предоставит доступ учетным записям пользователей, принадлежащих к доменам, содержащим "x" в своих именах. Хотя этот тест доступа вряд ли будет слишком полезен, он демонстрирует технологию использования специального объекта IAccessControl с CoInitializeSecurity.
1
Этот класс также реализует интерфейс IPersistStream. Его сериализованный формат распознается SCM с целью записи в элемент реестра AccessPermission во время саморегистрации.
Подводные камни внутрипроцессной активации
Итак, серверы COM были ранее представлены как внутрипроцессные модули кода, загружаемые в активизирующий их процесс с целью создания объектов и выполнения их методов. Для значительного класса объектов это является разумной стратегией развертывания. Эта стратегия, однако, не лишена недостатков. Одним из подводных камней при запуске объекта в клиентском процессе является отсутствие изоляции ошибок. Если объект вызывает нарушение условий доступа или другую фатальную ошибку во время исполнения, то клиентский процесс завершится вместе с объектом. Более того, если программа клиента вызовет какую-либо ошибку, то все объекты, созданные в его адресном пространстве, будут немедленно уничтожены без предупреждения. Эта проблема также относится к тем клиентам, которые решат осуществить нормальный выход, например, когда конечный пользователь закрывает одно из приложений. Когда клиентский процесс завершается, любые объекты, созданные в адресном пространстве клиента, будут уничтожены, даже если внешние клиенты вне процесса хранят легальные импортированные ссылки. Очевидно, что если клиентский процесс прекратится, то при активизации внутри процесса жизнь объекта может быть прервана преждевременно.
Другая возможная ловушка при выполнении клиентского процесса состоит в совместном использовании контекста защиты. Когда клиент активизирует объект внутри процесса, методы объекта выполняются с использованием мандата (credential) защиты клиента. Это означает, что объекты, созданные привилегированными пользователями, могут нанести значительные повреждения. Кроме того, это означает, что объекты, созданные клиентами с относительно меньшей степенью доверия, могут не получить достаточных привилегий для доступа к ресурсам, необходимым для корректного функционирования объекта. К сожалению, нет простого способа обеспечения внутрипроцессного объекта его собственным контекстом защиты.
Еще один подводный камень при внутрипроцессной активации состоит в том, что она не позволяет производить распределенные вычисления.
Если объект должен быть активирован в адресном пространстве клиента, то по определению он разделит CPU (central processor unit — центральный процессор) и другие локальные ресурсы с клиентом. Внутрипроцессная активация также делает затруднительным совместное использование одного и того же объекта несколькими клиентскими процессами. Хотя понятие апартамента и допускает экспорт объектных ссылок из любого процесса (включая и те процессы, которые по традиции рассматривались как клиентские), тем не менее, трудно представить себе семантику активации для совместного использования внутрипроцессного экземпляра.
Для решения этих проблем COM допускает активацию классов в отдельных процессах. При активации в отдельном процессе каждый класс (или группа классов) может иметь свой отдельный контекст защиты. Это означает, что разработчик класса сам контролирует, каким пользователям позволено связываться с его объектами. Кроме того, разработчик класса контролирует, какой набор мандатов защиты должен использовать процесс. В зависимости от фактической упаковки класса разработчик класса также управляет тем, когда окружающий процесс закончится (и закончится ли вообще). Наконец, активация класса в отдельном процессе обеспечивает уровень изоляции ошибок, достаточный для изоляции клиента и объектов от завершения в результате ошибок друг друга.
Приложения
int process_id == fork(); if (process_id == 0) exec(".../bin/serverd");
Аноним, 1981
В предыдущей главе были представлены основы апартаментов COM и проиллюстрирована COM-архитектура удаленного доступа с изрядным количеством деталей. Были исследованы правила управления ссылками на объекты COM в условиях многопоточной среды, а также методика реализации классов и объектов COM, работающих в потоках. В этой главе будут рассматриваться проблемы, возникающие при управлении процессами и приложениями при использовании COM. Основное внимание будет сосредоточено на том, как апартаменты соотносятся с локализацией ошибок, доверительными отношениями и контекстом защиты.
Программируемая защита
Установки, сделанные при помощи CoInitializeSecurity, называются автоматическими установками защиты, поскольку они автоматически применяются ко всем маршалированным объектным ссылкам. Часто бывает, что небольшому числу объектных ссылок необходимо использовать установки защиты, которые отличаются от установок по умолчанию для всего процесса. Наиболее часто встречающийся сценарий таков: для повышения производительности используется довольно низкий уровень аутентификации, но необходимо зашифровать один определенный интерфейс. Вместо того чтобы принудительно использовать шифрование во всем процессе, предпочтительно применить его к тем объектным ссылкам, для которых это необходимо.
Чтобы позволить разработчикам игнорировать автоматические установки защиты на базе интерфейсных заместителей, администратор заместителей выставляет интерфейс IClientSecurity:
[local, object, uuid(0000013D-0000-0000-C000-000000000046)] interface IClientSecurity : IUnknown { // get security settings for interface proxy pProxy // получаем установки защиты для интерфейсного заместителя pProxy HRESULT* QueryBlanket([in] IUnknown *pProxy, [out] DWORD *pAuthnSvc, [out] DWORD *pAuthzSvc, [out] OLECHAR **pServerPrincName, [out] DWORD *pAuthnLevel, [out] DWORD *pImpLevel, [out] void **pAuthInfo, [out] DWORD *pCapabilities );
// change security settings for interface proxy pProxy // изменяем установки защиты для интерфейсного заместителя pProxy HRESULT SetBlanket([in] IUnknown *pProxy, [in] DWORD AuthnSvc, [in] DWORD AuthzSvc, [in] OLECHAR *pServerPrincName, [in] DWORD AuthnLevel, [in] DWORD ImpLevel, [in] void *pAuthInfo, [in] DWORD Capabilities );
// duplicate an interface proxy // дублируем интерфейсный заместитель HRESULT CopyProxy([in] IUnknown *pProxy, [out] IUnknown **ppCopy ); }
Второй, третий и четвертый параметры методов SetBlanket и QueryBlanket соответствуют трем членам структуры данных SOLE_AUTHENTICATION_SERVICE. Под Windows NT 4.0 единственными допустимыми величинами являются соответственно RPC_C_AUTHN_WINNT, RPC_C_AUTHN_NONE и нуль.
Как показано на рис. 6.1, каждый отдельный интерфейсный заместитель имеет свои собственные установки защиты. Метод IClientSecurity::SetBlanket позволяет вызывающей программе изменять эти установки для каждого интерфейсного заместителя по отдельности.
Метод IClientSecurity::QueryBlanket позволяет вызывающей программе прочитать эти установки для отдельного интерфейсного заместителя. В качестве параметров, о которых вызывающая программа не беспокоится, могут быть переданы нулевые указатели. Метод IClientSecurity::СоруРгоху позволяет вызывающей программе копировать интерфейсный заместитель. Это дает возможность делать изменения в копии интерфейса, которая не будет возвращаться при последующих запросах QueryInterface об администраторе заместителей. В идеале установки защиты следовало бы делать только в скопированных интерфейсных заместителях, чтобы изолировать измененный заместитель от нормальной реализации администратора заместителей в QueryInterface; это также позволило бы нескольким потокам независимо друг от друга изменять полные установки защиты между вызовами метода.
Все параметры методов IClientSecurity::SetBlanket и IClientSecurity::QueryBlanket соответствуют параметрам CoInitializeSecurity с одним значительным исключением. Седьмой параметр (pAuthInfo) указывает на набор полномочий клиента. Точная форма этих полномочий специфична для каждого модуля безопасности. Для модуля безопасности NTLM этот параметр может указывать на структуру COAUTHIDENTITY:
typedef struct _COAUTHIDENTITY { OLECHAR *User; // user account name // имя учетной записи пользователя ULONG UserLength; // wcslen(User) // длина имени пользователя OLECHAR *Domain; // Domain/Machine name // имя домена/машины ULONG DomainLength; // wcslen(Domain) // длина имени домена OLECHAR *Password; // cleartext password // пароль открытым текстом ULONG PasswordLength; // wcslen(Password) // длина пароля ULONG Flags; // must be SEC_WINNT_AUTH_IDENTITY_UNICODE // должно быть SEC_WINNT_AUTH_IDENTITY_UNICODE } COAUTHIDENTITY;
Эта структура позволяет клиентам делать вызовы методов COM как любым принципалам защиты, при условии, что они знают открытые тексты паролей для желаемой учетной записи. Если вместо указателя на явную структуру COAUTHIDENTITY передается нулевой указатель, то каждый внешний вызов будет делаться с использованием полномочий вызывающего процесса.
Чаще всего метод IClientSecurity::SetBlanket применяется для повышения уровня аутентификации отдельного заместителя. Следующий код демонстрирует эту технологию:
HRESULT Encrypt(IApe *pApe) { IClientSecurity *pcs = 0; // ask proxy manager for IClientSecurity interface // запрашиваем интерфейс IClientSecurity у администратора заместителей HRESULT hr = pApe->QueryInterface(IID_IClientSecurity, (void**)&pcs); if (SUCCEEDED(hr)) { hr = pcs->SetBlanket(pApe, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IDENTIFY, 0, EOAC_NONE); pcs->Release(); } return hr; }
В идеале вызывающая программа передаст этой функции скопированный интерфейсный заместитель. В противном случае с целью проведения операции копирования эту функцию можно было бы изменить следующим образом:
HRESULT DupeAndEncrypt(IApe *pApe, IApe * &rpSecretApe) { rpSecretApe = 0; IClientSecurity *pcs = 0; // ask proxy manager for IClientSecurity interface // запрашиваем интерфейс IClientSecurity у администратора заместителей HRESULT hr = pApe->QueryInterface(IID_IClientSecurity, (void**)&pcs); if (SUCCEEDED(hr)) { hr = pcs->CopyProxy(pApe, (IUnknown**)&rpSecretApe); if (SUCCEEDED(hr)) hr = pcs->SetBlanket (rpSecretApe, RPC_AUUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_С_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IDENTIFY, 0, EOAC_NONE); pcs->Release(); } return hr; }
Для удобства в COM API предусмотрены оберточные функции вокруг каждого из трех методов IClientSecurity, которые изнутри вызывают QueryInterface для нахождения соответствующего интерфейса IClientSecurity и затем вызывают нужный метод:
// get security settings for interface proxy pProxy // получаем установки защиты для интерфейсного заместителя pProxy HRESULT CoQueryProxyBlanket([in] IUnknown *pProxy, [out] DWORD *pAuthnSvc, [out] DWORD *pAuthzSvc, [out] OLECHAR **pServerPrincName, [out] DWORD *pAuthnLevel, [out] DWORD *pImpLevel, [out] void **pAuthInfo, [out] DWORD *Capabilities);
// change security settings for interface proxy pProxy // изменяем установки защиты для интерфейсного заместителя pProxy HRESULT CoSetProxyBlanket([in] IUnknown *pProxy, [in] DWORD AuthnSvc, [in] DWORD AuthzSvc, [in] OLECHAR *pServerPrincName, [in] DWORD AuthnLevel, [in] DWORD ImpLevel, [in] void *pAuthInfo, [in] DWORD Capabilities);
// duplicate an interface proxy // копируем интерфейсный заместитель HRESULT CoCopyProxy([in] IUnknown *pProxy, [out] IUnknown **ppCopy);
Следующий код представляет собой модифицированную версию предыдущей функции, где используются эти удобные процедуры:
HRESULT DupeAndEncrypt(IApe *pApe, IАре *ArpSecretApe) { rpSecretApe = 0; HRESULT hr = СоСоруProxy(pApe, (IUnknown**)&rpSecretApe); if (SUCCEEDED(hr)) hr = CoSetProxyBlanket(rpSecretApe, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE , 0, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IDENTIFY, 0, EOAC_NONE); return hr; }
Первая версия несколько эффективнее, так как для нахождения интерфейса IClientSecurity в ней использован только один вызов QueryInterface. Для последней версии требуется меньше кода, поэтому и вероятность ошибок в ней меньше.
Важно отметить, что методы IClientSecurity могут применяться только в тех интерфейсах, которые используют интерфейсные заместители. Это означает, что те интерфейсы, которые реализованы локально администратором заместителей (например, IMultiQI, IClientSecurity), не могут использоваться с методами IClientSecurity. Интерфейс IUnknown — это особый случай. IUnknown фактически является локальным интерфейсом, реализованным администратором заместителей. Однако администратору заместителей часто требуется связаться с апартаментом сервера для запроса новых интерфейсов и для освобождения ресурсов, хранящихся в соответствующем администраторе заглушек. Эта связь осуществляется через закрытый интерфейс IRemUnknown, который реализуется библиотекой COM отдельно внутри каждого апартамента. Разработчики могут контролировать полную защиту, использованную для этих вызовов IRemUnknown путем передачи реализации IUnknown администратора заместителей в IClientSecurity::SetBlanket (подобно интерфейсному заместителю, администратор заместителей использует общие для процесса автоматические установки защиты в случае, если функция SetBlanket не вызывается).
Поскольку все интерфейсные заместители агрегированы администратором заместителей, то это, в сущности, означает, что вызовы IClientSecurity::SetBlanket на какой-либо определенный интерфейсный заместитель не влияют на функции QueryInterface, AddRef и Release. Скорее, на них влияют установки, примененные к реализации IUnknown администратором заместителей. Для того чтобы получить указатель на реализацию IUnknown администратором заместителей, можно просто запросить с помощью QueryInterface интерфейсный заместитель для IID_IUnknown. Следующий фрагмент кода демонстрирует эту технологию, отключая защиту как для интерфейсного заместителя, так и для его администратора заместителей:
void TurnOffAllSecurity(IApe *pApe) { IUnknown *pUnkProxyManager = 0; // get a pointer to the proxy manager // получаем указатель на администратор заместителей HRESULT hr = pApe->QueryInterface(IID_IUnknown, (void**)&pUnkProxyManager); assert(SUCCEEDED(hr)); // set blanket for proxy manager // устанавливаем защиту для администратора заместителей hr = CoSetProxyBlanket(pUnkProxyManager, RPC_C_AUTHN_NONE, RPC_C_AUTHZ_NONE, О, RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_ANONYMOUS, 0, EOAC_NONE); assert(SUCCEEDED(hr)); // set blanket for interface proxy // устанавливаем защиту для интерфейсного заместителя hr = CoSetProxyBlanket(pApe, RPC_C_AUTHN_NONE, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_ANONYMOUS, 0, EOAC_NONE); assert(SUCCEEDED(hr)); // release temporary pointer to proxy manager // освобождаем временный указатель на администратор заместителей pUnkProxyManager->Release(); }
Хотя представляется возможным установить и запросить защиту для администратора заместителей, невозможно скопировать администратор заместителей с помощью IClientSecurity::CopyProxy, так как это нарушило бы правила идентификации COM.
Когда ORPC-запрос направляется интерфейсной заглушке, COM создает объект контекста вызова (call context object), представляющий различные аспекты вызова, в том числе установки защиты того интерфейсного заместителя, который отправил этот запрос.
COM связывает этот контекстный объект с потоком, который будет выполнять вызов метода. Библиотека COM выставляет API-функцию CoGetCallContext, позволяющую реализациям метода получить контекст для текущего вызова метода:
HRESULT CoGetCallContext ([in] REFIID riid, [out, iid_is(riid)] void **ppv);
В Windows NT 4.0 единственным интерфейсом, доступным для контекстного объекта вызова, является интерфейс IServerSecurity:
[local, object, uuid(0000013E-0000-0000-C000-000000000046)] interface IServerSecurity : IUnknown { // get caller's security settings // получаем установки защиты вызывающей программы HRESULT QueryBlanket( [out] DWORD *pAuthnSvc, // authentication pkg // модуль аутентификации [out] DWORD *pAuthzSvc, // authorization pkg // модуль авторизации [out] OLECHAR **pServerName, // server principal // серверный принципал [out] DWORD *pAuthnLevel, // authentication level // уровень аутентификации [out] DWORD *pImpLevel, // impersonation level // уровень заимствования прав [out] void *pPrivs, // client principal // клиентский принципал [out] DWORD *pCaps // EOAC flags // флаги EOAC );
// start running with credentials of caller // начинаем выполнение с полномочиями вызывающей программы HRESULT ImpersonateClent(void); // stop running with credentials of caller // заканчиваем выполнение с полномочиями вызывающей программы HRESULT RevertToSelf(void); // test for Impersonation // тест для заимствования прав BOOL IsImpersonating(void); }
IServerSecurity::QueryBlanket возвращает установки полной защиты, фактически использованные для текущего ORPC-вызова (которые могут несколько отличаться от клиентских установок благодаря специфическому для SSP повышению уровней). Как было в случае с IClientSecurity::QueryBlanket, функции IServerSecurity::QueryBlanket также разрешается передавать нуль вместо неиспользуемых параметров. Ниже приведен пример реализации метода, которая гарантирует, что вызывающая программа обеспечила возможность шифрования перед обработкой вызова:
STDMETHODIMP Gorilla::SwingFromTree(/*(in]*/ long nTreeID) { // get current call context // получаем контекст текущего вызова IServerSecurity *pss = 0; HRESULT hr = CoGetCallContext(IID_IServerSecurity, (void**)&pss); DWORD dwAuthnLevel; if (SUCCEEDED(hr)) { // get authentication level of current call // получаем уровень аутентификации текущего вызова hr = pss->QueryBlanket(0, 0, 0, &dwAuthnLevel, 0, 0, 0); pss->Release(); } // verify proper authentication level // проверяем правильность уровня аутентификации if (FAILED(hr) dwAuthnLevel != RPC_C_AUTHN_LEVEL_PKT_PRIVACY) hr = APE_E_NOPUBLICTREE; else hr = this->ActuallySwingFromTree(nTreeID); return hr; }
Как было в случае с IClientSecurity, каждый метод IServerSecurity доступен в качестве удобной API-функции. Приводимая ниже реализация метода использует удобную подпрограмму вместо явного вызова интерфейса IServerSecurity
STDMETHODIMP Gorilla::SwingFromTree(/*[in]*/ long nTreeID) { DWORD dwAuthnLevel; // get authentication level of current call // получаем уровень аутентификации текущего вызова HRESULT hr = CoQueryClientBlanket(0, 0, 0, &dwAuthnLevel, 0, 0, 0); // verify proper authentication level // проверяем правильность уровня аутентификации if (FAILED(hr) dwAuthnLevel != RPC_C_AUTHN_LEVEL_РКТ_PRIVACY) hr = АРЕ_Е_NOPUBLICTREE; else hr = this->ActuallySwingFromTree(nTreeID); return hr; }
И снова мы видим, что последняя версия требует меньше кода и поэтому вероятность ошибок в ней меньше.
Метод IServerSecurity::QueryBlanket также позволяет разработчику объекта находить идентификатор защиты вызывающей программы через параметр pPrivs. Как и в случае с полномочиями, передаваемыми в IClientSecurity::SetBlanket, точный формат этого идентификатора является специфическим для конкретного модуля защиты. Для NTLM этот формат является просто строкой вида
Authority\AccountName
Следующая реализация метода отыскивает идентификатор защиты вызывающей программы с помощью API-функции CoQueryClientBlanket:
STDMETHODIMP Gorilla::EatBanana() { OLECHAR *pwszClientPrincipal = 0; // get security identifier of caller // получаем идентификатор защиты вызывающей программы HRESULT hr = CoQueryClientBlanket(0, 0, 0, 0, 0, (void**)&pwszClientPrincipal, 0); // log user name // регистрируем имя пользователя if (SUCCEEDED(hr)) { this->LogCallerIDToFile(pwszClientPrincipal); hr = this->ActuallyEatBanana(); } return hr; }
При вызове CoQueryClientBlanket для успешного возвращения идентификатора защиты вызывающей программы последняя должна определить:
По крайней мере RPC_C_IMP_LEVEL_IDENTIFY как автоматический (или явный) уровень заимствования прав;
По крайней мере RPC_C_AUTHN_LEVEL_CONNECT как автоматический (или явный) уровень аутентификации.
Если вызывающая программа явно изменила вызывающий принципал в установках полной защиты заместителя с помощью функции COAUTHIDENTITY, то вместо него будет возвращено имя явно заданного принципала.
Точно так же, как можно полностью контролировать установки защиты, использующиеся при вызове метода с помощью интерфейса IClientSecurity, представляется полезным контролировать установки защиты, использованные при вызове на активацию. К сожалению, активационные вызовы являются глобальными API-функциями, не имеющими соответствующего администратора заместителей, откуда можно было бы получить интерфейс IClientSecurity. Для того чтобы позволить вызывающим программам задавать установки защиты для активационных вызовов, каждый активационный вызов принимает структуру СОSERVERINFO:
typedef struct _COSERVERINFO { DWORD dwReserved1; LPWSTR pwszName; COAUTHINFO * pAuthInfo; DWORD * dwReserved2; } COSERVERINFO;
В одной из предыдущих глав было отмечено, что элемент данных pwszName позволяет вызывающей программе осуществлять явный контроль того, какая хост-машина будет обслуживать активационный запрос. Третий элемент данных, pAuthInfo, указывает на структуру данных, которая позволяет вызывающей программе контролировать установки защиты, использованные при осуществлении активационного вызова. Этот параметр является указателем на структуру COAUTHINFO, определенную следующим образом:
typedef struct _COAUTHINFO { DWORD dwAuthnSvc; DWORD dwAuthzSvc; LPWSTR pwszServerPrincName; DWORD dwAuthnLevel; DWORD dwImpersonationLevel; COAUTHIDENTITY * pAuthIdentityData; DWORD dwCapabilities; } COAUTHINFO;
Эти элементы данных соответствуют параметрам IClientSecurity::SetВlanket, однако используются только во время активационного вызова и не влияют на результирующий интерфейсный заместитель.
Следующий фрагмент кода осуществляет активационный вызов, используя структуру COAUTHINFO, чтобы заставить SCM использовать при активационном вызове шифрование (RPC_C_AUTHN_LEVEL_PKT_PRIVACY):
void CreateSecretChimp(IApe *&rpApe) { rpApe = 0; // create a COAUTHINFO that specifies privacy // создаем COAUTHINFO, которая определяет секретность COAUTHINFO cai = { RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IDENTIFY, 0, 0 }; // issue an activation call using the COAUTHINFO // осуществляем активационный вызов с использованием COAUTHINFO COSERVERINFO csi = { 0, 0, &cai, 0 }; IApeClass *pac = 0; hr = CoGetClassObject(CLSID_Chimp, CLSCTX_ALL, &csi, IID_IApeClass, (void**)&pac); assert(SUCCEEDED(hr)); // the activation call occurred with encryption, // but рас is using automatic security settings // активационный вызов произошел с шифрованием, // но пакет использует автоматические установки защиты hr = pac->CreateApe(&rpApe); pac->Release(); return hr; }
Важно отметить, что, поскольку структура COAUTHINFO оказывает воздействие только на сам активационный вызов, результирующий интерфейсный заместитель IApeClass будет использовать автоматические установки защиты, установленные более ранним вызовом CoInitializeSecurity. Это означает, что вызов метода IApeClass::CreateApe будет использовать автоматические установки защиты, а не те, которые определены структурой COAUTHINFO. Для того чтобы гарантировать применение шифрования во время создания или обработки нового Chimp, необходимо модифицировать функцию так, чтобы она ставила полную защиту на заместители обоих интерфейсов — IApeClass и IАре:
// encrypt calls on IApeClass reference // зашифровываем вызовы на ссылку на IApeClass CoSetProxyBlanket(pac, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, О, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_ANONYMOUS, 0, EOAC_NONE); // issue call to create object // осуществляем вызов для создания объекта pac->CreateApe(&rpApe); // encrypt calls on IApe reference // зашифровываем вызовы на ссылку на IApe CoSetProxyBlanket(rpApe, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_ANONYMOUS, 0, EOAC_NONE);
Использование явного вызова COAUTHIDENTITY во время активации может позволить вызывающей программе создавать объекты в процессах, которые в противном случае были бы недоступны принципалу вызывающего процесса. Однако в этом случае вызывающая программа должна гарантировать, что администратор заместителей использует эти же самые полномочия при освобождении интерфейсного указателя, иначе будет утечка ресурсов со стороны сервера. Как уже упоминалось ранее в этой главе, полная защита администратора заместителей контролируется отдельно путем вызова метода IClientSecurity::SetBlanket на реализацию IUnknown администратором заместителей.
1
Важно отметить, что во время написания этого текста структура COAUTHIDENTITY не поддерживается для связей внутри одной машины. Она работает надежно для удаленных связей с хостом.
2
Под Windows NT 5. 0 поддержка заимствования прав на уровне делегирования (delegation-level impersonation) может изменить такое поведение, используя маркер вызывающего потока. За дополнительной информацией обращайтесь к имеющейся документации.
3
Это утверждение нуждается в двух небольших уточнениях. Во-первых, если клиентский процесс был сконфигурирован для использования секретных ссылок в его вызове СоInitializeSecurity, то вызовы IRemUnknown::RemAddRef, IRemUnknown::RemRelease будут произведены с использованием принципала процесса, а не принципала, определенного IClientSecurity::SetBlanket. Во-вторых, до выпуска Windows NT 4.0 Service Pack 4 все вызовы IRemUnknown::RemAddRef, IRemUnknown::RemRelease осуществлялись с использованием принципала процесса, вне зависимости от установок полной защиты, сделанных администратором заместителей.
4
Важно отметить, что так как получателем активационного вызова в начальной стадии является SCM (Service Control Manager — диспетчер управления сервисами) со стороны сервера, то некоторые модули аутентификации могут не поддерживаться. SCM в Windows NT 4.0 поддерживает только NTLM. Для получения более подробной информации о поддерживаемых модулях под Windows NT 5.0 обращайтесь к соответствующей документации.
Снова о времени жизни сервера
В примере, показанном в предыдущем разделе, не было точно показано, как и когда должен прекратить работу серверный процесс. В общем случае серверный процесс сам контролирует свое время жизни и может прекратить работу в любой выбранный им момент. Хотя для серверного процесса и допустимо неограниченное время работы, большинство из них предпочитают выключаться, когда не осталось неосвобожденных ссылок на их объекты или объекты класса. Это аналогично стратегии, используемой большинством внутрипроцессных серверов в их реализации DllCanUnloadNow. Напомним, что в главе 3 говорилось, что обычно сервер реализует две подпрограммы, вызываемые в качестве интерфейсных указателей, которые запрашиваются и освобождаются внешними клиентами:
// reasons to remain loaded // причины оставаться загруженными LONG g_cLocks = 0; // called from AddRef + IClassFactory::LockServer(TRUE) // вызвано из AddRef + IClassFactory::LockServer(TRUE) void LockModule(void) { InterlockedIncrement(&g_cLocks); } // called from Release + IClassFactory::LockServer(FALSE) // вызвано из Release + IClassFactory::LockServer(FALSE) void UnlockModule(void) { InterlockedDecrement(&g_cLocks); }
Это сделало реализацию DllCanUnloadNow предельно простой:
STDAPI DllCanUnloadNow() { return g_cLocks ? S_FALSE : S_OK; }
Подпрограмму DllCanUnloadNow нужно вызывать в случаях, когда клиент решил "собрать мусор" в своем адресном пространстве путем вызова CoFreeUnusedLibraries для освобождения неиспользуемых библиотек.
Имеются некоторые различия в том, как ЕХЕ-серверы прекращают работу серверов. Во-первых, обязанностью серверного процесса является упреждающее инициирование процесса своего выключения. В отличие от внутрипроцессных серверов, здесь не существует "сборщика мусора", который запросил бы внепроцессный сервер, желает ли он прекратить работу. Вместо этого серверный процесс должен в подходящий момент явно запустить процесс своего выключения. Если для выключения сервера используется событие Win32 Event, то процесс должен вызвать API-функцию SetEvent:
void UnlockModule(void) { if (InterlockedDecrement(&g_cLocks) ==0) { extern HANDLE g_heventShutdown; SetEvent(g_heventShutdown); } }
Если вместо серверного основного потока обслуживается очередь событий Windows MSG, то для прерывания цикла обработки сообщений следует использовать некоторые из API-функций. Проще всего использовать PostThreadMessage для передачи в основной поток сообщения WM_QUIT:
void UnlockModule(void) { if (InterlockedDecrement(&g_cLocks) == 0) { extern DWORD g_dwMainThreadID; // set from main thread // установлено из основного потока PostThreadMessage(g_dwMainThreadID, WNLQUIT, О, 0); } }
Если серверный процесс на основе STA знает, что он никогда не будет создавать дополнительные потоки, то он может использовать несколько более простую API-функцию PostQuitMessage:
void UnlockModule(void) { if (InterlockedDecrement(&g_cLocks) == 0) PostQuitMessage(0); }
Этот способ работает только при вызове из главного потока серверного процесса.
Второе различие в управлении временем жизни внутрипроцессного и внепроцессного сервера связано с тем, что должно поддерживать сервер в загруженном или работающем состоянии. В случае внутрипроцессного сервера такой силой обладают неосвобожденные ссылки на объекты и неотмененные вызовы IClassFactory::LockServer(TRUE). Неосвобожденные ссылки на объекты необходимо рассмотреть в контексте внепроцессного сервера.
Безусловно, сервер должен оставаться доступным до тех пор, пока внешние клиенты имеют неосвобожденные ссылки на объекты класса сервера. Для внутрипроцессного сервера это реализуется следующим образом:
STDMETHODIMP_(ULONG) MyClassObject::AddRef(void) { LockModule(); // note outstanding reference // отмечаем неосвобожденную ссылку return 2; // non-heap-based object // объект, размещенный не в "куче" }
STDMETHODIMP_(ULONG) MyClassObject::Release(void) { UnlockModule(); // note destroyed reference // отмечаем уничтоженную ссылку return 1; // non-heap-based object // объект, размещенный не в "куче" }
Такое поведение является обязательным, поскольку если DLL выгружается, несмотря на оставшиеся неосвобожденные ссылки на объекты класса, то даже последующие вызовы метода Release приведут клиентский процесс к гибели.
К сожалению, предшествующая реализация AddRef и Release не годится для внепроцессных серверов. Напомним, что после входа в апартамент COM первое, что делает типичный внепроцессный сервер, — регистрирует свои объекты класса с помощью библиотеки COM путем вызова CoRegisterClassObject. Тем не менее, пока таблица класса сохраняет объект класса, существует по меньшей мере одна неосвобожденная ссылка COM на объект класса. Это означает, что после регистрации своих объектов класса счетчик блокировок всего модуля будет отличен от нуля. Эти самоустановленные (self-imposed) ссылки не будут освобождены до вызова серверным процессом CoRevokeClassObject. К сожалению, типичный серверный процесс не вызовет CoRevokeClassObject до тех пор, пока счетчик блокировок всего модуля не достигнет нуля, что означает, что серверный процесс никогда не прекратится.
Чтобы прервать циклические отношения между таблицей класса и временем жизни сервера, большинство внепроцессных реализации объектов класса попросту игнорируют неосвобожденные ссылки на AddRef и Release:
STDMETHODIMP_(ULONG) MyClassObject::AddRef(void) { // ignore outstanding reference // игнорируем неосвобожденную ссылку return 2; // non-heap-based object // объект, размещенный не в "куче" }
STDMETHODIMP_(ULONG) MyClassObject::Release(void) { // ignore destroyed reference // игнорируем уничтоженную ссылку return 1; // non-heap-based object //объект, размещенный не в "куче" }
Это означает, что после регистрации объектов своего класса счетчик блокировок всего модуля останется на нуле.
На первый взгляд такая реализация означает, что серверный процесс может прекратить работу, несмотря на то, что существуют неосвобожденные ссылки на объекты его класса. Такое поведение фактически зависит от реализации объекта класса.
Напомним, что сервер должен продолжать работу до тех пор, пока на объекты его класса есть внешние ссылки. Предшествующие модификации AddRef и Release влияют только на внутренние ссылки, которые хранятся в таблице классов библиотеки COM и поэтому игнорируются. Когда внешний клиент запрашивает ссылку на один из объектов класса серверного процесса, SCM входит в апартамент объекта класса для отыскания там ссылки на объект класса. В это время делается вызов CoMarshalInterface для сериализации объектной ссылки с целью использования ее клиентом. Если объект класса реализует интерфейс IExternalConnection, то он может заметить, что внешние ссылки являются неосвобожденными, и использовать эти сведения для управления временем жизни сервера. Если предположить, что объект класса реализует интерфейс IExternalConnection, тo следующий код достигает желаемого эффекта:
STDMETHODIMP_(DWORD) MyClassObject::AddConnection(DWORD extconn, DWORD) { DWORD res = 0; if (extconn & EXTCONN_STRONG) { LockModule(); // note external reference // записываем внешнюю ссылку res = InterlockedIncrement(&m_cExtRef); } return res; }
STDMETHODIMP_(DWORD) MyClassObject::ReleaseConnection(DWORD extconn, DWORD, BOOL bLastReleaseKillsStub) { DWORD res = 0; if (extconn & EXTCONN_STRONG) { UnlockModule(); // note external reference // записываем внешнюю ссылку res = InterlockedDecrement(&m_cExtRef); if (res == 0 & bLastReleaseKillsStub) CoDisconnectObject((IExternalConnection*)this, 0); } return res; }
Отметим, что счетчик блокировок модуля будет ненулевым до тех пор, пока существуют неосвобожденные внешние ссылки на объект класса, в то время как внутренние ссылки, удержанные библиотекой COM, игнорируются.
Хотя технология использования IExternalConnection для объектов класса существовала в COM с самых первых дней, лишь немногие разработчики используют ее на деле. Вместо этого большинство серверов обычно игнорируют неосвобожденные внешние ссылки на объекты класса и завершают серверные процессы преждевременно.
Этому положению способствовало присутствие метода LockServer в интерфейсе IClassFactory, который внушает разработчикам мысль, что клиенты будто бы способны в действительности обеспечить выполнение сервера. В то время как большинство разработчиков серверов успешно запирают модуль в методах LockServer, для клиента не существовало надежного способа вызвать данный метод. Рассмотрим следующий клиентский код:
IClassFactory *pcf = 0;
HRESULT hr = CoGetClassObject(CLSID_You, CLSCTX_LOCAL_SERVER, О, IID_IClassFactory, (void**)&pcf); if (SUCCEEDED(hr)) hr = pcf->LockServer(TRUE); // keep server running? // поддерживать выполнение сервера?
В первых версиях COM этот фрагмент кода находился бы в условиях серьезной гонки. Отметим, что существует интервал между вызовами CoGetClassObject и IClassFactory::LockServer. В течение этого периода времени другие клиенты могут уничтожить последний остающийся экземпляр класса. Поскольку неосвобожденная ссылка на объект класса игнорируется наивными реализациями серверов, серверный процесс прекратит работу раньше исходного вызова клиентом метода LockServer. Теоретически это можно было бы преодолеть следующим образом:
IClassFactory *pcf = 0; HRESULT hr = S_OK; do { if (pcf) pcf->Release(); hr = CoGetClassObject(CLSID_You, CLSCTX_LOCAL_SERVER, 0, IID_IClassFactory, (void**)&pcf); if (FAILED(hr)) break; hr = pcf->LockServer(TRUE); // keep server running? // поддерживать выполнение сервера? } while (FAILED(hr));
Отметим, что данный фрагмент кода периодически пытается подсоединиться к объекту класса и заблокировать его, пока вызов LockServer проходит успешно. Если сервер завершит работу преждевременно — между вызовами CoGetClassObject и LockServer, то вызов LockServer возвратит сообщение об ошибке, извещающее об отсоединенном заместителе, что вызовет повтор последовательности. Под Windows NT 3.51 и в более ранних версиях этот нелепый код был единственным надежным способом получения ссылки на объект класса.
Был признан тот факт, что многие реализации серверов не использовали IExternalConnection для должного управления временем жизни сервера, и в версии COM под Windows NT 4.0 введена следующая модернизация для замены этих наивных реализаций.
При маршалинге ссылки на объект класса в ответ на вызов CoGetClass0bject SCM вызовет метод объекта класса IClassFactory::LockServer. С тех пор как значительное большинство серверов реализуют IClassFactory в своих объектах класса, эта модернизация исполняемых программ COM исправляет значительное количество дефектов. Однако если объект класса не экспортирует интерфейс IClassFactory или если сервер должен выполняться и в более ранних версиях COM, чем Windows NT 4.0, то необходимо использовать технологию IExternalConnection.
Следует обсудить еще одну проблему, относящуюся ко времени жизни сервера. Отметим, что когда сервер решает прекратить работу, то он сообщает о том, что главный поток серверного приложения должен начать свою последовательность операций останова (shutdown sequence) до выхода из процесса. Частью этой последовательности операций останова является вызов CoRevokeClassObject для отмены регистрации его объектов класса. Если, однако, были использованы показанные ранее реализации UnlockModule, то появляются условия серьезной гонки. Возможно, что в промежутке между тем моментом, когда сервер сигнализирует главному потоку посредством вызова SetEvent или PostThreadMessage, и тем моментом, когда сервер аннулирует объекты своего класса, вызывая CoRevokeClassObject, в серверный процесс поступят дополнительные запросы на активацию. Если в этот интервал времени создаются новые объекты, то уже нет способа сообщить главному потоку, что прекращение работы — плохая идея и что у процесса появились новые объекты для обслуживания. Для устранения этих условий гонки в COM предусмотрены две API-функции:
ULONG CoAddRefServerProcess(void); ULONG CoReleaseServerProcess(void);
Эти две подпрограммы управляют счетчиком блокировок модуля от имени вызывающего объекта. Эти подпрограммы временно блокируют любой доступ к библиотеке COM, чтобы гарантировать, что во время установки счетчика блокировок новые активационные запросы не будут обслуживаться. Кроме того, если функция CoReleaseServerProcess обнаружит, что удаляется последняя блокировка в процессе, то она изнутри пометит все объекты класса в процессе как приостановленные и сообщит SCM, что процесс более не является сервером для его CLSID.
Следующие подпрограммы корректно реализуют время жизни сервера во внепроцессном сервере:
void LockModule(void) { CoAddRefServerProcess(); // COM maintains lock count // COM устанавливает счетчик блокировок }
void UnlockModule(void) { if (CoReleaseServerProcess() == 0) SetEvent(g_heventShutdown); }
Отметим, что прекращение работы процесса в должном порядке по-прежнему остается обязанностью вызывающей программы. Однако после принятия решения о прекращении работы ни один новый активационный запрос не будет обслужен этим процессом.
Даже при использовании функций CoAddRefServerProcess / CoReleaseServerProcess все еще остаются возможности для гонки. Возможно, что во время выполнения CoReleaseServerProcess на уровне RPC будет получен входящий запрос на активацию от SCM. Если вызов от SCM диспетчеризован после того, как функция CoReleaseServerProcess снимает свою блокировку библиотеки COM, то активационный запрос отметит, что объект класса уже помечен как приостановленный, и в SCM будет возвращено сообщение об ошибке со специфическим кодом (CO_E_SERVER_STOPPING). Когда SCM обнаруживает этот специфический код, он просто запускает новый экземпляр серверного процесса и повторяет запрос, как только новый серверный процесс зарегистрирует себя. Несмотря на системы защиты, используемые библиотекой COM, остается вероятность того, что поступающий активационный запрос будет выполняться одновременно с заключительным вызовом функции CoReleaseServerProcess. Чтобы избежать этого, сервер может явно возвратить CO_E_SERVER_STOPPING как из IClassFactory::Create Instance, так и из IPersistFile::Load в том случае, если он определит, что по окончании запроса на прекращение работы был сделан еще какой-то запрос. Следующий код демонстрирует этот способ:
STDMETHODIMP MyClassObject::CreateInstance(IUnknown *puo, REFIID riid, void **ppv) { LockModule(); // ensure we don't shut down while in call // убеждаемся в том, что не прекращаем работу // во время вызова HRESULT hr; *ppv = 0; // shutdown initiated? // процесс останова запущен? DWORD dw = WaitForSingleObject(g_heventShutdown, 0); if (dw == WAIT_OBJECT_0) hr = CO_E_SERVER_STOPPING; else { // normal CreateInstance implementation // нормальная реализация CreateInstance } UnlockModule(); return hr; }
Во время написания этого текста ни одна из коммерческих библиотек классов COM не реализовывала этот способ.
Управление маркерами
Под Windows NT каждый процесс имеет маркер доступа (access token), представляющий полномочия принципала защиты. Этот маркер доступа создается во время инициализации процесса и содержит различные виды информации о пользователе, в том числе его идентификатор защиты NT (SID), список групп, к которым принадлежит пользователь, а также список привилегий, которыми он обладает (например, может ли пользователь прекращать работу системы, может ли он менять значение системных часов). Когда процесс пытается получить доступ к ресурсам ядра безопасности (например, к файлам, ключам реестра, семафорам), контрольный монитор защиты NT (SRM — Security Reference Monitor) использует маркер вызывающей программы в целях аудита (отслеживания действий пользователей путем записи в журнал безопасности выбранных типов событий безопасности) и контроля доступа.
Когда в процесс поступает сообщение об ORPC-запросе, COM организует выполнение вызова соответствующего метода или в RPC-потоке (в случае объектов, расположенных в МТА), или в потоке, созданном пользователем (в случае объектов, расположенных в STA). В любом случае метод выполняется с использованием маркера доступа, соответствующего данному процессу. В целом этого достаточно, так как это позволяет разработчикам объекта прогнозировать, какие привилегии и права будут иметь их объекты, независимо от того, какой пользователь осуществляет запрос. В то же время иногда бывает полезно, чтобы метод выполнялся с использованием прав доступа клиента, вызывающего метод; чтобы можно было либо ограничить, либо усилить обычные права и привилегии объекта. Для поддержки такого стиля программирования в Windows NT допускается присвоение маркеров защиты отдельным потокам. Если поток имеет свой собственный маркер, контрольный монитор защиты не использует маркер процесса. Вместо него для выполнения аудита и контроля доступа используется маркер, присвоенный потоку. Хотя есть возможность программно создавать маркеры и присваивать их потокам, в COM предусмотрен гораздо более прямой механизм создания маркера на основе ORPC-запроса, обслуживаемого текущим потоком.
Этот механизм раскрывается разработчикам объекта посредством контекстного объекта вызова, то есть вспомогательного объекта, который содержит информацию об операционном окружении серверного объекта.
Напоминаем, что контекстный объект вызова сопоставляется с потоком, когда ORPC-запрос направляется на интерфейсную заглушку. Разработчики объекта получают доступ к контексту вызова через API-функцию CoGetCallContext. Контекстный объект вызова реализует интерфейс IServerSecurity:
[local, object, uuid(0000013E-0000-0000-C000-000000000046)] interface IServerSecurity : IUnknown { // get caller's security settings // получаем установки защиты вызывающей программы HRESULT QueryBlanket( [out] DWORD *pAuthnSvc, // authentication pkg // модуль аутентификации [out] DWORD *pAuthzSvc, // authorization pkg // модуль авторизации [out] OLECHAR **pServerName, // server principal // серверный принципал [out] DWORD *pAuthnLevel, // authentication level // уровень аутентификации [out] DWORD *pImpLevel, // impersonation level // уровень заимствования прав [out] void **pPrivs, // client principal // клиентский принципал [out] DWORD *pCaps // EOAC flags // флаги EOAC );
// start running with credentials of caller // начинаем выполнение с полномочиями вызывающей программы HRESULT ImpersonateClient(void); // stop running with credentials of caller // заканчиваем выполнение с полномочиями вызывающей программы HRESULT RevertToSelf(void); // test for impersonation // проверка заимствования прав BOOL IsImpersonating(void); }
В одном из предыдущих разделов этой главы уже рассматривался метод QueryBlanket. Остальные три метода используются для управления маркерами потока во время выполнения метода. Метод ImpersonateClient создает маркер доступа, основанный на полномочиях клиента, и присваивает этот маркер текущему потоку. Как только возвращается IServerSecurity::ImpersonateClient, все попытки доступа к ресурсам операционной системы будут разрешаться или запрещаться в соответствии с полномочиями клиента, а не объекта.
Метод RevertToSelf заставляет текущий процесс вернуться к использованию маркера доступа, принадлежащего процессу. Если текущий вызов метода заканчивает работу во время режима заимствования прав, то COM неявно вернет поток к использованию маркера процесса. И наконец, метод IServerSecurity::IsImpersonating показывает, что использует текущий поток: полномочия клиента или маркер процесса объекта. Подобно методу QueryBlanket, два метода IServerSecurity также имеют удобные оболочки, которые вызывают CoGetCallContext изнутри и затем вызывают соответствующий метод:
HRESULT CoImpersonateClient(void); HRESULT CoRevertToSelf(void);
В общем случае, если будет использоваться более одного метода IServerSecurity, то эффективнее было бы вызвать CoGetCallContext один раз, а для вызова каждого метода использовать результирующий интерфейс IServerSecurity.
Следующий код демонстрирует использование контекстного объекта вызова для выполнения части кода метода с полномочиями клиента:
STDMETHODIMP MyClass::ReadWrite(DWORD dwNew, DWORD *pdw0ld) { // execute using server's token to let anyone read the value // выполняем с использованием маркера сервера, чтобы // все могли прочитать данное значение ULONG cb; HANDLE hfile = CreateFile("C:\\file1.bin", GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); if (hfile == INVALID_HANDLE_VALUE) return MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, GetLastError()); ReadFile(hfile, pdwOld, sizeof(DWORD), &cb, 0); CloseHandle(hfile);
// get call context object // получаем контекстный объект вызова IServerSecurlty *pss = 0; HRESULT hr = CoGetCallContext(IID_IServerSecurity, (void**)&pss); if (FAILED(hr)) return hr; // set thread token to use caller's credentials // устанавливаем маркер потока для использования // полномочий вызывающей программы hr = pss->ImpersonateClient(); assert(SUCCEEDED(hr)); // execute using client's token to let only users that can // write to the file change the value // выполняем с использованием маркера клиента, чтобы // изменять это значение могли только те пользователи, // которые имеют право записывать в файл hfile = CreateFile("C:\\file2.bin", GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); if (hfile == INVALID_HANDLE_VALUE) hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, GetLastError()); else { WriteFile(hfile, &dwNew, sizeof(DWORD), &cb, 0); CloseHandle(hfile); } // restore thread to use process-level token // восстанавливаем режим использования потоком маркера процесса pss->RevertToSelf(); // release call context // освобождаем контекст вызова pss->Release(); return hr; }
Отметим, что первый вызов CreateFile выполняется с использованием полномочий процесса объекта, в то время как второй вызов — с полномочиями клиента. Если клиент имеет права доступа для чтения/записи в соответствующий файл, то второй вызов метода CreateFile может быть успешным, даже если обычно процесс объекта не имеет доступа к этому файлу.
Важно, что хотя методы IServerSecurity::ImpersonateClient всегда достигают цели, исключая катастрофический сбой, клиент объекта контролирует уровень заимствования прав, допускаемый результирующим маркером. Каждый интерфейсный заместитель имеет свой уровень заимствования прав, который должен быть равным одной из четырех констант (RPC_C_IMP_LEVEL_ANONYMOUS, RPC_C_IMP_LEVEL_IDENTIFY, RPC_C_IMP_LEVEL_IMPERSONATE или RPC_C_IMP_LEVEL_DELEGATE). Во время демаршалинга COM устанавливает этот уровень равным величине, определенной в клиентском вызове CoInitializeSecurity; однако данная установка может быть изменена вручную с помощью IClientSecurity::SetBlanket. Когда объект вызывает IServerSecurity::ImpersonateClient, новый маркер будет ограничен уровнем, заданном в интерфейсном заместителе, который использовался в данном вызове. Это означает, что если клиент задал только уровень RPC_C_IMP_LEVEL_IDENTIFY, то объект не может получить доступ к ресурсам ядра во время выполнения с полномочиями клиента. Объект, однако, может применить API-функции Win32 OpenThreadToken или GetTokenInformation для чтения информации о клиенте (например, ID защиты, групповое членство) из маркера режима анонимного воплощения (impersonation token). Важно отметить, что пока клиент не задал уровень RPC_C_IMP_LEVEL_DELEGATE, объект не может получить доступ ни к одному из удаленных ресурсов защиты, используя полномочия клиента. В их число входят открытие файлов в удаленной файловой системе, а также выполнение аутентифицированных COM-вызовов к удаленным объектам. К сожалению, протокол аутентификации NTLM не поддерживает уровень RPC_C_IMP_LEVEL_DELEGATE, так что под Windows NT 4.0 делегирование невозможно.
Во время предыдущего обсуждения акцент делался на том, что в нормальном режиме методы объекта выполняются с использованием маркера доступа процесса объекта. Однако не обсуждался вопрос о том, как проконтролировать, какой принципал защиты должен использоваться для создания начального маркера серверного процесса. Когда SCM запускает серверный процесс, то он присваивает новому серверному процессу маркер, основанный на конфигурации именованной величины RunAs из AppID. Если же в AppID нет величины RunAs, то считается, что сервер неправильно сконфигурирован для работы в режиме распределенного доступа. Для того чтобы этот тип серверного процесса не внедрял указанные "дыры" в защите в систему, SCM запускает такие процессы с использованием того принципала защиты, который произвел запрос на активацию. Такой тип активации часто называют активацией "как активизатор" ("As Activator"), так как серверный процесс выполняет тот же принципал защиты, что и запускающий пользователь. Активация типа "как активизатор" предназначена для поддержки удаленной активации старых серверов и содержит несколько ловушек. Во-первых, чтобы придерживаться семантики типа "как активизатор", COM запустит отдельный серверный процесс для каждой активационной учетной записи пользователя, независимо от того, используется ли REGCLS_MULTIPLEUSE в CoRegisterClassObject. Это вступает в серьезный конфликт с принципом расширяемости и вдобавок делает невозможным сохранение всех экземпляров класса в одном и том же процессе. Во-вторых, каждый серверный процесс запускается с маркером, ограниченным уровнем RPC_C_IMP_LEVEL_IMPERSONATE, из чего следует, что серверные процессы не имеют доступа ни к каким удаленным ресурсам или объектам.
В идеале серверные процессы конфигурируются для запуска как отдельные принципалы защиты. Управлять этим можно, помещая именованную величину RunAs в имя учетной записи в AppID:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}] RunAs="DomainX\UserY"
Если эта именованная величина присутствует, SCM будет использовать указанное имя учетной записи для создания нового регистрационного маркера (login token) и присвоит этот маркер серверному процессу. Для правильной работы этой схемы требуются два условия. Во-первых, соответствующий пароль должен быть записан в определенном месте реестра в качестве ключа локальных средств защиты (LSA — Local Security Authority). Во-вторых, указанная учетная запись пользователя должна иметь полномочия "Вход в систему как пакетное задание" ("Logon as a batch job"). При установке значения RunAs утилита DCOMCNFG.EXE обеспечивает выполнение обоих этих условий.
Для предотвращения спуфинга (spoofing, получение доступа путем обмана) классов злонамеренными программами CoRegisterClassObject проверяет, зарегистрирован ли AppID данного класса. Если AppID имеет установку RunAs, то COM гарантирует, что принципал вызывающей программы совпадает с именем принципала, записанным в реестре. Если же вызывающая программа не имеет указанной учетной записи RunAs для AppID класса, то вызов метода CoRegisterСlassObject будет отклонен и возвратится известный HRESULT CO_E_WRONG_SERVER_IDENTITY. Поскольку конфигурационные установки COM записаны в защищенной части реестра, только привилегированные пользователи могут изменять список соответствия классов и пользователей.
Важно отметить, что когда в AppID имеется явная учетная запись пользователя RunAs, то SCM всегда будет запускать серверный процесс в его собственной отдельной window-станции (window station). Это означает, что серверный процесс не может с легкостью ни создавать окна, видимые для интерактивного пользователя на данной машине, ни принимать информацию с клавиатуры, от мыши или из буфера (clipboard). Вообще говоря, такая защита полезна, поскольку не дает простым (naive) серверам COM влиять на деятельность пользователя, работающего на машине. К сожалению, иногда серверному процессу бывает необходимо связаться с авторизовавшимся (logged on) в данный момент пользователем.
Одним из способов достижения этого является использование для управления window-станциями и рабочими столами (desktop) явных API-функций COM, что дает потоку возможность временно выполняться на интерактивном рабочем столе. При выполнении на интерактивном рабочем столе любые окна, которые создает поток, будут видимы интерактивному пользователю, и, кроме того, поток может получать аппаратные сообщения (hardware messages) от клавиатуры и мыши. Если же все, что нужно, — это получить от пользователя ответ типа да/нет, то на этот случай в API-функции Win32 MessageBox имеется флаг MB_SERVICE_NOTIFICATION, при выставлении которого, без какого-либо добавочного кода, на интерактивном рабочем столе появится окно сообщения.
Если требуется расширенное взаимодействие с интерактивным пользователем, то использование Win32 API window-станции может стать весьма громоздким. Лучшим подходом могло бы стать выделение компонентов пользовательского интерфейса во второй внепроцессный сервер, который сможет работать на window-станции, отличной от той, на который запущена основная иерархия объектов. Чтобы заставить серверный процесс, содержащий компоненты пользовательского интерфейса, работать при интерактивной пользовательской window-станции, COM распознает характерное значение RunAs "Interactive User" ("Интерактивный пользователь"):
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}] RunAs="Interactive User"
При использовании этого значения COM запускает новый серверный процесс в window-станции, соответствующей подсоединенному в текущий момент пользователю. Для запроса полномочий для нового серверного процесса COM при создании этого нового серверного процесса просто копирует маркер текущего интерактивного сеанса. Это означает, что в реестр не требуется записывать никаких паролей. К сожалению, и этот режим активации не обходится без ловушек. Во-первых, если активационный запрос поступает в момент, когда на хост-машине не зарегистрировано ни одного пользователя, то активационный запрос даст сбой с результатом E_ACCESSDENIED.
Кроме того, если интерактивный пользователь выйдет из сети в тот момент, когда у серверного процесса еще есть подключенные клиенты, то серверный процесс будет преждевременно прерван, что приведет к грубому отсоединению всех существующих в тот момент заместителей. И наконец, часто невозможно предсказать, какой пользователь будет подсоединен во время активации, что усложняет обеспечение достаточных прав и привилегий доступа ко всем необходимым ресурсам для данного объекта. Эти ограничения сводят применимость такого режима активации к простым компонентам пользовательского интерфейса.
Одна интересная разновидность управления маркером и window-станцией серверного процесса относится к службам NT. Напомним, что наличие именованной величины LocalService заставляет SCM использовать для запуска серверного процесса NT Service Control Manager вместо CreateProcess или CreateProcessAsUser. При запуске серверных процессов как сервисов NT COM не контролирует, с каким принципалом запускается этот процесс просто потому, что это жестко запрограммировано в конфигурации соответствующей запущенной службы NT. В этом случае COM игнорирует именованную величину RunAs, чтобы убедиться, что случайные процессы не могут имитировать вызовы CoRegisterClassObject. Наличие именованной величины LocalService требует, чтобы вызывающая программа выполнялась как сервис NT. Если сам этот сервис сконфигурирован на запуск как встроенная учетная запись SYSTEM, то серверный процесс либо запустит интерактивную window- станцию, либо будет запущена заранее определенная window-станция, совместно используемая всеми сервисами NT в качестве SYSTEM (это зависит от того, как именно сконфигурирован сервис NT). Если вместо этого сервис NT сконфигурирован для выполнения как отдельная учетная запись пользователя, то NT Service Control Manager будет всегда запускать сервис NT под новой window- станцией, специфической для данного серверного процесса.
Одно общее соображение в пользу реализации сервера COM как сервиса NT заключается в том, что только сервисы NT способны выполняться со встроенной учетной записью SYSTEM.
Эта учетная запись обыкновенно имеет больший доступ к таким локальным ресурсам, как файлы и ключи реестра. Кроме того, эта учетная запись часто является единственной, которая может выступать как часть доверительной компьютерной базы (trusted computing base) и использовать низкоуровневые службы защиты, доступ к которым был бы опасен из обычных пользовательских учетных записей. К сожалению, хотя учетная запись SYSTEM воистину всемогуща в локальной системе, она полностью бессильна для доступа к защищенным удаленным ресурсам, в том числе к удаленным файловым системам и к удаленным объектам COM. Это обстоятельство делает учетную запись SYSTEM отчасти менее полезной для построения распределенных систем, чем можно было бы ожидать. Вне зависимости от того, используется ли сервер как сервис NT или в качестве традиционного процесса Win32, принято создавать отдельную учетную запись пользователя для каждого приложения COM, которое имеет полные полномочия для доступа в сеть.
1
Из этого, конечно, следует, что вызывающая программа должна была задать уровень не ниже RPC_C_IMP_LEVEL_IMPERSONATE при создании активационного запроса, либо неявно через вызов CoInitializeSecurity, либо явно, используя структуру COAUTHINFO.
2
Обе эти операции могут быть выполнены во время саморегистрации. Посмотрите прекрасный пример DCOMPERM из SDK Win32, приведенный Майком Нельсоном (Mike Nelson).
3
Если в AppID нет установки RunAs (то есть класс сконфигурирован для использования активации в режиме "как активизатор"), то SCM начинает серверный процесс в window-станции активизатора (или в новой window-станции, если активизатор является удаленным клиентом). Это означает, что сервер может взаимодействовать с интерактивным пользователем только в том случае, если сам активизатор окажется интерактивным пользователем.
4
За такую изоляцию приходится платить снижением эффективности. Каждый серверный процесс, который SCM запускает с учетной записью RunAs, потребляет ресурсы на window-станцию и рабочий стол.По умолчанию Windows NT 4.0 сконфигурировано для работы примерно с 14 рабочими столами. Из этого следует, что только 14 (или меньше) серверов типа RunAs могут одновременно работать в конфигурации по умолчанию. В соответствующей статье Q171890 базы знаний фирмы Microsoft (Microsoft Knowledge Base) объясняется, как поднять это ограничивающее число до более приемлемого уровня.
5
Тем не менее, этот режим активации необходим для предотвращения ошибок RPC_E_WRONG_SERVER_IDENTITY при отладке инициализации серверного процесса.