COM

из цикла "COM vs. CORBA"

Автор: Чистяков В.Ю.
Источник: «Технология Клиент-Сервер»

Версия текста: 1.0.1
Что такое COM
COM-объект
CoClass
СОМ-интерфейс
Экземпляр СОМ-объекта
Инициализация и сохранение объектов
Сервер и клиент
Подсчет ссылок
Библиотеки
Создание простого COM-объекта
Установление связи с удаленным объектом
Создание экземпляра объекта и вызов метода
Регистрация компонентов в распределенном приложении
Управление объектами
Агрегация
Делегация
Управление временем жизни экземпляров COM-объектов
Количество экземпляров объектов
Экземпляры и EXE-серверы
Использование нестандартных типов данных в методах удаленного объекта
Два пути
Основные возможности низкоуровневого MIDL/С++ варианта
Высокоуровневый вариант (Automation/VB)
Статическое и динамическое связывание
Обработка ошибок
HRESULT
Возврат информации об ошибках
Получение информации об ошибках
Метаданные объектов
Управление потоками
Масштабируемость
Организация пула объектов.
Just-in-Time (JIT) -активация
Shared Property Manager
Обеспечение безопасности
Безопасность в COM+
Маскировка (Cloaking)
Безопасность в Windows 95 и Windows 98
Устойчивость к сбоям
Context
Транзакции
Compensating Resource Manager
Использование кластеров серверов
Асинхронное взаимодействие между объектами
Queued Components
COM+ Events
Асинхронные вызовы

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

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

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

И то, и другое – стандарты. Оба они, как стандарты, переносимы и независимы от платформы. Преимущество стандарта Microsoft в том, что он создается одной мощной компанией, являющейся крупнейшим игроком на конкурентном рынке программного обеспечения. Мicrosoft старается сделать универсальной и переносимой реализацию, выполненную для конкретных нужд. Для этого Microsoft использует другие открытые стандарты и участвует в работе стандартообразующих организаций, в том числе OMG. Следствием является то, что эталонная реализация сама по себе становится спецификацией. OMG принципиально не располагает такой возможностью. Отсутствие эталонных реализаций CORBA приводит к тому, что для физической совместимости ORB разных производителей необходимо производить отладку и вносить специфический код для каждого из них. СОМ же, позволяя производить отладку на эталонной реализации, гарантирует совместимость приложений разных разработчиков. Если бы OMG кроме стандарта создавала эталонную реализацию, Microsoft проигрывал бы по всем статьям. Но поскольку это не так, подход Microsoft производительнее и создает меньше конфликтных ситуаций.

При сравнении CORBA и COM на память приходит ситуация с IBM PC образца первой половины восьмидесятых. Большая и известная IBM, решив выйти на новый рынок, создала реализацию персонального компьютера. Это не было стандартом, это даже не воспринималось всерьез. Многие конкуренты в то время считали эту реализацию избыточной и обвиняли IBM в переносе своих суперкомпьютерных привычек на устройство, предназначенной для решения примитивных задач. В сравнении с серьезными по тем временам (например, UNIX-совместимыми) компьютерными платформами, IBM РС не представляла интереса в силу явной маломощности. Фирмы производители таких систем громко заявляли, что РС никогда не сможет решать серьезных задач. Но, поскольку IBM располагала огромными ресурсами и авторитетом, эта модель персонального компьютера быстро завоевала рынок, а открытость архитектуры породила огромное количество клонов и привела к тому, что IBM РС стала стандартом "де-факто". Забавно, что теперь появляются стандарты на физическую реализацию РС, например, РС-99, не имеющие никакого отношения к IBM, а выдвигаемые Microsoft, монополистом в области ОС.

Очень похожая ситуация складывается с COM. COM был создан в процессе разработки технологии OLE 2. Но замах при создании COM был куда больше, чем требовалось для спецификации OLE 2. COM разрабатывался, как архитектура взаимодействия между объектами, причем заранее предполагалось, что взаимодействующие объекты могут находиться как в одном процессе, так и в разных. Для первой стадии, OLE 2, достаточно было взаимодействия процессов на одном компьютере. Но COM изначально не был ограничен рамками одного компьютера. Идеология с самого начала строилась так, чтобы в дальнейшем гладко перейти к сетевому взаимодействию. Именно в этом, то есть в исходно локальной природе COM, и пытаются упрекнуть Microsoft его конкуренты. Эти заявления основаны на реальных фактах, но апеллируют к фактам, не относящимся к обсуждаемой проблеме. По большому счету, это демагогия. Посмотрите, насколько эти заявления похожи на обвинения, звучавшие в адрес IBM.

Очень часто в споре о превосходстве той или иной технологии начинаются безосновательные заявления, а зачастую и попыткам необоснованно унизить конкурирующую технологию и их создателей. Основным недостатком, приписываемым COM, обычно является его Microsoft'овское происхождение (в стиле Маздай95). Это простительно для компьютерной прессы, но неприемлемо для технологического диспута, который должен демонстрировать преимущества и недостатки каждой из технологий, как концептуальные, так и технические. Что же касается Microsoft, то, конечно, Microsoft близок к положению монополиста – но на уровне ОС и офисных приложений, а не в объектных технологиях. Здесь у него есть несколько серьезных конкурентов, в число которых, кстати, не входит OMG, но входят некоторые его участники (например, Sun).

То, что на рынке компонентных технологий распределенных вычислений имеется конкуренция, в нашем случае – COM и CORBA, приводит к повышению качества продуктов, что нам с вами исключительно на руку.

Конкуренция производителей CORBA-систем никак не влияет на качество самой спецификации. В это же время конкуренция COM, EJB и CORBA через рыночные механизмы приводит к тому, что Microsoft и Sun, как игроки конкурентного рынка, вынуждены совершенствовать свои технологии и форсировать выпуск своих продуктов на рынок. Если еще раз вспомнить ситуацию с IBM РС, то сейчас мы находимся где-то на уровне поколения 386 процессора. Именно тогда Compaq выпустил компьютер на базе этого процессора раньше, чем IBM. Соответственно, именно сейчас у Microsoft есть возможность либо сделать COM открытым стандартом, либо монополизировать его окончательно и бесповоротно. К счастью, сейчас события развиваются по первому сценарию. Создана организация OpenGroup, занимающаяся вопросами стандартизации COM и переносом его на другие платформы. Стоит заметить, что многие компании одновременно входят и в OMG, и в Open Group.

Многие (например, автор CORBA-раздела этой публикации) глубоко убеждены, что COM и CORBA – диаметрально противоположные вещи. Простой взгляд на реализации этих технологий показывает, что они очень похожи, предназначены для решения одних и тех же задач, и, более того, иногда для реализации поддержки одной технологии используется другая. Так, например, поддержка CORBA в Delphi реализована на базе COM, то есть, базовый CORBA-класс реализует интерфейс IDispatch со всеми вытекающими из этого последствиями. При этом CORBA-интерфейс по существу становится и COM-интерфейсом тоже. В тех же Delphi и C++ Builder существует замечательное средство Midas, позволяющее работать с БД в мноугоровневой среде. Хотя это средство и базируется на COM, транспортом для передачи курсоров по сети может служить метод любого CORBA-интерфейса. При этом данные из SAFEARRAY помещаются в тип Any из CORBA.

Стандарт CORBA 3 во многом описывает вещи, уже присутствующие в COM, например, поддержку нескольких интерфейсов одним объектом. Судя по произведению Inprise (VisiBrocker), одинаковы у них и скоростные характеристики. Так что в COM и CORBA больше похожего, чем различий.

Что такое COM

COM является платформно-независимой, объектно-ориентированной технологией, позволяющей создавать бинарные компоненты. Эти компоненты можно использовать как локально, так и в распределенном сетевом окружении. COM служит основой для: OLE (технология составных документов), ActiveX-объектов и элементов управления ActiveX, DCOM, COM+.

На базе COM создано большинство новейших продуктов (MS Office, MTS, …) и технологий Windows (Automation, Drag & Drop, ...).

Целью этой статьи является сравнение COM и Corba, и, так как Corba на сегодня является технологией используемой, в основном, для организации распределенных вычислений, то и COM мы будем рассматривать с тех же позиций. Поэтому по умолчанию под «COM» будет подразумеваться DCOM и COM+.

DCOM (Distributed COM) – это расширение COM, делающее эту модель распределенной, то есть позволяющей вызывать COM-объекты, находящиеся на другом компьютере в сети.

MTS (Microsoft Transaction Server) – это сервер приложений позволяющий создавать распределенные приложения, поддерживающие транзакции. Вопреки распространенному мнению, хотя слово «транзакция» входит в название этого сервера, приложения создаваемые на базе MTS совершенно не обязаны использовать предоставляемый механизм транзакций.

COM+ – это эволюция COM и MTS. COM+ полностью встроен в Windows 2000. Он существенно расширяет возможности своих предшественников. COM+ обратно совместим с DCOM, MTS и COM, и позволяет создавать распределенные приложения, клиентские части которых можно запускать на старых ОС (Windows 9x и Windows NT).

COM – это технология, позволяющая объектам взаимодействовать, несмотря на границы процесса или машины, так же легко, как и объектам внутри одного процесса. COM обеспечивает такое взаимодействие, определяя, что единственный путь управления данными, ассоциированными с объектом, лежит через интерфейс объекта. Термин «интерфейс», о котором речь пойдет чуть ниже, означает реализацию в коде COM-совместимого двоичного интерфейса, ассоциированного с объектом.

COM-объект

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

Так, в COM присутствует понятие класса. Класс в COM носит название CoClass.

CoClass

CoClass – это класс, поддерживающий набор методов и свойств (один или более), с помощью которых можно взаимодействовать с объектами этого класса. Такой набор методов и свойств называется интерфейсом (Interface).

Каждый CoClass имеет два идентификатора – один из них, текстовый, называется ProgID и предназначен для человека, а второй, бинарный, называется CLSID.

CLSID является глобально уникальным идентификатором (GUID). GUID имеет размер 128 бит и уникален в пространстве и времени. Его уникальность достигается путем внедрения в него информации об уникальных частях компьютера, на котором он был создан, таких, как номер сетевой карты, и времени создания с точностью до миллисекунд. Эта технология, как и большинство других базовых концепций в СОМ, позаимствована из OSF DCE RPC. С помощью CLSID можно точно указать, какой именно объект требуется. Тип данных GUID применяется и для идентификации COM-интерфейсов. В этом случае он называется IID. Сгенерировать новое значение типа GUID можно с помощью API-функции Win32 CoCreateGuid. На практике использовать эту функцию приходится не часто, так как современные средства разработки полностью автоматизируют эту задачу, а VB вообще скрывает от программиста такие тонкости, как работу с CLSID и IID.

Для создания экземпляра объекта используется CLSID. Если имеется только ProgID CoClass’а или CLSID в строковом виде ("{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} ", где X – шестнадцатеричная цифра), то CLSID можно получить, вызвав функцию CLSIDFromString. Для случая с ProgID информация о CoClass’е должна содержаться в реестре машины, на которой производится вызов функции. В реестр информация заносится автоматически при регистрации объекта (во время процедуры инсталляции или при компиляции).

Перевести CLSID, IID или любой другой GUID в строку можно с помощью функции StringFromGUID2. Как уже говорилось выше, практически все необходимые GUID генерируются автоматически, но при необходимости можно сгенерировать GUID вручную, с помощью утилиты guidgen.

Программист никогда не взаимодействует с объектом и его данными напрямую. Для этого используются интерфейсы объектов.

СОМ-интерфейс

COM проводит фундаментальное различие между определением интерфейса и его реализацией. Это свойство СОМ аналогично подходам, принятым в OSF DCE RPC и CORBA. По степени абстракции СОМ ближе к первому из них, так как CORBA менее требовательна к неизменности и уникальности интерфейсов во времени и пространстве.

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

Реализация интерфейса (interface implementation) – это код, который программист создает для выполнения действий, оговоренных в определении интерфейса. Реализации интерфейсов, помещенные в COM-библиотеки или exe-модули, могут использоваться при создании объектно-ориентированных приложений. Разумеется, программист может игнорировать эти реализации и создать собственные. Интерфейсы ассоциируются с CoClass’ами. Чтобы воспользоваться реализацией функциональности интерфейса, нужно создать экземпляр объекта соответствующего класса, и запросить у этого объекта ссылку на соответствующий интерфейс.

Например, для описания взаимодействия с некоторым абстрактным стеком можно определить интерфейс IStack (в COM стало доброй традицией начинать названия интерфейсов с «I»). Этот интерфейс может содержать два метода, скажем, Push и Pop. Вызов метода Pop возвращает значения, заложенные до этого методом Push в стек, но в обратном порядке. Это определение интерфейса не говорит, как функции будут реализованы в коде. Один программист может реализовать стек как массив, а методы Push и Pop – как методы доступа к этому массиву. Другому же взбредет в голову использовать связанный список и соответствующую реализацию методов. Независимо от конкретной реализации методов, представление в памяти указателя на интерфейс IStack, и, соответственно, его использование клиентом полностью специфицируется определением интерфейса.

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

Слово «интерфейс» используется в COM не в том смысле, что в С++. Интерфейс в С++ ссылается на все функции, поддерживаемые классом. COM-интерфейс ссылается на предварительно оговоренную группу связанных функций, реализуемых COM-классом, но не обязательно на ВСЕ функции, поддерживаемые классом.

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

Java-программистам концепция СОМ-интерфейсов будет понятна сразу, без объяснений – в Java интерфейсы выглядят точно так же, как в COM.

Для описания интерфейсов в COM, как и в CORBA, применяется язык IDL. В отличие от CORBA использование IDL не обязательно (СОМ является практически бинарным стандартом).

MIDL

Для описания COM-интерфейсов используется MIDL (MS Interface Definition Language). При создании COM-объектов на C++ использование MIDL является стандартной практикой. Некоторые среды программирования (Delphi, VB) обходятся без MIDL. Delphi имеет свой, Паскале-подобный синтаксис, а в VB любой класс априори является COM-объектом и дополнительное описание не требуется. MIDL является развитием OSF DCE IDL и имеет обратную совместимость с ним.

Как видно из примера, MIDL-описание очень похоже на C++. Главные отличия MIDL от C++ в том, что MIDL позволяет задать только описание интерфейса, и в том, что MIDL содержит дополнительные атрибуты, помещаемые в квадратные скобки. Самым главным атрибутом интерфейса является uuid. Он задает IID интерфейса.

Ниже приведен пример описания интерфейса на MIDL.

[
   uuid(F3792A83-69C9-11D2-AC8C-525400DDA17A),
   helpstring("Этот интерфейс определяет методы работы со стеком.")
]
interface IStack : IUnknown
{
   HRESULT Push([in] VARIANT Val);
   HRESULT Pop([out, retval] VARIANT *pVal);
}

Интерфейс и указатели на интерфейс

Экземпляр «реализации интерфейса» на самом деле является указателем на массив указателей на методы (таблицу функций, ссылающуюся на реализации всех методов, определенных в интерфейсе, также называемую виртуальной таблицей – VTbl). Объекты с несколькими интерфейсами могут предоставлять указатели на несколько таблиц функций. Любой код, содержащий указатель, через который он имеет доступ к массиву, может вызывать методы этого интерфейса.

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

Если компилятор позволяет (что типично для С/С++), клиент может вызвать метод интерфейса по имени, а не по позиции в массиве. Поскольку интерфейс – это тип, зная имена методов, компилятор может проверить типы параметров и возвращаемого значения каждого из методов интерфейса. Напротив, такая проверка типов недоступна даже в С/С++, если клиент использует схему вызова по позиции. К счастью, все современные средства разработки (C++, VB, Delphi, Java, ...) позволяют вызывать методы интерфейсов по имени. Вызов методов интерфейсов по позиции может понадобиться только при формировании COM-вызова, что называется, на лету. Естественно, в повседневной работе прикладного программиста такое практически не случается. На практике для реализации динамического связывания используется специальный интерфейс IDispatch, но о нем речь пойдет позже (в разделе «Динамическое связывание»).

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

В COM нет нужды в том, что в Corba называется строковым связыванием или объектной ссылкой, записанной в строку. В отличие от CORBA-объектов, объекты COM не живут после смерти. Поэтому ссылки нужно получать только на загруженный в память экземпляр объекта (вернее на один из его интерфейсов). Получить ссылку на объект можно, попросту создав его, или запросив эту ссылку у другого объекта. Есть некоторые особенности при передаче указателей на интерфейс другому потоку того же приложения. Эта тема рассматривается в разделе "Управление потоками".

Интересной задачей является определение того, указывают ли два указателя на интерфейсы разных объектов или одного и того же . Сделать это очень просто. Надо запросить от каждого из них указатель на IUnknown (см. ниже) и просто сравнить их. Если они равны, значит, они получены от одного и того же объекта. Это верно даже в случае, если указатели получались через дюжину компьютеров, а сам объект находится на другой стороне земного шара. Не меняет этого правила даже то, что объект был получен агрегированием и на самом деле создан из нескольких других. Главное, чтобы указатели на интерфейс были получены легальными путями.

IUnknown и наследование интерфейсов

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

Наследование в стандартных COM-интерфейсах используется довольно скупо. Все интерфейсы должны быть унаследованы от интерфейса IUnknown. IUnknown содержит три жизненно важных для COM метода: QueryInterface, AddRef и Release. Все COM-объекты должны реализовать интерфейс IUnknown, поскольку он обеспечивает возможность получать поддерживаемые объектом интерфейсы с помощью QueryInterface, и управлять сроком жизни объекта через AddRef и Release.

Высокоуровневые средства разработки (такие, как VB) полностью скрывают реализацию IUnknown, облегчая тем самым работу программиста. Низкоуровневые средства разработки (например, VC++) напротив, дают полный доступ к исходным текстам, реализующим IUnknown. На C++ или Delphi можно самостоятельно реализовать IUnknown. Но проще воспользоваться стандартными реализациями. Для C++ самый удобный, но в тоже время гибкий и очень компактный способ реализации IUnknown – воспользоваться библиотекой ATL (Active Template Library). ATL – это библиотека, главным образом состоящая из набора шаблонов C++. ATL упрощает работу с COM, предоставляя реализации для многих стандартных интерфейсов. Эта библиотека входит в поставку MS VC++ и Borland C++ Builder. В сочетании с визуальными средствами разработки этих сред ATL позволяет очень быстро создавать сложные приложения, базирующиеся на COM.

Экземпляр СОМ-объекта

Чтобы воспользоваться функциональностью COM-объекта, нужно создать его экземпляр (instance).

Экземпляр COM-объекта аналогичен объекту в C++ или VB, за тем исключением, что он может быть создан в другом процессе или на другом компьютере.

Для создания экземпляра COM-объекта на C++ применяются функции CoCreateInstance, CoCreateInstanceEx, CoGetObject, CoGetClassObject, CoGetInstanceFromFile, CoGetInstanceFromIStorage, OleLoadFromStream и некоторые другие, всех и не упомнишь. На VB все немного проще. Там есть две универсальные функции, CreateObject и GetObject, и оператор new. Все объекты VB являются COM-объектами.

Чаще всего применяются функции CoCreateInstance, CoCreateInstanceEx, CreateObject и оператор new .

Чтобы создать экземпляр объекта с помощью CoCreateInstance, этой функции необходимо передать: CLSID требуемого объекта, контекст создаваемого объекта, IID требующегося интерфейса, и указатель, в который и будет возвращен указатель на интерфейс созданного объекта.

Контекст создаваемого объекта – это информация о том, где должен создаваться объект: в том же процессе (адресном пространстве EXE-модуля), в другом процессе того же компьютера, на удаленном сервере, или как локальная заглушка для удаленного объекта.

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

Функция CoCreateInstance была создана на заре существования COM-модели и предназначена для локального COM. Она не позволяет создавать объекты на конкретном удаленном сервере, вместо этого она берет информацию о сервере из реестра. Информацию о местонахождении сервера и атрибуты защиты можно настроить с помощью утилиты DCOMCNFG или средств администрирования MTS или COM+. К недостаткам CoCreateInstance можно отнести также то, что при ее использовании нет возможности задать учетную запись (и пароль), от имени которой будет создаваться объект, и то, что она позволяет получить указатель только на один интерфейс создаваемого объекта. Для локально создаваемого объекта эти ограничения не существенны – атрибуты защиты при этом не работают (имеется прямой доступ к объекту), а указатель на другой интерфейс можно молниеносно получить с помощью вызова метода IUnknown::QueryInterface (ведь по принципам COM любой COM-интерфейс должен быть унаследован от IUnknown). Вызов же QueryInterface у удаленного объекта приводит к передаче данных по сети, что значительно медленней локального вызова.

Всех этих недостатков лишена новая версия этой функции – CoCreateInstanceEx.

Остальные функции позволяют создать объект, загрузив его состояние из разных источников (OleLoadFromStream, CoGetInstanceFromFile, CoGetInstanceFromIStorage), или создать объект с помощью моникера и строки параметра (CoGetObject).

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

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

Создание моникера – задача сложная, и по плечу она не каждому. Спасает то, что имеется много стандартных реализаций моникеров. Так, через них делается связывание документов в OLE, создание Queued-объектов (асинхронно работающих объектов повышенной надежности, о них речь пойдет дальше), и получение указателей на WMI-объекты, интерфейс новой технологии Windows 2000 – Active Directory. Существует интересная разновидность моникеров – URL-моникеры, которые позволяют получить указатель на объект, основываясь на URL.

Экземпляр СОМ-объекта можно сравнить с экземпляром CORBA-объекта, сопоставленного с конкретным сервантом. Его, однако, нельзя сравнивать с экземпляром CORBA-объекта, так как последний может виртуально существовать после смерти его серванта и воскреснуть в новом облике с другим сервантом.

Инициализация и сохранение объектов

COM-объекты не имеют конструкторов, привычных для объектно-ориентированных языков программирования. Вместо этого можно инициализировать объекты (или загружать в них заранее сохраненное состояние) с помощью стандартных IPersistXXX COM-интерфейсов. Вот список этих интерфейсов: IPersistStream, IPersistStreamInit, IPersistStorage, IPersistFile, IPersistMemory, IPersistPropertyBag. Всех их объединяет то, что они унаследованы от интерфейса IPersist, а назначение каждого понятно из его названия. Хотелось бы обратить внимание на два интерфейса – IPersistStreamInit и IPersistPropertyBag. Первый имеет метод InitNew, предназначенный для инициализации новых объектов, а второй позволяет сохранять информацию в виде читабельного текста. IE и VB используют эти интерфейсы для сохранения и загрузки состояния элементов управления ActiveX, встроенных в html-странички и в VB-формы. Эти интерфейсы можно использовать и для сохранения/загрузки объектов в БД на удаленном сервере или для передачи COM-объектов через параметры методов удаленных объектов по значению (в CORBA аналогичная возможность носит название ValueType).

Сервер и клиент

В отличие от интерфейса, COM-объект – это не абстракция, и его код должен где-то храниться. Можно, конечно, создавать и использовать COM-объекты в одном исполняемом файле, но значительно интересней помещать COM-объекты в независимые модули. COM позволяет помещать COM-объекты в DLL- или EXE-модули и подгружать их при необходимости. Причем программисту даже не надо следить за подгрузкой необходимых модулей. За него это делает COM. Программист просто говорит, что ему надо загрузить некоторый объект, а COM лезет в реестр той машины, на которой надо создать объект, берет оттуда нужную информацию, загружает необходимый модуль (если он еще не загружен), создает объект и возвращает ссылку на него.

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

Здесь есть одна тонкость. Как известно, новый процесс создается только при загрузке EXE-модуля, а DLL грузится в адресное пространство того процесса, в котором требуется его функциональность. Значит, компоненты, помещенные в DLL-модули, не могут создаваться удаленно. Это именно так, но не все так плохо... Если создать COM-объект в одном процессе, то ничто не мешает передать указатель на его интерфейс в другой процесс. Это достигается реализацией одного COM-объекта как EXE-сервера. У этого объекта создается метод, который получает на вход CLSID объекта (объект должен быть зарегистрирован на той же машине, где создан первый COM-объект) и IID интерфейса (который изначально требуется от второго объекта). Ему же нужно передать указатель, через который будет возвращен интерфейс создаваемого объекта. В общем, метод первого объекта должен походить на функцию CoCreateInstance. Вызывая этот метод, можно удаленно создавать объекты, расположенные в DLL-модулях на другом компьютере. Процесс, в который загружается DLL на удаленном компьютере, называется суррогатным процессом. Можно даже скрыть все эти ухищрения от программиста, специальным образом зарегистрировав объект на вызывающей машине.

Именно это и сделано в MTS и COM+, причем так искусно, что создание объекта таким образом ничем не отличается от создания объектов традиционным для DCOM образом.

Более того, MTS и, особенно, COM+ предоставляют множество дополнительных функций и упрощают создание и внедрение распределенных приложений.

Подсчет ссылок

Для каждой реализации интерфейса ведется подсчет ссылок (Reference counting). Любой код, который хочет хранить указатель (ссылку) на интерфейс, должен увеличить счетчик ссылок этого интерфейса. Сделать это можно, вызвав у интерфейса метод AddRef (Помните? Каждый COM-интерфейс унаследован от IUnknown.). Пока у объекта счетчик ссылок не равен нулю, этот объект не будет уничтожен. Чтобы уменьшить счетчик, надо вызвать метод Release того же интерфейса. COM-объект жив, пока хотя бы у одного интерфейса счетчик ссылок больше нуля. Обычно сразу после создания объекта имеется только один указатель на его интерфейс со счетчиком ссылок, установленным в единицу. Так что, если в этот момент вызвать у его интерфейса метод Release, объект автоматически уничтожится.

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

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

В распределенных системах DCOM берет на себя управление подсчетом ссылок на удаленные объекты. Даже если клиент потерпит крах и не сможет самостоятельно освободить сделанные им ссылки, DCOM через некоторое время сделает это за него. Чтобы избежать чрезмерной услужливости DCOM, proxy объекты, создаваемые тем же DCOM, регулярно сообщают о своем существовании, производя так называемый ping. Однако есть две проблемы, которые портят радужную картину.

Первая – это то, что если следить за ссылками вручную, довольно-таки просто ошибиться и забыть сделать AddRef или Release. Лишний AddRef приведет к тому, что объект не освободится до закрытия клиентского приложения. Лишний Release приведет к появлению неверных указателей в клиентском приложении, что, в свою очередь, приводит к его краху. Как уже говорилось, сетевым подсчетом ссылок занимается сам DCOM, и проблема со ссылками в клиентском приложении не может повлиять на работоспособность сервера. В некоторых языках проблема подсчета ссылок в клиентском приложении решена путем встраивания механизма автоматического подсчета ссылок в сам язык. Так сделано в VB и Delphi. А в C++ эта проблема может быть решена средствами самого языка. Для ее решения используются три свойства языка.

  1. Автоматический вызов деструктора при уничтожении переменных.
  2. Автоматическое уничтожение стековых переменных.
  3. Поддержка шаблонов. Используя две предыдущие возможности, можно создать классы-обертки для интерфейсов, которые автоматически вызывают Release при своем уничтожении. Но такую обертку пришлось бы писать для каждого интерфейса. Шаблоны позволяют написать одну реализацию такого класса-обертки, а компилятор сам создаст реализацию для каждого интерфейса.

В сочетании с перегружаемыми операторами классы-обертки практически полностью снимают проблему подсчета ссылок в C++. Лучшей реализацией такого класса-обертки является шаблон CComPtr<*> из библиотеки ATL. Переменная, созданная на базе такого класса-обертки, называется Smart Pointer, то есть «умный указатель». Объявив вместо простого указателя на интерфейс (например, IStak * pIStak) Smart Pointer, (CComPtr<IStak> * spIStak), можно не заботится о правильности подсчета ссылок.

Вторая проблема – это то, что если два объекта делают ссылки друг на друга, то они попадают в deadlock, взаимно блокируя друг друга. Эта ситуация возникает нечасто, но если уж возникает, ее решение отнимает много нервов и времени. Есть два пути решения этой проблемы. Один – нарушить соглашения COM, и не делать увеличения счетчика ссылок одному из объектов. При этом время жизни объекта, которому не делается увеличение счетчика, должно быть заведомо больше, чем время, на которое требуется указатель на его интерфейс. Этот путь опасен и не всегда работает в сетевом окружении. Второй путь – создать промежуточный (третий) объект, который и подсунуть объекту, приводящему к взаимоблокировке. При этом на третий объект делается две ссылки, а на первый ссылок не делается вообще. В момент, когда первый объект уничтожается, он отпускает первый и третий. Далее уничтожается второй, и, в свою очередь, отпускает третий, что приводит к уничтожению третьего объекта. Второй способ избежания взаимоблокировок является более предпочтительным, но требует дополнительного кода. Очень плохо, что COM не взял на себя решение проблем с взаимоблокировкой.

Библиотеки

COM-объекты помещаются в библиотеки. Чаще всего это файл с расширением DLL, но иногда библиотеки помещаются в EXE- или OCX-модули. Расширение OCX всего лишь говорит о том, что в библиотеке находится элементы управления ActiveX, по существу это тоже DLL. EXE-формат (исполняемый файл) используется в двух случаях: 1) когда надо создать отдельный COM-сервер, не требующий при своей работе наличия MTS или COM+, 2) когда создается настольное Windows-приложение, которое должно поддерживать Automation (выставлять некоторую объектную модель, доступную из других приложений).

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

[
   uuid(96479D03-B2D5-11D3-AE78-004095E1F072),
   version(1.0),
   helpstring("Stack Type Library v 1.0")
]
library StackLib
{
   [
      uuid(95F3208B-F9AC-11d3-A6AD-0050BAC0EF0F),
      helpstring("Iauaeo Stack")
   ]
   coclass Stack
   {
      [default] interface IStack;
   };
}

В теле библиотеки содержится описание входящих в нее объектов. На основании такого описания MIDL генерирует библиотеку типов. Как и другие объекты в COM, библиотеки должны иметь свой уникальный идентификатор (GUID). В применении к библиотеке он называется LibID. В случае DLL библиотека типов прилинковывается к DLL, а в случае с EXE обычно поставляется отдельно в файле с расширением TLB или OLB. Как бы там ни было, библиотека типов регистрируется в системе, и с ее помощью можно получать информацию о любом элементе библиотеки. С помощью библиотеки типов можно даже сгенерировать MIDL-описание для всех элементов библиотеки. В поставку MS Visual Studio входит утилита «OLE View», позволяющая просматривать библиотеки типов и получать MIDL-описание.

Создание простого COM-объекта

Создавать COM-объекты очень просто. Однако для создания проекта COM-библиотеки на C++ нужно написать довольно много одинакового (повторяющегося из проекта в проект и занудного) кода. Чтобы не делать этого каждый раз, лучше всего воспользоваться библиотекой ATL (о ней уже говорилось ранее) и соответствующими визардами (речь идет про Microsoft VC и Borland C++ Builder). На VB вообще надо только выбрать тип проекта «ActiveX DLL». При этом VB создаст класс. Любой метод или свойство этого класса, не помеченный как Private, будет являться методом или свойством COM-объекта. Пример, который далее будет реализован на C++, на VB будет выглядеть так:

Public Function Summa(ByVal P1 As Long, ByVal P2 As Long)
    Summa = P1 + P2
End Function

В C++ (и на VC и на C++ Builder) надо создать проект. В VC надо выбрать меню «File\New» и в появившемся визарде на закладке «Projects» вбить имя проекта, путь к нему, и нажать Enter. Назовем проект «VCTest1». На следующей странице достаточно еще раз нажать Enter, так как ее установки полностью нас удовлетворяют. Появится окно, перечисляющее, какие файлы будут созданы. Это последний шанс отменить создание проекта. Еще раз нажмем Enter.


Рис. 1

Итак, что же создает этот визард?

Если скомпилировать проект, получится VCTest1.dll, не содержащая ни одного объекта. Поэтому вместо компиляции добавим COM-объект.

Для этого из меню Insert нужно выбрать пункт «New ATL Object». Появится «ATL Object Wizard». Надо выбрать «Objects\Simple Objects» и нажать кнопку «Next >» (см. Рис.1).

На этом шаге визард попросит задать имя объекта. Сделать это можно в поле «Short Name». Введем в него строку «VCTestObj» (см. рис.2).


Рис. 2

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

  1. VCTestObj.h – содержащий описание C++-объекта (CVCTestObj), являющегося реализацией нашего COM-объекта;
  2. VCTestObj.cpp – сейчас он пуст (в будущем в него будет добавляться код для объекта CVCTestObj);
  3. VCTestObj.rgs – файл, содержащий описание – информацию, которую надо занести в реестр.

В IDL-файл при этом добавилось описание нового объекта и нового интерфейса.

Осталось добавить метод, и можно компилировать проект.

Для этого следует переключиться на закладку «ClassView» в окне «Workspace», щелкнуть правой кнопкой мыши по имени интерфейса (IVCTestObj) и в контекстном меню выбрать пункт «Add Method» (см. Рис.3).


Рис. 3

В появившемся диалоге надо ввести имя метода и описание его параметров, как это показано на Рис.4.

После нажатия Enter в описании интерфейса (файл VCTest1.idl) и в описании объекта C++ (файлы VCTestObj.h и VCTestObj.cpp) добавится описание нового метода. Осталось перейти к новому методу и внести необходимый код:

*piResult = iP1 + iP2;


Рис. 4

Перейти к нужному методу можно, выбрав его на закладке «ClassView». Результат этих действий показан на Рис.5.


Рис. 5

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

Единственное, что необычно в приведенном примере – это то, что результат возвращается через параметр, а не как возвращаемое значение функции. Это вызвано тем, что возвращаемое значение в методах COM-объекта зарезервировано для возврата кода ошибки (см. «Обработка ошибок»).

Установление связи с удаленным объектом

Создание экземпляра объекта и вызов метода

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

#include "stdafx.h"
#include <stdio.h>
#include <conio.h>
#include "objbase.h"
#include "..\VCTest1.h"
#include "..\VCTest1_i.c"
int main(int argc, char* argv[])
{
   // Инициализируем COM
   CoInitialize(NULL);
   IVCTestObj * pIVCTestObj;
   // Создаем объект - VCTestObj
   HRESULT hr = CoCreateInstance(CLSID_VCTestObj,
      NULL, CLSCTX_SERVER,
      IID_IVCTestObj, (void**)&pIVCTestObj);
   if(SUCCEEDED(hr))
   {
      // Если объект создан...
      int iP1 = 1, iP2 = 2, iResult = 0;
  
      // Вызываем метод COM-объекта...  
      hr = pIVCTestObj->Summa(iP1, iP2, &iResult);
      // Освобождаем ссылку на COM-объект...  
      pIVCTestObj->Release();
      printf("Call 'Summa(%d, %d)' return %d.\n", iP1, iP2, iResult);
   }
   printf( "\n\nError code: 0x%08x\n\n"
      "Press eny key to continue...\n", hr);
   _getch(); // Ожидаем ввода символа (для удобства отладки)
  
   CoUninitialize(); // Деинициализируем COM
   return 0;
}

В этом примере, CLSID объекта, IID и описание интерфейса берутся из сгенерированных компилятором MIDL для первого проекта (см. «Создание простого COM-объекта») файлов (VCTest1.h и VCTest1_i.c).


Рис. 6

Такой способ удобно применять, если и объект и вызывающее его приложение созданы на C++ и доступны описания в текстовом виде. Если же вам доступна только бинарная библиотека, то можно воспользоваться расширением C++ – директивой «#import». Это расширение сделано Microsoft. Если я не ошибаюсь, оно доступно начиная с пятой версии VC. Чтобы получить результат, аналогичный включению файлов VCTest1.h и VCTest1_i.c, надо использовать следующий синтаксис:

#import "..\Debug\VCTest1.dll" \
   no_implementation no_namespace \
   named_guids raw_interfaces_only

С помощью директивы «#import» также можно создать классы-обертки, позволяющие упростить работу с включенными в библиотеку компонентами. Подробнее об этой директиве можно узнать в документации по VC.

В C++ Builder тоже реализована поддержка «#import», но, к сожалению, криво. В документации предлагается для взаимодействия с COM-объектами пользоваться импортом библиотек типов. Для этого нужно в меню «Project» выбрать пункт «Import Type Library», найти в списке необходимую библиотеку типов и нажать кнопку «Install». При этом создаются файлы описания и классы-обертки. После перекомпиляции пакеджа (в который происходит импорт) в палитре компонентов, на закладке «ActiveX» появятся новые компоненты, в нашем случае один «VCTestObj». Если поместить один из них на форму, то с его помощью можно будет производить вызовы методов COM-объекта. Вот как выглядит вызов метода нашего тестового объекта:

VCTestObj1->Connect();
ShowMessage("Summa(1, 2) = " + String(VCTestObj1->Summa(1, 2)));
VCTestObj1->Disconnect();

Метод Connect создает экземпляр объекта, а Disconnect освобождает ссылку (вызовом Release). Таким образом, объект создается и, после вызова метода Summa, уничтожается. Если надо обеспечить жизнь объекта на протяжении всей жизни формы, то можно вместо вызова Connect установить свойство AutoConnect в True во время разработки. Это приведет к тому, что COM-объект создастся при загрузке формы. Метод Disconnect в этом случае тоже можно проигнорировать, при уничтожении формы ссылка на COM-объект будет освобождена автоматически.

Этот механизм C++ Builder позаимствовал у Delphi. Сравните код Delphi с приведенным ранее.

VCTestObj1.Connect();
ShowMessage('Summa(1, 2) = ' + IntToStr(VCTestObj1.Summa(1, 2)));
VCTestObj1.Disconnect();

Еще одна полезная черта «оберток» заключается в том, что они преобразуют обработку ошибок «в стиле COM» в схему, принятую для базового языка.

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

Dim obj As New VCTestObj
MsgBox "Summa(1, 2) = " & obj.Summa(1, 2)

Регистрация компонентов в распределенном приложении

В приведенных выше примерах предполагается, что сервер соответствующим образом зарегистрирован. По умолчанию DLL-библиотека, в которую помещен COM-объект, регистрируется как внутрипроцессный (in-process) сервер. Это приводит к тому, что DLL будет грузиться внутрь каждого процесса, который попытается создать находящийся в ней объект. Чтобы с объектами из этой DLL-библиотеки можно было работать по сети, необходимо зарегистрировать ее в COM+ или MTS. Вот как выглядит эта операция.

Первое что необходимо сделать, это открыть COM+ Component Services (MTS Explorer) и добраться до папки «My Computer». В ней необходимо выбрать папку «COM+ Applications» и создать (или открыть, если оно уже существует) новое приложение COM+. Создать новое приложение можно, выбрав из меню «Action» (или из контекстного меню) пункт «New\Applications».

В приложение COM+ необходимо добавить компоненты. Это делается аналогично созданию приложения, с той лишь разницей, что в этот раз надо выбрать папку «Components» приложения. Результат регистрации должен быть похож на Рис. 6.

В одно приложение можно помещать компоненты из разных библиотек. Приложение олицетворяет процесс, в котором будут загружаться и исполняться компоненты. Если есть необходимость загружать компоненты в разных процессах, то нужно помещать их в разные приложения COM+.

COM+ Component Services (для Windows NT 4.0 – MTS Explorer) позволяют не только производить первичную регистрацию компонентов, но и дают возможность настраивать их характеристики, производить отладку компонентов, осуществлять их мониторинг и даже создавать инсталляторы для распределенных приложений.

Чтобы зарегистрировать приложение COM+ на клиентской машине, необходимо с помощью COM+ Component Services (MTS Explorer) создать proxy-инсталлятор. Затем нужно прогнать этот инсталлятор на каждой клиентской машине. Деинсталляция COM+-приложения, как и любого другого Windows-приложения, доступна через «Control Panel» – «Add\Remove Programs».

Не-СОМ+-приложения можно зарегистрировать и администрировать с помощью утилиты DCOMCNFG

Управление объектами

Агрегация

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

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

Делегация

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

Управление временем жизни экземпляров COM-объектов

Время жизни COM-объекта ограничено временем жизни серверного приложения, в котором создан этот объект. К тому же объект самоуничтожается при освобождении (вызовом метода Release) последней ссылки на него. Тем не менее, на практике это не приводит к каким-нибудь ограничениям. Если логическое время жизни объекта должно превышать время жизни экземпляра объекта в оперативной памяти, можно просто сохранить в файл или в поле БД его состояние перед уничтожением, а когда он понадобится, загрузить его вновь. Сохраненный объект может путешествовать между компьютерами, например, через параметр удаленного объекта или в электронном письме. На нужном компьютере надо просто создать новый экземпляр нужного класса и инициализировать его состояние пришедшими данными.

Сохранение и загрузку объектов можно инкапсулировать в методах другого удаленного объекта. Например, можно создать объект с методами Save и Load, передавая им в качестве параметров идентификатор БД или имя файла и указатель на интерфейс сохраняемого (загружаемого) объекта. Часто для этого применяются моникеры. OLE DB и ADO позволяют записывать в БД объекты, поддерживающие стандартные интерфейсы IPersistXXX (см. «Инициализация и сохранение объектов»).

COM+ даже позволяет вызывать методы объектов на выключенном компьютере. Эти возможности освещается в разделе «Асинхронное взаимодействие между объектами».

Количество экземпляров объектов

При вызове CoCreateInstance вы не обязательно получаете указатель на новый объект. Вполне возможно, что вы получите указатель на уже созданный, разделяемый объект. Такой объект называется singleton. За то, как выполняется операция создания экземпляра COM-объекта, отвечает так называемая фабрика классов. Ее реализация находится в том же модуле, что и реализация объектов создаваемых этой фабрикой.

Если для создания COM-объектов вы используете ATL, то для реализации singleton-объекта надо добавить одну строчку в код вашего объекта:

DECLARE_CLASSFACTORY_SINGLETON(CMyClass)

где CMyClass это имя вашего класса.

Singleton-объекты удобны, когда необходимо организовать управление общими ресурсами. Такие объекты похожи на Corba-объекты, использующие сервант по умолчанию.

В COM не реализован сервис имен (Naming Services), аналогичный CORBA. Этот Corba-сервис позволяет получать ссылку на конкретный экземпляр объекта, созданный на сервере. Идентификация объекта при этом происходит по имени.

Обычно необходимость такой функциональности встречается редко, так как COM придерживается отличных от Corba принципов взаимодействия с объектами. Но если такая функциональность вам необходима, то можно создать свой сервис имен. Для этого удобно воспользоваться функциональностью Singleton-объектов. Singleton-объект создает другие объекты и помещает их в массив, статический в простом случае, и динамический, или мап (объект, позволяющий ассоциировать одни значения с другими – в нашем случае имена объектов с указателями на их интерфейс) в более сложном случае. Один из методов этого Singleton-объекта принимает в качестве входного параметра имя и возвращает ассоциированный с ним объект. Простой пример, того, как это можно сделать с помощью ATL, находится на нашем сайте (www.optim.su) в электронной версии этой статьи. Там же приведен и Corba-вариант, использующий сервис имен.

Экземпляры и EXE-серверы

Если объект реализован в DLL, то, в принципе, его экземпляры можно создавать внутри нескольких EXE-серверов, но регистрировать в COM+ или MTS их можно только один раз (на одной машине). Если есть необходимость создавать такие COM-объекты в разных EXE-модулях, то их или вовсе не надо регистрировать в COM+/MTS, или нужно регистрировать как библиотечное приложение (библиотечный пакет в MTS). При этом создание экземпляров и передачу указателей на их интерфейсы необходимо делегировать другим объектам, зарегистрированным в COM+/MTS.

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

Использование нестандартных типов данных в методах удаленного объекта

Два пути

В COM есть два варианта организации обработки нестандартных (user-defined или сконструированных) типов данных. Первый – это уровень компилятора MIDL и C/++. Второй – использование стандартных для COM и совместимых с Automation универсальных типов данных и соответствующей техники.

Оба варианта позволяют использовать базовые типы (long, byte, ...), перечисления (enum) и структуры. Разница заключается в требованиях и ограничениях, предъявляемых обоими вариантами.

Первый вариант является более низкоуровневым и обычно приводит к тому, что методы, описанные с использованием хитрых MIDL-атрибутов, начинают неправильно восприниматься (или вообще становятся недоступными) в высокоуровневых языках типа VB. Еще одной особенностью первого варианта является то, что при его использовании не задействуются библиотеки типов. Маршалинг (запаковка параметров, передача их по сети и распаковка на другом компьютере) при этом выполняется создаваемой MIDL-компилятором заглушкой – специальной DLL-библиотекой, которая определенным образом регистрируется на взаимодействующих компьютерах.

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

Главное преимущество первого варианта заключается в том, что он позволяет «протащить через проволоку» практически любые типы данных, но если есть возможность, лучше пользоваться вторым подходом.

Если создавать приложения на VB, просто нет никакого выбора, кроме использования второго подхода. Собственно, VB все делает сам, и полностью обходится без MIDL-компилятора, да впрочем, и без самого IDL. Зато VB генерирует библиотеку типов, по которой впоследствии может быть создано описание для любого языка.

Второй вариант можно использовать и при создании приложений на С++. Надо только выполнять некоторые требования и не использовать низкоуровневые MIDL-конструкции.

Основные возможности низкоуровневого MIDL/С++ варианта

Массивы

Для передачи по сети массивов MIDL использует атрибуты size_is() и length_is(). При этом метод, передающий массив, будет выглядеть так:

HRESULT Method1(
    [in] short iLength,
    [in, length_is(iLength)] short asNumbers[10]);
HRESULT Method2(
    [in] short iLength,
    [in] short iSize,
    [in, length_is(iLength), size_is(iSize)] short * asNumbers);

Атрибут size_is определяет, сколько памяти будет занято RPC-заглушкой, а length_is определяет, сколько данных надо реально передавать. Так, в первом методе на вызываемой (так как все параметры типа "in") стороне всегда будет заниматься память для десяти элементов (10 * sizeof(short) = 20 байт) массива, но передаваться по сети будет iLength элементов. Во втором случае динамически будут определяться оба параметра. Если объем занимаемой памяти равен передаваемому, то можно пользоваться только атрибутом size_is. В работе с чисто входными (in) и чисто выходными параметрами (out) есть несколько нюансов, но рассказ об этом может занять слишком много места. За подробностями можно обратиться к MSDN. В одном из номеров нашего журнала (3 квартал 1998 года, статья «Visual C++ 6.0 в составе Microsoft Visual Studio» ) была статья, посвященная этой теме.

Работа со строками

MIDL позволяет использовать заканчивающиеся нулем строки. Правила работы со строками те же, что и в C/C++. Поддерживаются как обычные, так и двухбайтные строки. Чтобы сообщить MIDL, что некоторый параметр рассматривается, как строка, нужно пометить его специальным атрибутом – string. VB не поддерживает этот атрибут. Для работы со строками в нем применяется специальный тип – BSTR. Используя атрибуты size_is() и length_is(), можно передавать строки, содержащие в своем теле несколько NULL-символов.

Union

Еще одной интересной особенностью MIDL является поддержка им объединений (union), аналогичных объединениям в С++. Расширенный атрибут switch_is позволяет задать переменную, используемую для определения элемента объединения, имеющего смысл в данный момент времени. Обычно объединение (вернее, его описание и переменную, созданную на базе этого объединения) помещают в структуру. Одно из полей этой структуры и используется в атрибуте switch_is.

Ручная запаковка и распаковка

Самым радикальным случаем при работе с MIDL является так называемый wire marshaling. Этот тип маршалинга преобразует сложный тип в простой с помощью пользовательских функций. Суть его вкратце можно описать так. У вас есть тип данных, который по каким-то причинам нельзя переслать по сети в качестве параметра функции или в составе другого типа данных. Примером может служить указатель на объект C++ или тип данных, содержащий ссылки на локальные ресурсы. В этом случае вы объявляете этот тип в IDL с помощью атрибута wire_marshal или user_marshal. При этом вы задаете исходный тип и так называемый wire-тип. Wire-тип – это некоторый базовый тип, который априори может передаваться по сети, например, массив байтов. Далее надо реализовать четыре функции:

На основе IDL-описания и кода этих функций MIDL-компилятор создает специальную заглушку, используемую RPC-подсистемой ОС для передачи по сети того, что иначе не передается.

Листинг 1. IDL-файл примера «работа со структурами и массивами»

// RemTest1.idl : IDL source for RemTest1.dll
//
// Этот файл будет обработан MIDL для
// создания библиотеки типов (RemTest1.tlb) и кода маршаллинга.

import "oaidl.idl";
import "ocidl.idl";

[
   uuid(421D531D-A54F-4137-93E8-7006448611B7),
   version(1.0),
   helpstring("RemTest1 1.0 Type Library")
]
library REMTEST1Lib
{
   importlib("stdole32.tlb");
   importlib("stdole2.tlb");

   typedef [uuid(7EF6E5F1-ACF8-49a5-B49B-A92349F27157), version(1.0)] 
   enum SomeEnum
   {
      Some,
      Some1,
      Some2,
      Some3
   }SomeEnum;

   typedef [uuid(080C357D-EE8D-40b0-AD0A-5667995BB186), version(1.0)] 
   struct SomeStruct
   {
      int i;
      BSTR str;
      SomeEnum se;
   }SomeStruct;

   [
      object,
      uuid(153450BE-9F71-4193-9A00-95C58175FD8F),
      version(1.0),
      dual,
      nonextensible,
      pointer_default(unique),
      oleautomation
   ]
   interface IO : IDispatch
   {
      [id(1)] HRESULT M1([in,out] SomeStruct * ss);
      [id(2)] HRESULT M2([in,out] SAFEARRAY(SomeStruct) * ssbs);
      [id(3)] HRESULT M3([in] long len, [in, size_is(len)] SomeStruct * ssa);
   };

   [uuid(51ABF99E-4583-4DF1-85F5-51DE0EB57CDC)]
   coclass O
   {
      [default] interface IO;
   };
};

Листинг 2. Заголовочный файл примера «работа со структурами и массивами»

// O.h : Declaration of the CO
#ifndef __O_H_
#define __O_H_
#include "resource.h" // main symbols

/////////////////////////////////////////////////////////////////////////////
// CO
class ATL_NO_VTABLE CO : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CO, &CLSID_O>,
    public ISupportErrorInfo,
    public IDispatchImpl<IO, &IID_IO, &LIBID_REMTEST1Lib>
{
public:
    CO(){}

DECLARE_REGISTRY_RESOURCEID(IDR_O)
DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CO)
    COM_INTERFACE_ENTRY(IO)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()

// ISupportsErrorInfo
    STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);

// IO
public:
    STDMETHOD(M3)(/*[in]*/ long len, /*[in, size_is(len)]*/ SomeStruct * ssa);
    STDMETHOD(M2)(/*[in,out]*/ SAFEARRAY ** ssbs);
    STDMETHOD(M1)(/*[in,out]*/ SomeStruct * ss);
};
#endif //__O_H_

Высокоуровневый вариант (Automation/VB)

Концепция этого варианта очень проста: вы не можете использовать низкоуровневые средства MIDL. Вместо этого вы используете стандартные (их еще называют Automation-совместимыми) типы данных, структуры и перечисления.

VB делает это автоматически.

Ниже приведено краткое описание Automation-совместимых типов:

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

Не обязательно, но очень рекомендуется добавлять к определенным пользователям типам (структурам и перечислениям) уникальный идентификатор (в атрибуте uuid). Эти идентификаторы сделают описания типов уникальными (по крайней мере, в пределах нашей планеты ;-)) и позволят получать их описание с помощью таких API-функций, как GetRecordInfoFromGuids.

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

Структуры в массивах и с переменной типа VARIANT

С хранением структур в VARIANT или SAFEARRAY есть некоторая тонкость. Хотя в библиотеке типов можно указать, что в поле структуры или параметре функции может содержаться массив (SAFEARRAY) конкретных структур, во время работы приложения все равно приходится иметь дело с VARIANT’ом и SAFEARRAY’ем. Даже то, что у них можно узнать, что в них лежит структура (VT_RECORD), не дает никакого представления о том, что это за структура. А раз неизвестно, какая структура лежит, то и взаимодействовать с ней затруднительно. Как уже говорилось ранее, COM придерживается стратегии идентификации объектов, с которыми он взаимодействует, при помощи GUID. И если с некоторым объектом, в данном случае со структурой, не ассоциирован GUID, то COM не может поместить ее в VARIANT или SAFEARRAY. Поэтому при описании структуры, которую необходимо поместить в SAFEARRAY, надо задавать GUID. Зато после выполнения этого нехитрого требования вам становится доступна вся информация о структуре, хранящейся в ячейке массива SAFEARRAY или переменной типа VARIANT. Вы можете проверить, какого типа структура лежит в массиве, динамически получить информацию о полях структуры, считать или установить значение отдельного поля (по имени) и многое другое. Вы также можете просто узнать количество записей в массиве, получить указатель на данные, привести его к указателю на нужную структуру и заняться обработкой данных.

Листинг 3. cpp-файл примера «работа со структурами и массивами»

// O.cpp : Implementation of CO
#include "stdafx.h"
#include "RemTest1.h"
#include "O.h"

/////////////////////////////////////////////////////////////////////////////
// CO
STDMETHODIMP CO::InterfaceSupportsErrorInfo(REFIID riid)
{
    static const IID* arr[] = 
    {
        &IID_IO
    };
    for (int i=0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        if (InlineIsEqualGUID(*arr[i],riid))
            return S_OK;
    }
    return S_FALSE;
}
STDMETHODIMP CO::M1(SomeStruct *ss)
{
    if(!ss)
        return E_INVALIDARG;
    ss->i = 111;
    ss->se = Some3;
    return S_OK;
}
STDMETHODIMP CO::M2(SAFEARRAY ** ssbs)
{
    HRESULT hr = S_OK;
    // Проверяем, что в массиве лежат структуры...
    if(FADF_RECORD != (*ssbs)->fFeatures)
        return E_INVALIDARG;
    // С помощью IRecordInfo можно получать различную информацию о структуре
    CComPtr<IRecordInfo> spIRecordInfo;
    hr = SafeArrayGetRecordInfo(*ssbs, &spIRecordInfo);
    if(FAILED(hr))
        return hr;

    // Проверяем, та ли структура лежит в массиве...
    GUID RecordGuid;
    spIRecordInfo->GetGuid(&RecordGuid);
    GUID SomeStructGuid;
    // Переводим взятый из IDL-файла строковый GUID в бинарный GUID
    CLSIDFromString(OLESTR("{080C357D-EE8D-40b0-AD0A-5667995BB186}"), 
                  &SomeStructGuid);
    // Если GUID-ы не совпали, значит, нам подсунули массив не тех структур
    if(RecordGuid != SomeStructGuid)
        return E_INVALIDARG;

    // Если вы уверены, что получаете правильный массив, то предыдущий код 
    // не нужен. Дальше начинается код, меняющий первый элемент в массиве
    SomeStruct * pss;
    // Блокируем массив
    if(SUCCEEDED(SafeArrayLock(*ssbs)))
    {// Теперь можно производить доступ к его элементам

        // Получаем доступ к элементу 1
        LONG iIndex = 1;
        hr = SafeArrayPtrOfIndex(*ssbs, &iIndex, (void**)&pss);
        if(FAILED(hr))
            return hr;

        // Изменяем значения полей...
        pss->i = 987654321;
        SysFreeString(pss->str);
        pss->str = SysAllocString(OLESTR("Изменено на C++"));
        pss->se = Some3;

        // Отпускаем массив
        return SafeArrayUnlock(*ssbs);
    }
    return S_OK;
}
STDMETHODIMP CO::M3(long len, SomeStruct * ssa)
{
    if(len < 2)
        return S_FALSE;
    ssa[1].i = 555;
    ssa[1].se = Some2;
    return S_OK;
}

В Листингах 1, 2 и 3 приведена реализация COM-объекта, демонстрирующего работу с отдельной структурой (метод M1), с массивом структур через SAFEARRAY (метод M2), и работа с массивом, описанным средствами MIDL (метод M3). Под Windows 2000 этот пример не требует создания и регистрации заглушки, но так как метод M3 содержит прямое обращение к не-Automation типу, на других платформах этот пример может потребовать компиляции и регистрации заглушки. Чтобы избежать этого, можно изменить описания метода M3 так, чтобы использовались только типы VARIANT и SAFEARRAY.

Код метода M2 перегружен. Но, во-первых, основную его часть занимают проверки, которые на практике не нужны. Во-вторых, можно сделать шаблонный класс, который принимал бы в качестве параметра тип структуры и эмулировал простой массив. Сделать это можно, скрыв блокировку и ее снятие в конструкторе и деструкторе, а эмулировать массив – перегрузив оператор [ ].

Клиентское приложение, взаимодействующее с объектом из примера, можно создать на C++ (например, выбрав проект MFC EXE) или на VB. В Листинге 4 приведен пример на VB.

Листинг 4. VB-клиент примера «работа со структурами и массивами»

'Не забудьте подключить ссылку на эту библиотеку
Dim rem1 As New REMTEST1Lib.O

Private Sub Command1_Click()
    Dim SS As SomeStruct
    SS.i = 12345
    SS.se = Some1
    SS.Str = "Изменено на VB"
    MsgBox "i = " & SS.i & " SS.se = " & SS.se & " str = " & SS.Str
    rem1.M1 SS
    MsgBox "i = " & SS.i & " SS.se = " & SS.se & " str = " & SS.Str
End Sub

Private Sub Command2_Click()
    Dim ssa(10) As SomeStruct
    Dim ssa1() As SomeStruct
    For i = 0 To 10
        ssa(i).i = i
        ssa(i).se = Some1
        ssa(i).Str = "Str " & 1
    Next
    ssa1 = ssa
    MsgBox " ssa1(1).se = " & ssa1(1).se & " ssa1(1).Str = " & ssa1(1).Str
    rem1.M2 ssa1
    MsgBox " ssa1(1).se = " & ssa1(1).se & " ssa1(1).Str = " & ssa1(1).Str
End Sub

Статическое и динамическое связывание

Интерфейсы COM по умолчанию поддерживают статическое связывание. Для динамического связывания используется специальный интерфейс IDispatch. В принципе объект может поддерживать только IDispatch-интерфейс. Такой интерфейс называется disp-интерфейсом. Этот подход применялся во времена VB третьей версии. Сейчас чаще всего применяются так называемые dual-интерфейсы, но об этом позже.

Disp-интерфейсы и IDispatch

Как уже говорилось, IDispatch применяется в тех случаях, где нужна интерпретация, то есть при применении OLE Automation или скриптовых языков, таких, как VBScript и JScript. Хотя один объект и может поддерживать более одного disp-интерфейса, скриптовые языки могут воспользоваться только одним из них.

Вызовы методов disp-интерфейса производятся по номеру называемому DISPID. Отображением имен методов в DISPID занимается GetIDsOfNames.

Вот полный перечень методов, входящих в IDispatch:

Производительность Invoke ниже, чем прямого вызова интерфейса с теми же параметрами. Главным образом разница в скорости объясняется необходимостью запаковки и последующей распаковки параметров в VARIANT. При сетевом вызове разница в скорости вызова метода disp-интерфейса и метода простого интерфейса становится менее значимой. Однако большинство высокоуровневых средств (типа VB и Delphi) при вызове удаленного метода кроме Invoke вызывают GetIdsOfNames, причем норовят сделать это при каждом вызове, без какой бы ни было оптимизации. Так как GetIdsOfNames выливается в отдельный сетевой вызов, на методах с небольшим числом маленьких по размеру параметров производительность вызовов методов через IDispatch может быть медленнее статического вызова в 4 раза.

Для вызова методов disp-интерфейсов в VB, Delphi и Java используется стандартный синтаксис этих языков. На C++ для этого применяются dispatch-драйверы, специальные классы-обертки.

На C++ более удобно и производительно использовать VTBL-часть dual-интерфейсов.

dual-интерфейсы

Это интерфейсы, поддерживающие как IDispatch, так и прямой вызов через VTBL.

dual-интерфейсы совмещают скорость обыкновенных интерфейсов с гибкостью disp-интерфейсов. Именно поэтому Microsoft рекомендует использовать их везде, где это возможно.

Практически все среды разработки позволяют легко создавать dual-интерфейсы. VB позволяет создавать только dual-интерфейсы, хотя использовать может и обыкновенные, и disp-интерфейсы. Если нормальный интерфейс описан вне VB (например, в библиотеке типов, созданной с помощью MIDL), то на VB можно создать реализацию этого интерфейса, воспользовавшись ключевым словом Implements.

Вызов Idispatch-части при вызове методов dual-интерфейсов еще больше снижает производительность, так как на стороне сервера этот вызов превращается в нормальный VTBL-вызов, что приводит к ненужным расходам на работу с библиотекой типов. Dual-интерфейсы защищены гораздо лучше, чем чистые disp-интерфейсы, поскольку при конвертации, о которой говорилось выше, производится контроль и приведение типов параметров вызванного метода.

Обработка ошибок

HRESULT

По спецификации COM каждый метод COM-объекта должен возвращать специальное 32-х битное число, называемое HRESULT. В нем вызывающей стороне возвращается код ошибки (отрицательное значение), информация об успехе (константа S_OK, равная нулю) или предупреждение (значение, большее нуля).

Обычно COM-объект имеет определенный набор кодов возврата, но так как объекты могут транслировать сообщения об ошибке, полученные от вызываемых ими других методов, то не всегда можно гарантировать, что список значений неизменен. Чтобы определить, был ли вызов успешен или произошла ошибка, можно воспользоваться макросами: SUCCEEDED() – успех и FAILED() – неудача.

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

Возврат информации об ошибках

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

Чтобы записать в этот объект собственную информацию, необходимо выполнить следующие шаги:

  1. Для объекта, в котором инициируется ошибка, реализовать интерфейс ISupportErrorInfo.
  2. Для создания объекта, описывающего ошибку, вызвать функцию CreateErrorInfo.
  3. Для задания атрибутов этого объекта воспользоваться методами ICreateErrorInfo.
  4. Для ассоциации объекта, сообщающего об ошибке, с текущим логическим потоком, вызвать функцию SetErrorInfo.

Получение информации об ошибках

Чтобы получить информацию об ошибках, необходимо выполнить следующие шаги:

  1. Следует проверить, может ли объект обработать ошибку, соответствующую возвращенному значению.
  2. Для этого через IUnknown::QueryInterface запрашивается указатель на интерфейс ISupportErrorInfo, а затем – InterfaceSupportsErrorInfo, для проверки того, что ошибка случилась именно с тем объектом, который возвратил ее, и что объект, сообщающий об ошибке, относится именно к этой ошибке, а не к предыдущему вызову.
  3. Затем нужно вызвать функцию GetErrorInfo, чтобы получить указатель на объект, описывающий ошибку. В принципе, если вас не интересует, кто записал информацию об ошибке, шаг 2 можно пропустить.
  4. Чтобы получить информацию от объекта, вызываются методы IErrorInfo.

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

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

  1. В объекте доступа к данным (например, OLE DB) происходит ошибка «нарушение ссылочной целостности».
  2. Объект доступа к данным закладывает соответствующее сообщение и возвращает код ошибки. Если это сообщение в неизменном виде дойдет до клиента, тот ни за что не поймет, что же случилось...
  3. Промежуточный компонент, управляющий бизнес-логикой, вынимает это сообщение, закладывает сообщение о том, что «Операция добавления данных о клиенте не удалась, так как:» и дописывает сообщение, присланное объектом доступа к данным. Далее компонент, управляющий бизнес-логикой, закладывает свой код ошибки и возвращает управление вызвавшему его методу.
  4. Пункт три может повторяться до тех пор, пока управление не получит клиентское приложение, инициировавшее всю цепочку вызовов. Клиентское приложение вынимает сообщение и показывает его пользователю.

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

Прямую работу с IErrorInfo можно встретить только в коде, написанном на C/C++. Да и то обычно его пытаются «завернуть» в обработку исключений C++. VB, например, берет на себя обработку COM-ошибок, скрывая от программиста работу с HRESULT и IErrorInfo в специальном объекте «Err».

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

Метаданные объектов

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

Библиотеки типов используются для получения информации об объектах, интерфейсах и пользовательских типах данных. Они также позволяют избавиться от proxy/stub при маршалинге. Создавать библиотеки типов можно с помощью MIDL или программно. VB тоже создает библиотеки типов, но при этом они неразрывно связаны с создаваемыми на нем компонентами. Естественно, VB позволяет использовать библиотеки типов, созданные другими средствами.

Для создания библиотек типов «вручную» используется специальный объект, реализующий интерфейс ICreateTypeLib[2]. Для создания такого объекта используется API-функция CreateTypeLib[2].

Прочитать содержимое библиотеки типов можно с помощью интерфейса ITypeLib. Получить указатель на этот интерфейс можно, вызвав API-функции LoadTypeLib или LoadRegTypeLib.

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

Disp-интерфейсы (как и dual-интерфейсы) позволяют получить их описание через свой метод IDispatch::GetTypeInfo. На нашем сайте в электронной версии этой статьи можно найти пример, показывающий, как получить информацию о типе содержимого VARIANT’а. Хотя в этом примере речь описывается техника работы с типом VARIANT, часть этого примера можно использовать как общее описание работы с библиотеками типов и интерфейсом ITypeInfo. Там же можно найти и CORBA-аналог этого проекта.

Управление потоками

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

COM помогает предотвратить взаимоблокировки вызовов объектов. Существуют и некоторые функции, созданные для предотвращения состояния гонок во внепроцессных серверах.

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

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

Самый простой путь понять архитектуру потоков, принятую в COM – это представить, что все COM-объекты в процессе разделены на группы, называемые апартаментами (apartments). COM-объекты всегда живут в одном апартаменте, в том смысле, что их методы могут быть легально вызваны только потоком, принадлежащим этому апартаменту. Любой другой поток, желающий вызвать объект, должен делать это через proxy.

Существует два типа апартаментов: однопоточные апартаменты (single-threaded apartments, STA) и многопоточные апартаменты (multi-threaded apartments, MTA).

Однопоточный апартамент (STA) – это апартамент, содержащий только один поток. COM синхронизирует все обращения к методам COM-объекта, находящегося в STA. Делается это через очередь сообщений Windows. Каждый поток клиентского процесса или внепроцессного (EXE) сервера, желающий работать в отдельном апартаменте, должен вызвать CoInitialize или CoInitializeEx с параметром dwCoInit, равным COINIT_APARTMENTTHREADED (эти вызовы и приводят к инициализации потока и выделению ему синхронизирующей очереди сообщений). Внутрипроцессные объекты, располагающиеся в DLL (в том числе COM+/MTS-объекты), регистрируют свою поточную модель в реестре с помощью именованного значения ThreadingModel внутри ключа InprocServer32. При передаче указателей на интерфейсы между разными STA, или между STA и MTA, должен обязательно производиться маршалинг. При передаче указателя на интерфейс как параметра метода какого-нибудь интерфейса, или при создании нового COM-объекта, маршалинг делается автоматически. Если необходимо вручную (без использования COM, например, через общую область памяти) передать указатель на интерфейс другому потоку, необходимо произвести «ручной» маршалинг.

Для «ручного» маршалинга используется функция CoMarshalInterThreadInterfaceInStream (пожалуй, эта функция – рекордсмен среди Win32 API-функций по длине имени). В качестве первого параметра ей передается IID интерфейса. В качестве второго – указатель на этот интерфейс. В качестве третьего – указатель на интерфейс IStream, в который и будет записана информация о передаваемом интерфейсе. Перед вызовом этой функции необходимо создать объект, реализующий интерфейс IStream. Самый простой способ сделать это – воспользоваться API-функцией CreateStreamOnHGlobal, создающей Stream, базирующийся на оперативной памяти. Последнее, что необходимо сделать – это из другого потока выполнить обратный маршалинг. Для этого необходимо вызвать CoGetInterfaceAndReleaseStream. Ручной маршалинг очень редко применяется на практике. Если же возникнет необходимость делать его часто, то можно создать простой класс-обертку, или воспользоваться глобальной (для процесса) таблицей интерфейсов. Она похожа на AOM (Active Object Map) в Corba. Управление глобальной таблицей интерфейсов производится через стандартный интерфейс IGlobalInterfaceTable. Указатель на него можно получить, создав COM-объект с CLSID CLSID_StdGlobalInterfaceTable. Еще один способ избавиться от «ручного маршалинга» – агрегировать специальный объект FreeThreadedMarshaler. Для создания такого объекта используется функция CoCreateFreeThreadedMarshaler. ATL Wizard позволяет автоматизировать всю работу по созданию и агрегированию ThreadedMarshaler.

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

Процесс может содержать любое количество STA.

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

Процесс может содержать только один MTA.

Все процессы имеют апартаментную модель, просто некоторые апартаменты содержат один поток, а некоторые – несколько. Поточная модель применима к апартаменту, а не к процессу. Она также может быть применима к CoClass’у но не к DLL-модулю. Разные классы в одной DLL могут иметь разные модели потоков. Но, все же, ATL позволяет определять так называемую глобальную потоковую модель, влияющую на потоковые характеристики всех входящих в модуль объектов.

Процесс, содержащий два или более STA, и не содержащий MTA, называется процессом с апартаментной моделью (apartment model process).

Процесс с одним потоком является частным случаем STA-модели. Такой процесс называется однопоточным (single-threaded process).

Процесс, содержащий MTA и один или более STA, называется процессом со смешанной моделью (mixed model process).

Процесс, содержащий MTA и не содержащий STA, называется свободно-поточным процессом (free-threaded process).

Главным апартаментом процесса считается тот, который был первым проинициализирован. В однопоточном процессе он остается единственным.

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

В СОМ не существует аналогичной CORBA сессионной идеологии работы с потоками, когда все вызовы от одного клиента выполняются в одном потоке, а другого – в другом. Но если каждый клиент будет создавать отдельную копию STA-компонента, то внешне это будет похоже на сессионный метод CORBA. Единственное отличие заключается в том, что если двум таким компонентам взбредет в голову обратиться друг к другу, или к общему STA-компоненту, то в COM эти методы будут автоматически синхронизированы.

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

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

На сегодня большинство языков программирования позволяют создавать COM-объекты в STA-архитектуре, но только C++ позволяет создавать многопоточные компоненты. Библиотека ATL позволяют существенно облегчить эту непростую задачу. Скоро должна появиться седьмая версия VB. Разработчики обещают ввести в эту версию поддержку режима свободных потоков.

Масштабируемость

Организация пула объектов.

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

Just-in-Time (JIT) -активация

Just-in-Time (JIT)-активация – предоставляемый COM+ сервис, помогающий более эффективно использовать ресурсы сервера, особенно при использовании большого количества транзакций. Если компонент сконфигурирован, как JIT-активируемый, COM+ будет порой деактивировать его экземпляр, несмотря на то, что клиент по-прежнему имеет активную ссылку на объект. При следующем вызове метода объекта клиентом, считающим объект активным, COM+ снова активирует объект прозрачно для клиента.

Преимущество такого подхода состоит в том, что клиент держит ссылку на объект столько времени, сколько ему нужно, не тратя ресурсов сервера, например, памяти.

Shared Property Manager

С помощью Shared Property Manager (SPM) можно в пределах одного COM+-приложения организовать набор свойств, хранящих общие данные для нескольких компонентов. Это позволяет получить простой способ передачи данных между компонентами, реализованными в разных DLL-библиотеках. При доступе к этим свойствам SPM автоматически осуществляет синхронизацию.

Обеспечение безопасности

Безопасность в COM основывается на службах безопасности Windows NT и Windows 2000, и механизме безопасности RPC. Базовыми понятиями являются аутентификация (процесс определения прав пользователя) и авторизация (процесс определения, имеет ли пользовать право делать то, что ему требуется). Два основных типа безопасности в COM – безопасность активации (activation security) и безопасность вызова (call security). Безопасность активации определяет, имеет ли клиент право запустить сервер. После запуска сервера вы можете использовать безопасность вызова для контроля доступа к серверным объектам.

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

В Windows 2000 реализованы протокол аутентификации Kerberos v5 и пакет средств безопасности SChannel. Появились такие возможности, как имперсонация на уровне делегирования (delegate-level impersonation), взаимная аутентификация (для протокола Kerberos), возможность устанавливать уровни аутентификации для AppID реестра, и маскировка (cloacking). Используя средства безопасности COM, можно реализовать объекты, способные выполнять привилегированные операции без ущерба для безопасности.

Два главных интерфейса, IClientSecurity и IServerSecurity, позволяют программно управлять безопасностью на уровне вызова.

Безопасность в COM+

COM+ предоставляет средства безопасности, которые можно использовать для защиты приложений COM+. COM+ позволяет управлять защитой как с помощью визуального задания через Component Services атрибутов защиты, так и программно, вызывая в коде специальные API-функции. Вот краткое перечисление расширений стандартных механизмов защиты NT, появившихся в COM+:

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

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

Ролевая безопасность – главное средство безопасности приложений COM+. Используя роли, можно автоматически создать политику авторизации, указывая (если нужно, вплоть до уровня метода), кому какие ресурсы доступны. Кроме этого, ролевую безопасность можно применять программно, если приложение требует более тщательного контроля доступа.

Аутентификация – прежде, чем авторизовать клиентов для доступа к ресурсам, следует убедиться, что они – именно те, за кого себя выдают. Это и делают службы аутентификации COM+. Вы просто административно включаете сервис аутентификации, работающий прозрачно для приложения.

Заимствование прав/делегирование (Impersonation/Delegation) – иногда приложению нужно выполнить некие действия от имени клиента, используя права клиента, например, при доступе к БД, аутентифицирующей клиента. Это называется заимствованием прав, или имперсонацией. Возможны разные уровни имперсонации. Чтобы задействовать ее, следует, во-первых, административно ее настроить, и, во-вторых, прописать её поддержку в коде компонентов приложения.

Сегодня реально существуют три уровня имперсонации: идентификация (identify), имперсонация (impersonate), и делегация (delegate). До появления W2K их было два – идентификация и имперсонация. В W2K появился третий, делегация.

Для работы на уровне делегации нужно выполнять следующие требования:

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

Контрольный журнал (Audit Trails) – ролевая безопасность позволяет отслеживать обращения к вашему компоненту, вплоть до ведения детальных логов.

Безопасность активации

Безопасность активации (называемая также безопасностью запуска) контролирует, кто имеет право запускать сервер. Эту службу автоматически исполняет Service Control Manager (SCM) конкретной машины. При получении запроса на активацию объекта от клиента, SCM проверяет запрос на основе информации, хранящейся в его реестре (в том числе и для вызовов, поступающих с этой же машины).

W2K, определяя права клиента при активации, проверяет, установлен ли клиентом флаг маскировки. Клиент может сделать это, вызвав CoInitializeSecurity. Если да, то для идентификации клиента используется маркер потока (если он имеется). Если маскировка выключена, вместо маркера потока используется маркер процесса.

Windows 9х и MacOS не поддерживают безопасности активации.

Маскировка (Cloaking)

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

Безопасность в Windows 95 и Windows 98

Если сеть построена из машин под Windows 9х, важно, имеется ли в сети домен Windows NT/W2K. Если домен присутствует, то Windows 9х может обеспечить аутентификацию и авторизацию через сквозной механизм безопасности. Если же нет, то проверка безопасности при вызовах невозможна.

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

Устойчивость к сбоям

Context

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

Context – новинка MTS, COM+ и Windows 2000. Каждый COM-объект создается с ассоциированным контекстом. В случае объектов, не использующих служб COM+, контекст в значительной степени игнорируется. Но для приложений, сконфигурированных в COM+, контексты – это основа, на которой обеспечивается сервис COM+.

Контекст – это, в сущности, набор runtime-свойств одного или нескольких объектов. Каждый COM-объект во время исполнения (то есть, между его активацией и деактивацией) ассоциирован с единственным контекстом. Каждый контекст существует в единственном апартаменте COM, но в одном апартаменте могут сосуществовать несколько контекстов. В одном контексте могут исполняться несколько объектов. (Следует пояснить, что объекты, агрегирующие Free Threaded Marshaller, являются исключением – они исполняются в любом контексте).

COM+ использует контекстные свойства как основу предоставляемых run-time служб. Эти свойства содержат состояние, которое определяет, как среда исполнения будет осуществлять сервис для объектов в контексте. Изменяя эти свойства, можно влиять на состояние и поведение run-time служб СОМ+.

Каждому контексту соответствует ассоциированный объект, ObjectContext, следящий за его свойствами. Вы можете получить доступ к ObjectContext, вызвав CoGetObjectContext. Получив ObjectContext, вы можете воспользоваться его интерфейсом IObjectContext для работы с контекстными свойствами.

Кроме IObjectContext есть еще несколько специализированных интерфейсов для доступа к контекстным свойствам: IObjectContextInfo, IContextState и IObjectContextActivity.

Транзакции

MTS и COM+ поддерживают так называемый «механизм автоматических транзакций». Транзакции выполняются на уровне компонентов, т.е. каждый компонент, участвующий в транзакции необходимо специальным образом пометить. Это можно сделать как визуально, так и программно. Run-time система COM+ считывает эту информацию и исходя из нее, создает для компонента новую транзакцию или помещает компонент в уже существующую. Компонент может управлять транзакцией с помощью контекста компонента (см. «Context»).

Compensating Resource Manager

Compensating Resource Managers (CRM) – эта особенность Component Services позволяет просто интегрировать ресурсы приложений с транзакциями Microsoft Distributed Transaction Coordinator (MS DTC). Это альтернатива разработке полного менеджера ресурсов Microsoft Transaction Services. С помощью пары COM-компонентов и реализации специфичных интерфейсов ресурсы вашего приложения могут влиять на результат транзакции и получать оповещение о ее результате.

CRM – это пара компонентов COM – CRM Worker и CRM Compensator. CRM Worker отвечает за основную часть работы CRM и реализует COM-интерфейс, характерный для выполняемой им задачи. Инфраструктура CRM предоставляет интерфейс к CRM Worker, через который CRM Worker может помещать записи в лог-файл. Эти записи применяются для правильного восстановления данных в случае сбоя.

CRM Compensator – компонент, создаваемый инфраструктурой CRM после завершения транзакции. Он реализует определенный интерфейс, через который инфраструктура CRM может передать уведомления о завершении транзакции и записи в лог-файле, сделанные CRM Worker.

Использование кластеров серверов

W2K Advanced Server и Data Center позволяют организовать кластеры из нескольких серверов. Это обеспечивает дополнительную устойчивость серверных COM+-приложений за счет автоматического переключения на другой сервер в случае сбоя одного из них. Использование продукта Microsoft AppCenter Server позволяет распределять нагрузку между серверами с помощью CLB (COM+ Load Balancing), что дает возможность запуска компонентов на наименее загруженном сервере. Это повышает производительность и доступность приложения.

Асинхронное взаимодействие между объектами

Queued Components

QC-компоненты являются модификацией COM+/MTS-компонентов. QC-компоненты отвечают требованиям механизма Store-and-Forward. Клиентское приложение может использовать QC-компоненты, так же, как любой другой COM-объект. Они основаны на COM, но в качестве транспортного протокола вместо RPC используют MSMQ-сервис (сервис асинхронной работы с очередями сообщений от Microsoft). Классический DCOM использует синхронный RPC для передачи информации от одного компьютера другому посредством заместителей (proxy) и заглушек (stub), как показано на рис.7.


Рис. 7. COM использует RPC

QC взаимодействует между клиентским proxy и stub сервера с помощью MSMQ, как показано на рис.8.

Во время активизации QC-объекта, клиент подключается не к COM-объекту, а к Сall Recorder’у. Клиентское приложение производит вызовы как обычно, но они не отправляются немедленно через RPC. Вместо этого информация о них записывается в очередь MSMQ. Когда клиент деактивирует компонент (вызывает Release()), QC использует MSMQ для асинхронной посылки последовательности вызовов серверу, на котором создается реальный компонент. Сервер читает сообщения из MSMQ-очереди, содержащей эту последовательность, активизирует компонент Player (исполнитель) и передает ему информацию о вызовах. Player создает реальный объект и воспроизводит на нем вызовы.


Рис. 8. QC использует MSMQ

Достоинства QC:

С помощью QC-компонентов можно реализовать режим работы off-line и добиться полной независимости клиента от сервера. Их единственным недостатком является то, что они доступны только на W2K.

COM+ Events

Распределенные приложения, в которых сервер должен оповещать клиентов об изменениях важной для них информации, встречаются довольно часто. В таком приложении трудно понять, кто является сервером, а кто – клиентом. Клиенты в момент получения уведомлений сами становятся серверами. Чтобы избежать путаницы, следуя примеру Microsoft, назовем программу, рассылающую сообщения «Издателем» (Publisher), а приложения, заинтересованные в получении уведомлений – «Подписчиками» (Subscriber).

Система поддержки событий COM+ – это служба операционной системы, предоставляющая базовые сервисы поддержки событийного механизма. Событие в этой системе описывается как метод некоторого COM-интерфейса. Интерфейс необходимо поместить в «COM+»-объект, который в свою очередь специальным образом зарегистрировать в системе поддержки событий. Не правда ли, очень похоже на сказку про Василису прекрасную – «Смерть кощеева на конце иглы, игла в яйце, яйцо в утке, утка в зайце...». Рассылка событий – процесс, инициируемый издателем и осуществляющий уведомление подписчиков о случившемся событии. Издатель – любая программа, инициирующая событие посредством вызова метода интерфейса, принадлежащего событийному объекту. Подписчик – «COM+»-объект, реализующий тот же самый событийный интерфейс, что и событийный класс. Подписчик должен быть зарегистрирован как обычный «COM+»-компонент. Для того чтобы получать уведомления о событиях необходимо, чтобы для этого подписчика была оформлена (создана) подписка. Подписка – формальная регистрация производящаяся на компьютере где установлен класс событий. Сама подписка производится в ветке subscriptions на зарегистрированном здесь же подписчике. Класс событий (или Событийный класс) – «COM+»-объект, содержащий событийный интерфейс. Этот объект должен быть специальным образом зарегистрирован в системе поддержки событий. Ни одного экземпляра этого класса никогда не создается. При попытке создания экземпляра этого класса система поддержки событий создает специальный событийный объект, указатель на который и передается Издателю. Событийный объект – квази-компонент создаваемый на базе описания событийного интерфейса, взятого из событийного класса. Событийный интерфейс – интерфейс, который должны реализовать все подписчики, которые хотят получать уведомления о событиях. Вызов методов этого событийного интерфейса и инициирует рассылку сообщений. Заметьте, что подписка может быть осуществлена на конкретный метод этого интерфейса, но реализовать интерфейс придется полностью. Это не проблема, так как VB скрывает сложности реализации интерфейсов, а в VC есть визард, автоматизирующий эту операцию.

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

Издатель создает событийный объект вызовом функции CreateObject (CoCreateInstance – для C++).

Через вызов необходимого метода событийного интерфейса издатель инициирует рассылку уведомлений о событии (на языке американских программистов – производит Firing).

Система поддержки событий просматривает базу данных (COM+ каталог) в поисках зарегистрированных подписчиков (на соответствующий интерфейс или метод). Она связывается с подписчиками (создавая их напрямую, через моникер или Queued-компонент, как указано в базе данных подписки («COM+»-каталоге)) и уведомляет их о событиях, вызывая у них соответствующий метод событийного интерфейса.

Схема взаимодействия частей системы поддержки событий показаны на Рис. 9.


Рис.9. Схема взаимодействия частей системы поддержки событий

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

В системе поддержки событий подписку производит наделенное администраторскими правами лицо (через консоль Component Services) или программа. Подписка – это структура данных, снабжающая сервис событий информацией о подписчике. Она определяет, кто и от кого будет получать уведомления. Естественно, в терминах COM «кто и от кого» определяются через CLSID соответствующих объектов. Кроме CLSID подписчика можно вдобавок указывать и имя компьютера, на котором необходимо создавать объект-подписчик.

Подписка может быть постоянной (persistent) или временной (transient). В случае постоянной подписки COM+ сам создает объект подписчика (на машине подписчика) так же, как и в случае с QC. Transient-объект должен быть создан вручную и подключен через соответствующий API. Вышесказанное означает, что постоянный объект можно регистрировать визуально, но он не может загружаться в адресное пространство клиентского приложения. Постоянный объект может переживать перезагрузку компьютера. Transient-объект не переживает перезагрузки, но может быть создан внутри любого приложения. Как издатель, так и подписчик могут быть Queued-компонентами и, соответственно, работать асинхронно.

Подробнее о COM+ Events можно прочесть в номере нашего журнала за 4 квартал 1999 года.

Асинхронные вызовы

В W2K появилась возможность осуществлять асинхронные вызовы компонентов без использования Queued-подсистемы. При этом асинхронность достигается за счет вызовов в отдельных потоках. Подробнее об этой технологии вы можете прочесть в статье «Асинхронные вызовы методов COM-компонентов в Windows 2000» в этом номере журнала.


Впервые статья была опубликована в журнале <Технология Клиент-Сервер>.
Эту и множество других статей по программированию, разработке БД, многоуровневым технологиям (COM, CORBA, .Net, J2EE) и CASE-средствам вы можете найти на сайте www.optim.su и на страницах журнала.