![]() |
Новое
поколение выбирает...
|
|
Открытый проект по созданию кроссплатформенного
броузера.
|
|
|
|
|
|
|
|
|
|
|
|
Статьи. Технологии, спецификации - COM/DCOM/COM+/CORBA/ActiveX.
Фрагменты курса “ОС Windows” Часть “Технологии СОМ и ActiveX” Предисловие к публикации Конспект лекций по курсу “ОС Windows” изначально писался автором для “собственного употребления” и должен был выполнять свою основную задачу – облегчить автору чтение лекций. В связи с возникшей идеей опубликовать текст на Веб-сервере автор считает своим долгом сделать ряд существенных оговорок.
Условные обозначения в тексте: © - попытка определения ¨ - пример (!) – выводы (· ) - правило
История появления технологии ActiveX До последнего времени программные приложения имели монолитную структуру. НО, монолитные приложения тяжело: Разрабатывать
Особенно сложна модификация, так как свойства приложения обычно так переплетены, что не могут быть индивидуально и независимо изменяться. Кроме того, программное обеспечение не легко интегрировать, когда оно написано на разных языках, запускается в отдельных процессах или на разных машинах. В связи с ростом сложности программного обеспечения и уменьшением нагрузки на отдельное клиентское оборудование, становится необходимым создание распределенного компонентного окружения. Т.е. должна быть возможность использования неких компонент (сервисов) вне зависимости от того, кто их обеспечивает и где они находятся. Любое реальное решение этой проблемы должно: а) брать преимущества объектно-ориентированных концепций и б) иметь способность работать с наследуемым кодом; т.е. смотреть в будущее, не забывая истории. Как решение проблемы долгое время рассматривалось объектно-ориентированное программирование (ООП). Но, несмотря на свою мощь, ООП к настоящему времени исчерпало свой потенциал, так как не существует стандартного окружения (=framework), через которое программное обеспечение, созданное различными производителями, может взаимодействовать внутри одного и того же адресного пространства и за границами сетевой и машинной архитектуры. Главный итог ООП-революции – создание островков объектов, которые не могут общаться друг с другом через океанские границы. Решение проблемы – в создании программных компонент. © Программная компонента – это повторно используемые куски кода и данных в двоичной форме, которые могут быть с относительно малыми усилиями вставлены в другие программные компоненты от других производителей. Программные компоненты должны иметь возможность соединяться в соответствии с неким внешним двоичным стандартом, но их внутренняя реализация никак не ограничена! Они могут быть созданы с использованием процедурных языков, объектно-ориентированных языков или оболочек и т.п. Программные компоненты сегодня можно сравнить с интегральными компонентами в аппаратуре. 20 лет назад производители стали изобретать способы уменьшения транзисторов и объединения их в пакеты, реализующие одну логическую функцию (например, И-НЕ). Проектировщики покупали эти пакеты и строили свои решения. По мере усложнения аппаратных функций усложнялись и пакеты, превращаясь в чипы. Теперь уже из чипов строятся еще большие чипы. Индустрия ПО сейчас находится в такой точке, когда разработчики заняты построением программных эквивалентов транзисторов – программных процедур. Наиболее популярным в настоящее время решением, обеспечивающим производство программных компонент, является технология ActiveX. ActiveX предлагает расширяемые стандарты и механизмы, которые дают возможность разработчикам программного обеспечения паковать функциональность в программные компоненты. Вместо того, чтобы заботиться о создании процедур, разработчики могут просто купить эту процедуру, не заботясь о ее внутренней реализации. Автором технологии ActiveX является лидер мировой индустрии программирования – компания Microsoft. Родителем технологии ActiveX можно назвать технологию OLE версии 1, а старшим братом – OLE 2. OLE версии 1 (“Object Linking and Embedding” – связывание и внедрение объектов) представляло собой механизм работы с составными документами (=compound documents). © Составной документ – документ, чье содержимое собирается из множества источников (текст, графика, графики, таблицы, звук, видео и т.п.). Эта возможность в монолитном приложении ограничена лишь теми типами данных, которые известны во время создания приложения. Если появится новый тип, приложение должно быть разработано, построено и распространено заново. А для расширяемого компонентного приложения, использующего OLE, вновь инсталлированная компонента доступна сразу же для всех существующих компонент и приложений. ¨ С помощью OLE 1 в документ Microsoft Word можно было вставить электронную таблицу из Microsoft Excel. Идея появления OLE была в том, чтобы от программно-ориентированной модели работы с компьютером можно бы было перейти к документо-ориентированной модели. Тогда бы пользователь смог бы больше думать о документе, и меньше о приложении, которое этот документ создает. OLE 1, как и другие 1-ые версии, была достаточно слабой с точки зрения реализации. Кроме того, составные документы - это лишь часть общей проблемы создания компонентного программного обеспечения. Поэтому, хотя OLE версии 2 изначально планировалась как средство повышения производительности и усовершенствование OLE 1, она переросла границы составных документов и выросла в общую сервисную архитектуру, включающую группу технологий. Основой OLE 2 стала важнейшая из технологий – модель многокомпонентных объектов (=Component Object Model – COM). COM дала общую парадигму взаимодействия программ любых типов: приложений, системного программного обеспечения, библиотек и т.п. Так как “технология связывания и внедрения объектов” – это лишь один из примеров технологий, базирующихся на COM, то термин OLE перестали расшифровывать. Кроме того, одним из принципиальных свойств OLE 2 стало то, что это – расширяемая архитектура. Т.е. все новые появляющиеся конкретные технологии должны были укладываться в рамках этой архитектуры и могли бы быть добавлены в OLE внутри существующей концепции без ее изменения. Поэтому OLE постепенно потеряла номер версии, т.к. архитектура OLE 3 принципиально не могла появиться. Таким образом, OLE стало маркой любой технологии, созданной на основе COM. Технологии эти, вообще говоря, очень разные. То есть этот термин скорее стал знаком некой группы новых перспективных технологий (как, например, термин Искусственный Интеллект объединяет и технологию автоматического доказательство теорем, и обработку естественного языка, и программирование действий роботов). В 1996 Microsoft “родила” новый термин – ActiveX. Сначала этот термин относился только к технологиям, связанным с Internet. Затем он стал “захватывать территории”, традиционно принадлежащие OLE. И теперь, на осень 1997 года, состояние дел таково:
Достаточно точное, но весьма “заумное” определение технологии ActiveX звучит следующим образом: © ActiveX – расширяемая архитектура, основанная на множестве ключевых приспосабливаемых к требованиям пользователя (=кастомизируемых) сервисов, каждый из которых обеспечивает создание пользовательских сервисов любой сложности, которые в свою очередь расширяют данную архитектуру. Все сервисы, вне зависимости от их сложности, реализации, места в памяти и места их выполнения, могут быть использованы всеми приложениями, ОС, либо другими сервисами. ActiveX – это не технология для написания частей приложения (как например, Win32 API). ActiveX применяется для совместного использования частей приложения с чем-либо еще и для доступа к таким разделяемым компонентам.
В предыдущих рассуждениях использовалось понятие сервис. С него и начнем. © Под сервисом будем понимать часть программного обеспечения, отвечающего за решение конкретной четко определенной подзадачи в рамках решения общей проблемы. Физически сервисы могут быть выражены разными способами. ¨ Например, сервисы ОС физически могут предоставляться через системные вызовы, сервисы библиотек – через вызовы функций этих библиотек и т.п. Другой взгляд на проблему препарирования программного обеспечения приводит к понятию компоненты. © Обычно под компонентой понимается часть программного обеспечения, которая также решает какую-то определенную подзадачу общей задачи, но разбиение задачи происходит не по функциональному признаку, а с точки зрения удобства программирования, распространения, инсталляции и т.п. Реально сервис может состоять из одной и более компонент, но наиболее часто компонента и является сервисом. В любом случае, сама компонента состоит из одного или более объектов, где каждый объект обеспечивает свои функциональность и содержимое через один или более интерфейсов. В свою очередь, интерфейс содержит одну или более функций-членов (= методов). Именно через них компонента и может делать нечто. ¨ Существуют простые сервисы распределения памяти, которые имеют одну компоненту, один объект и один интерфейс на объект. С другой стороны, другие компоненты могут иметь несколько объектов, каждый из которых выставляет несколько интерфейсов для доступа к множеству свойств. В целом, © Компонентное программное обеспечение – практическая, ориентированная на потребителя реализация ориентированных на разработчика принципов объектно-ориентированного программирования. Т.е., это взгляд на компьютерное окружение, в котором разработчики и конечные пользователи могут последовательно добавлять новые свойства в свои приложения, просто покупая дополнительные компоненты. Подход, исповедуемый компонентной технологией, отличается также от подхода, принятого в открытой архитектуре сервисов Windows (=Windows Open Service Architecture – WOSA). Если в WOSA между клиентом и сервисами должен был находится некий менеджер, который бы управлял ходом работ (например, в MAPI, ODBC и т.п.),
то при компонентном подходе и использовании СОМ СОМ отвечает лишь за установление соединения, а дальше клиент и компонента общаются напрямую.
Смысл существования технологии ActiveX – способствовать развитию компонентного программного обеспечения и интеграции компонент. 1.2. Клиенты и серверы Как отмечалось ранее, сервис или компонента состоит из одного или более объектов, каждый из которых реализует определенный набор свойств компоненты. Эти объекты устанавливают коммуникационные каналы между пользователем этих объектов – некоторым фрагментом кода, и тем, кто обеспечивает этот объект (=провайдером). © Будем называть клиентом фрагмент кода, которому обеспечивается доступ к функциональности и содержимому объекта. © Будем называть сервером фрагмент кода, который отвечает за обеспечение клиентов компонентами и составляющими их объектами. С точки зрения программирования сервер – это загружаемый по запросу модуль, такой как DLL или EXE, который делает компоненты и их объекты доступными из внешнего мира. Без сервера объекты оставались бы скрытыми от внешнего мира. Сервер как бы “держит их на серебряной тарелочке и предлагает отведать клиентам”. На рисунке показаны отношения между клиентом и сервером в технологии ActiveX. Клиент использует компоненту или сервис, предлагаемый сервером, и коммуникация между клиентом и сервером осуществляется через ActiveX-объекты.
1.3. Понятие объекта Технология ActiveX описывает сервисы как “основанные на объектах”. Но, что такое объекты в контексте ActiveX? Концепция, которая формирует идею объекта ActiveX называется Component Object Model (COM). Вообще, после “переименования” OLE в ActiveX термин “объект ActiveX” почти не используется. Гораздо чаще употребляется термин “объект COM” и именно им мы и будем пользоваться. В настоящее время COM является чрезвычайно популярной, но далеко не единственной объектно-ориентированной технологией. (Существует, например, спецификация CORBA, также исповедуемая весьма солидными производителями программного обеспечения). Что же дает в настоящее время право технологии называться объектно-ориентированной (и что такое объект COM)? Объектная ориентированность должна обеспечивать поддержку трех свойств: инкапсуляцию, полиморфизм, наследование. · инкапсуляция заключается в том, что все детали о составе, структуре и внутренней работе объекта скрыты от клиента. Вообще, видение клиентом объекта называется интерфейсом объекта. Говорят, что внутренности объекта скрыты за его интерфейсом. Инкапсуляция предохраняет данные объекта от нежелательного доступа, предотвращая случайные, некорректные изменения объекта. Это, безусловно, сказывается на качестве разрабатываемого ПО. Как известно, объектно-ориентированные языки программирования (например, С++) также поддерживают инкапсуляцию (хотя и позволяют ее обойти, например, через дружественные функции классов). Хотя COM – не язык программирования, идея та же: доступ к данным объекта осуществляется только через методы его интерфейса. · полиморфизм заключается в возможности видеть два сходных объекта через общий интерфейс, избегая, таким образом, необходимости различать эти объекты. ¨ Рассмотрим, например, пишущие инструменты: ручки и карандаши. Каждый из них может иметь различные тип, размер, цвет, но все они разделяют общий интерфейс того, как их держать и ими писать. Все указанные объекты полиморфны через этот интерфейс – любой из них может быть использован в одной манере, так же как и другие инструменты, которые поддерживают этот интерфейс. На данном примере становится понятным и еще одно, широко используемое в объектно-ориентированном программировании понятие – понятие класса. “Ручки” – это суть один класс, “карандаши” – суть другой класс, а вот конкретный объект, например, моя шариковая ручка с надписью “Microsoft” – экземпляр данного класса “ручки”. В COM © класс – это конкретная реализация набора интерфейсов. Могут существовать различные реализации набора интерфейсов, каждая из которых будет отдельным классом. Клиент может ничего не знать о конкретной реализации, т.е. о конкретном классе объектов. Т.е. он может работать с объектами разных классов совершенно одинаково. Это и есть основная идея полиморфизма.
¨ Например, базовый класс “пишущие инструменты” является основой для дочерних классов “шариковая ручка”, “карандаш”, “перьевая ручка” и т.д. Таким образом, идея наследования проста – имея исходный объект, можно создать новый, автоматически поддерживающий все функции базового. Существует 2 вида наследования:
В первом случае объект наследует программный код родителя. Т.е. обращение к унаследованной функции в дочернем объекте на самом деле вызовет исполнение кода в родительском объекте. Во втором случае объект наследует лишь определение методов родительского объекта, т.е. наследует лишь спецификацию. Т.е. при обращении к унаследованной функции в дочернем объекте исполняется код, обеспеченный самим дочерним объектом. Языки объектно-ориентированного программирования типа С++ или Smalltalk поддерживают и наследование реализации, и наследование интерфейса. Объекты СОМ поддерживают только наследование интерфейса. Основная причина – опасение создателей СОМ за то, что при наследовании реализации дочерний объект будет иметь доступ к коду базового объекта, что противоречит концепции СОМ (например, идее инкапсуляции). Несмотря на отсутствие наследования реализации СОМ оставляет возможность для повторного использования компонент, определив два механизма:
В следующем параграфе эти механизмы будут кратко описаны с использованием понятия интерфейса. (!) Таким образом, объект COM – это настоящий объект:
1.4. Понятие интерфейса СОМ СОМ проектировалась так, чтобы быть независимым от языков программирования, аппаратной архитектуры и других технических деталей. СОМ не противопоставляется обычному объектно-ориентированному программированию: языки и методы объектно-ориентированного программирования весьма полезны для решения проблем пользователей СОМ. Природа объекта СОМ определяется не тем, как он внутренне реализован, а тем, как он виден снаружи. Пусть некоторый программный объект имеет определенные свойства (данные и содержание) и методы (функциональность). Доступ к этим свойствам и методам внутри объекта определяется использованием языка программирования.
Так вот доступность членов объекта из его родного языка программирования не интересна СОМ. СОМ интересно то, как обеспечить доступ к возможностям объекта за пределами его мира, который может не соответствовать внутренней структуре объекта. Именно внешний вид объекта, т.е. то, как клиент объекта будет иметь доступ к его функциональности и содержимому, и есть та вещь, которую помогает определить и реализовать СОМ. © В СОМ все свойства объекта собраны в одну или несколько групп семантически связанных функций, где каждая группа называется интерфейсом. Microsoft уже определил большое количество интерфейсов, которые представляют многие общие свойства объектов. Например, группа функций, описывающих структурный обмен данными, легко специфицируется и, таким образом, может быть стандартным СОМ-интерфейсом. Можно также свободно определить пользовательский (custom) интерфейс для своих нужд. Он также естественно попадает в архитектуру СОМ, как и стандартные интерфейсы. Независимо от того, как определяется интерфейс объекта, весь доступ к объекту осуществляется через методы интерфейса. Это означает, что СОМ не допускает прямой доступ к внутренним переменным объекта. Причины такого подхода:
Таким образом, © Метод – это функция или процедура, выполняющая некоторое действие и вызываемая программным обеспечением, использующим данный объект (=клиентом объекта). В двоичном стандарте интерфейса объект обеспечивает реализацию каждой функции интерфейса и создает массив указателей к этим функциям, называемый vtable (“virtual function table”, т.к. проектирование СОМ-интерфейса шло на основе структуры объекта С++, который имеет виртуальные функции). Vtable разделяется всеми экземплярами класса объекта. Для того, чтобы экземпляры различались между собой, код объекта реализует некую вторую структуру (первая – vtable), которая содержит собственные (приватные) данные экземпляра. В спецификации интерфейса СОМ сказано, что первые 4 байта в этой структуре данных содержат 32-битный указатель на vtable. Указатель на интерфейс – это указатель на вершину этой второй структуры экземпляра, т.е. указатель на указатель на vtable. Именно через этот указатель на интерфейс клиент имеет доступ к объекту, т.е. он может вызывать функции-члены интерфейса, но не имеет доступ к приватным данным.
Эта структура напоминает ту, что генерирует компилятор для экземпляров объектов С++. Однако, такая же интерфейсная структура легко создается и на других языках, например, С. Так как данные экземпляров располагаются в зависимости от языка и компилятора, клиент не имеет доступа к этим данным непосредственно через указатель на интерфейс. Фактически, клиент вообще никогда не имеет указатели к объектам, только к интерфейсам. С другой стороны, конструкция pInterface->MemberFunction( . . . ) позволяет всегда вызывать функцию по имени и обеспечивает проверку типов аргументов. Это лучше, чем вызывать функции в массиве указателей на них через смещение (следовательно, без проверки типов). Конечно, неудобно рисовать полную бинарную структуру каждый раз, когда надо нарисовать объект. По принятому соглашению, интерфейсы рисуются как штекеры, вставленные в объект. Когда клиент хочет использовать объект через конкретный интерфейс, он просто “включается” в этот интерфейс. Т.е. клиент должен иметь то, что вставлять – код, который знает, как использовать члены интерфейса.
Такое представление объекта и интерфейса позволяет понять один важный тезис: интерфейс не есть объект. Интерфейс лишь один из каналов для объекта. Поэтому принято перед всеми интерфейсами писать префикс – заглавную “I” (IUnknown, IdataObject и т.д.). Символьное имя интерфейса описывает основу функциональности, определенную в этом интерфейсе. НО, в период исполнения (= run time) интерфейс идентифицируется не по символьному имени, а по двоичному 128-битному идентификатору. Кстати, класс С++ идентифицируется только на этапе компиляции по текстовому имени. Один объект обычно имеет более одного интерфейса, что и показано на рисунке. ¨ Объект, отвечающий за лингвистическое обеспечение, например, текстового процессора, может иметь:
Отметим, что скрытие объекта за его интерфейсом в точности соответствует фундаментальному свойству инкапсуляции. СOМ также поддерживает идею полиморфизма между интерфейсами. Два интерфейса могут разделять общее множество функций – базовый интерфейс – в своих vtables.
Для определения таких отношений хорошо работает наследование из С++. Интерфейс IUnknown имеет три функции и является общим базовым интерфейсом для всех других интерфейсов в СOМ. Все интерфейсы полиморфны с IUnknown. Этот интерфейс обеспечивает 2 базовых характеристики объекта СОМ:
Вернемся к вопросу повторного использования и к механизмам включения и агрегирования. Очевидно, что клиент объекта сам может быть объектом. Такой клиентский объект может реализовать любой из интерфейсов, используя внутреннюю реализацию интерфейса другого объекта. Это свойство получило название включение. Клиентский объект содержит внутри себя повторно используемый объект и внешние клиенты этого клиентского объекта не знают о таком повторном использовании. Включение весьма общий метод повторного использования в СОМ и не требует специальной поддержки от объектов. ¨ Некая фирма оказывает услуги по прогнозу погоды. Клиент звонит на фирму, оператор идет по ссылке на соответствующий веб-сайт другой компании, смотрит там прогноз погоды и читает его клиенту. В некоторых специальных случаях один объект может захотеть непосредственно выставить указатель на интерфейс другого объекта как свой собственный. Это требует установления специального отношения, называемого агрегирование. ¨ Например, эта же фирма называет клиенту по телефону адрес веб-сайта с прогнозом погоды, клиент теперь пользуется напрямую услугами этой третьей фирмы, но деньги переводит на счет первой фирмы, т.к. прогноз погоды входит в ее перечень услуг. 1.5. Для чего нужна СОМ? Использовавшиеся ранее объектные технологии имели ряд проблем:
СОМ решает все указанные проблемы:
Идея решения проблемы – в возможности для объекта иметь несколько интерфейсов. Вся новая функциональность должна вводиться через новые интерфейсы, никак не затрагивая старые!
Данный раздел пока опубликован не будет. С одной стороны, обзоры технологий ActiveX есть во всех книгах по данной теме. С другой стороны, сейчас семейство технологий ActiveX растет неимоверными темпами. Поэтому ранее популярные схемы, описывающие внутреннюю иерархию в семействе этих технологий, устаревают чрезвычайно быстро. Автор вынашивает идею создания действительно актуального обзора всех технологий семейства ActiveX. 3. Основы СОМ-модели 3.1. Идентификация объектов. Глобальные уникальные идентификаторы (GUIDs). Итак, нам уже известно, что существуют объекты, реализующие некоторые полезные функции. Единственный способ программно взаимодействовать с объектом – использовать его интерфейсы. НО, как получить первый указатель на интерфейс к объекту? Имеются 4 способа сделать это:
В соответствии с четырьмя способами получения первого указателя на объект есть четыре основных пути уникально идентифицировать класс:
Все способы объединяет одно – уникальность. Но четвертый метод хитрее других. В нем компоненты уникально идентифицируются внутри всей системы или даже сети. Распределенное окружение имеет потенциально миллионы компонент, объектов и интерфейсов, которые нужно уникально идентифицировать. Использование символьных “читабельных” имен для нахождения и связывания всех этих элементов будет приводить к коллизии. А ведь многие компоненты и интерфейсы разрабатываются в разное время разными людьми в различных местах. Как же гарантировать уникальность? Проблема уникальной межсетевой идентификации не выдумана СOМ. Фактически, эта проблема появилась с Remote Procedure Calls (RPC). Поэтому организация Open Software Foundation (OSF) ввела понятие универсального уникального идентификатора (Universally Unique Identifier - UUID) как часть своего распределенного вычислительного окружения (Distributed Computing Environment - DCE). © UUID, который в COM и OLE известен как GUID (произносится “гуид”) – 128-битное (16-байтное) целое, которое фактически гарантированно уникально для всего мира в пространстве и времени. В СОМ GUID используются для того, чтобы программно идентифицировать классы компонент (в таких случаях они называются class ID = CLSID) или интерфейсы (в таких случаях они называются interface ID = IID). В различных ситуациях нужно поддерживать один или более GUID для обозначения собственных реализованных компонент или описанных интерфейсов. Для этой фундаментальной цели COM обеспечивает API-функцию CoCreateGuid, которая, на самом деле, вызывает функцию Win32 RPC API UUIDCreate. Эта функция выполняет специфицированный OSF DCE алгоритм, который использует комбинацию следующей информации для генерации GUID:
Шанс генерации двух одинаковых GUIDs ничтожно мал. Подсчитано, что используемый сейчас алгоритм генерации GUID начнет выдавать повторяющиеся значения в 3400 году. Хотя можно создавать GUID с CoCreateGuid в период выполнения, обычно он получается однажды и присваивается разрабатываемой компоненте. Обычно имеется некое средство с именем типа UUIDGEN.EXE или GUIDGEN.EXE, которое генерирует идентификатор. Он имеет вид: 3fad3020-16b7-11ce-80eb-00aa003d7352 в принятом шестнадцатиричном стандарте. Через несколько секунд идентификатор станет другим: 42754580-16b7-11ce-80eb-00aa003d7352 Свой собственный GUID нужно определить где-либо в исходном коде (GUID, определенные OLE, описываются в заголовочных файлах или связующих библиотеках). GUID можно рассматривать как следующую структуру данных: typedef struct GUID { DWORD Data1; WORD Data2; WORD Data3; BYTE Data4[8]; } GUID; typedef GUID CLSID; typedef GUID IID; Каждое поле структуры представляет собой фрагмент GUID между ‘-‘, позволяя легко адресоваться к каждой части. Например, для использования последовательности идентификаторов при отладке достаточно распечатывать первое DWORD GUID. Структура на С может иметь следующий вид: MyGUID = { /* 891a0d90-16b7-11ce-80eb-00aa003d7352 */ 0x891a0d90 , 0x16b7, 0x11ce, {0x80, 0xeb, 0x00, 0xaa, 0x00, 0x3d, 0x73 , 0x52} }; Тем не менее, обычно непосредственно GUID не манипулируют. Обычно используют символьную константу или переменную, чье абсолютное значение не имеет значения. Существует несколько функций COM API для работы с CLSID:
3.2. Идентификаторы классов (CLSID) и программные идентификаторы (ProgID). Для хранения информации, относящейся к компонентам, COM использует системный реестр. Нелишне будет напомнить структуру реестра, которая является иерархической. Каждый элемент реестра называется разделом (=key). Раздел может включать:
Подразделы, в свою очередь, могут содержать другие подразделы и параметры. В реестре системы, где инсталлировано OLE, будет существовать раздел, названный CLSID. Этот раздел “растет” из корня одной из ветвей реестра, называемой HKEY_CLASSES_ROOT. В разделе CLSID перечислены CLSID всех компонент, установленных в системе. Первым входом, который нужно создать для конкретной компоненты, является подраздел в разделе CLSID, представляющий собой шестнадцатеричную форму CLSID типа 42754580-16b7-11ce-80eb-00aa003d7352. Параметром по умолчанию данного подраздела будет человекочитаемое “дружеское имя”, которое предполагается связать с подразделом. В имени возможны все символы, включая пробелы и пунктуацию. Это имя может быть показано в программах конечному пользователю. ¨ HKEY_CLASSES_ROOT . . . CLSID {42754580-16b7-11ce-80eb-00aa003d7352} по умолчанию = DBEngine для DAO InprocServer32 по умолчанию = C:\WINDOWS\SYSTEM\DAO350.DLL TreadingModel = Apartment ProgID по умолчанию = DAO.DBEngine.35 VersionIndependentPorgID по умолчанию = DAO.DBEngine . . . Что же в разделе HKEY_CLASSES_ROOT скрывается за многоточием?
Итак, что же такое программный идентификатор? CLSID может быть связан с программным идентификатором (ProgID), который эффективно (но менее точно) идентифицирует тот же класс. ProgID - это текстовая строка без пробелов, которая может быть использована в программном контексте вместо неудобочитаемого CLSID. Т.е., фактически, это независимое от языка (т.е. может быть использовано не только из С и С++) альтернативное символьное имя класса. Стандартный формат для ProgID раньше предлагался как: <Производитель>.<Компонента>.<Версия> ¨ Microsoft.Chart.5 ¨ Lotus.AmiProDocument.4.2 Теперь более распространен вариант <Программа>.<Компонента>.<Версия> ¨ Visio.Drawing.4 ¨ PowerPoint.Slide.8 Этот формат достаточно уникален, т.е. обычно коллизий не будет (это обеспечивает законодательство по охране торговых марок). Указанный формат – не жесткое правило, а лишь рекомендация, и в любом Реестре можно найти для него опровержения. Имеется также специальный VersionIndependentProgID, который имеет тот же формат, но без номера версий. И ProgID, и VersionIndependentProgID компоненты приводятся в разделе его CLSID. Однако, основное назначение ProgID – искать по нему соответствующий CLSID. Для эффективной организации поиска все ProgID указываются непосредственно под HKEY_CLASSES_ROOT. Так как сами PorgID не предназначены для представления пользователям, то их значения по умолчанию – “дружественные имена”. В разделе ProgID имеется подраздел с именем CLSID, который содержит CLSID компонента в качестве значения по умолчанию. Не зависящий от версии ProgID также приводится непосредственно в разделе HKEY_CLASSES_ROOT и имеет дополнительный подраздел CurVer, содержащий ProgID текущей версии компоненты. ¨ HKEY_CLASSES_ROOT Acme.Component.3 = Acme Component Version 3.0 CLSID = {42754580-16b7-11ce-80eb-00aa003d7352} Acme.Component = Acme Component CurVer = Acme.Component.3 СОМ обеспечивает функции для получения ProgID по CLSID и наоборот из реестра:
3.3. Категории компонент Как узнать, умеет ли компонента делать ту или иную работу? Наверное, стоит понять, какие интерфейсы компонента поддерживает. Есть различные способы добиться желаемого. Один из них позволяет узнать о поддерживаемых компонентой интерфейсах, не создавая экземпляр компоненты. Этот способ заключается в использовании категорий компонент. © Категория компонент – это набор интерфейсов, которому присвоен GUID, называемый в таком случае CATID. Компоненты, реализующие все интерфейсы данной категории, могут зарегистрироваться как члены данной категории. Клиенты могут более осмысленно выбирать компоненты из реестра, рассматривая только компоненты определенной категории. Регистрируя себя в некоторой категории, компонента гарантирует, что поддерживает ВСЕ интерфейсы данной категории. Использование категорий аналогично использованию абстрактных базовых классов в С++. Абстрактный базовый класс – это набор функций, которые производный класс обязан реализовать. Поэтому производный класс – конкретная реализация данного абстрактного базового класса. Аналогично, компонента определенной категории – конкретная реализация этой категории. Компонента может входить в любое число категорий, так как может поддерживать любое число интерфейсов. Работа с категориями в системах Windows для программистов облегчена, т.к. в систему входит стандартный Диспетчер категорий компонент (=Component Category Manager), сам являющийся компонентой. Диспетчер может использоваться:
3.4. Определение интерфейсов Объект и клиент должны иметь заранее согласованный способ описания интерфейса. СОМ не регламентирует, как это должно быть сделано. НО, в любом случае, СОМ-объект точно должен следовать стандарту двоичного интерфейса СОМ. Давайте обобщим, что же входит в стандарт двоичного интерфейса:
В таком жутком правиле можно запутаться, поэтому напомню рисунок и то, что массив указателей на функции-члены интерфейса называется Таблицей виртуальных функций (или vtable).
В принципе, это правило может быть отдельно детализировано для каждого конкретного метода, но это не практично. В реальности, на конкретной платформе все методы всех интерфейсов используют единое соглашение о вызове. ¨ В Win32 используется соглашение о вызове __stdcall. Это соглашение о вызове в стиле Паскаль. Суть соглашения в том, что функция выбирает параметры из стека перед возвратом в вызывающую процедуру (В соответствии с соглашением о вызове С/С++ стек очищает вызывающая процедура). Соглашение в стиле Паскаль используют не только функции СОМ, но и все функции Win32 с постоянным числом аргументов. Правило это введено для единообразного взаимодействия объектов СОМ как внутри процессов, так и за их границами. Несмотря на наличие двоичного стандарта, удобно было бы иметь некий стандартный инструмент для определения интерфейсов. Такой инструмент в СОМ имеется – это язык описания интерфейсов (=Interface Definition Language – IDL). IDL имеет свою историю. Он является расширением языка IDL Microsoft RPC, который сам заимствован у IDL OSF DCE (напомню, что это Open Software Foundation Distributed Computing Environment). Разработчик может определить новый “кастомизированный” интерфейс посредством написания файла определения интерфейса (=interface definition file). Этот файл использует IDL для описания типов данных и методов интерфейса. Файл определения интерфейса содержит информацию, которая определяет реальный контракт между приложением-клиентом и серверным объектом. Этот контракт специфицирует 3 вещи:
Использование IDL тесно связано с библиотекой типов, поэтому вернемся к нему позднее. 3.5. Реализация интерфейсов Хотя СОМ совершенно безразлично, на каком именно языке будет реализовываться компонента и ее интерфейсы, типичным языком реализации является С++. ¨ Рассмотрим пример, в котором присутствует компонента с двумя интерфейсами: class IX // Интерфейс 1 { public: virtual void Fx1() = 0; virtual void Fx2() = 0; }; class IY // Интерфейс 2 { public: virtual void Fy1() = 0; virtual void Fy2() = 0; }; class CA : public IX, // Компонента public IY { public: // реализация интерфейса 1 virtual void Fx1() {A=1;} virtual void Fx2() {B=2;} // реализация интерфейса 2 virtual void Fy1() {A=2;} virtual void Fy2() {B=1;} }; Интерфейсы IX и IY описаны как чисто абстрактные базовые классы. Напомним, что: 1) © Чисто абстрактный базовый класс (=pure abstract base class) – базовый класс, содержащий только чисто виртуальные функции. 2) © Чисто виртуальная функции (=pure virtual function) – функция, которая не реализуется в классе, в котором определена, о чем свидетельствует спецификатор =0. К приведенному примеру следует дать еще ряд комментариев:
Принято использовать ключевое слово interface в определении классов, которое в файле objbase.h из Win32 SDK определяется как #define interface struct Использование struct вместо class позволяет избавиться и от слова public. С учетом замечаний о принципах передачи параметров интерфейс, например, IX будет выглядеть как: ¨ interface IX // Интерфейс 1 { virtual void _stdcall Fx1() = 0; virtual void _stdcall Fx2() = 0; }; Напомним еще раз, что интерфейс в СОМ не может быть изменен. В случае необходимости модификации интерфейса нужно просто добавить интерфейс с новым именем. 3.6. Код возврата HRESULT За исключением специальных случаев, почти каждая COM и OLE API функция и почти каждая функция-член интерфейса возвращает величину типа HRESULT. Дословно, HRESULT - handle of result, т.е. описатель результата. В Windows обычно “описателями” называют некие ссылки или идентификаторы самого предмета ссылки (например, “описатель” окна), но в данном случае это неверно. Фактически, HRESULT сам представляет некоторое 32-разрядное значение результата.
Старший бит HRESULT отмечает, успешно или нет выполнена функция. Этот бит в сочетании с другими позволяет определить множество кодов как в случае успеха функции, так и при неудачи. СОМ предлагает следующую конвенцию имен для кодов. Если в имени есть “E_”, то это ошибка. Например, E_FAIL или RPC_E_NOTCONNECTED. Если в имени есть “S_”, то это успех. Например, S_TRUE, S_FALSE, или STG_S_CONVERTED. Есть два макроса SUCCEEDED и FAILED, определяющие, в какую категорию попадает код. Наиболее общие HRESULT:
На код ошибки влияет старший бит и последние 16. 15 бит (с 30-го по 16-ый) содержат идентификатор, позволяющий определить место в ОС, где произошла ошибка. Таким образом, данный идентификатор позволяет лучше понять и возможную причину ошибки. ¨ FACILITY_RPC – ошибка в RPC FACILITY_CONTROL – ошибка в управляющих элементах ActiveX. Такая структура HRESULT позволяет разработчикам различных групп, работающих над различными частями ОС, не согласовывать коды ошибок между собой. Все идентификаторы задают стандартизированные СОМ коды возврата. Существует лишь одно исключение: FACILITY_ITF. Он дает возможность определять коды, специфичные для данного интерфейса. Если разработчик пишет свои компоненты со своими (нестандартными) интерфейсами, он должен помечать код возврата с помощью FACILITY_ITF. 3.7. Правила управления памятью Для СОМ типичным является случай, когда функции API или методы интерфейса, написанные одним программистом, вызываются из кода, написанного другим программистом. Вспомним, что принципиально существую 2 способа передачи параметра в функцию и возврата значения из функции:
Когда программист имеет дело с СОМ, то лучше, где возможно, передавать параметры по значению. НО, что, если нужно передать, например, структуру данных? Мало того, что структуру нужно передавать по ссылке, нужна также договоренность между передающим и принимающим параметры, кто из них будет выделять и освобождать память под структуру. Теоретически, можно договариваться об этом для каждой функции, но практически это нереально. Поэтому, существует общее соглашение о передаче параметров по ссылке (это соглашение не касается передачи указателей на интерфейсы – там вопрос особый). Каждый параметр и возвращаемая величина может принадлежать одной из трех групп:
Для двух последних случаев необходимо, чтобы используемые обеими сторонами “распределители” и “освободители” памяти действовали адекватно. Поэтому СОМ рекомендует использовать стандартный механизм распределения памяти. Другая причина использования такого механизма – корректная передача памяти за пределы границ процесса. Стандартный механизм распределения памяти СОМ называется сервисом COM для распределения памяти задачи (=COM's task memory allocation service) и основывается на API управления памятью конкретной системы. Этот сервис обеспечивается через специальный объект – распределитель (=allocator), который поддерживает единственный интерфейс IMalloc. Все компоненты должны использовать этот сервис для обмена распределенной памятью между компонентами. Код задачи имеет доступ к этому сервису через функцию СОМ API CoGetMalloc. ¨
Функция CoGetMalloc возвращает новый интерфейсный указатель, поэтому на нем увеличивается счетчик ссылок и нужно не забыть вызвать Release по окончанию. (Первый аргумент всегда передается как MEMCTX_TASK). С объектом-распределителем можно вызывать любые функции интерфейса IMalloc, который описан как следующий:
Эти функции действуют как стандартные С-функции периода выполнения. Поясним лишь, что функция DidAlloc возвращает 1, если память распределена распределителем, содержащимся в функции, 0 – если память не распределена, -1 – если распределитель ничего не знает. Остановимся еще на одной проблеме: что делать с out и in-out параметрами в случае ошибки в вызванной функции? Ведь тогда вызвавший не сможет корректно очистить память. Нужно следовать дополнительным правилам:
Указанные соглашения являются стандартными для СОМ (API и интерфейсов), но внутри компонент разработчик может их и не соблюдать. 3.8. Интерфейс IUnknown. Функция QueryInterface. Интерфейс IUnknown – это тот интерфейс, который должны реализовать все объекты. Он определяет “объектность” СОМ-технологии. Этот интерфейс инкапсулирует две операции:
Поскольку все интерфейсы СОМ наследуют IUnknown, в каждом интерфейсе есть функции QueryInterface, AddRef, Release – три первые функции в vtable. Остановимся пока на функции QueryInterface. © QueryInterface - фундаментальный механизм, через который клиент может запросить объект о поддерживаемых им свойствах, спрашивая указатель на специфический интерфейс. Сама функция QueryInterface довольно проста: передает IID и out-параметр для указателя, и если функция возвращает NOERROR, у Вас есть новый интерфейсный указатель. ¨ Если имеется указатель IUnknown в переменной pIUnknown, и нужно запросить объект, имеет ли он некоторый тип информации, запрашивается IProvideClassInfo:
Вызов QueryInterface запрашивает объект, поддерживает ли он свойство, идентифицированное IID соответствующего интерфейса. Если вызов QueryInterface успешен, перед возвратом будет вызываться AddRef через выходной параметр (&pCPI), так что клиент должен вызвать для него Release. Есть ряд преимуществ такого подхода:
¨ Пусть клиент хочет запоминать состояние объекта. Он может, во-первых, запросить IPersistStorage. Если это сработает, клиент указывает объекту запомниться в IStorage. Если запрос неудачен, клиент запрашивает IPersistStream, и если он работает, объекту указывается запоминаться в IStream. Если и второй запрос неудачен, можно попытаться запросить IPersistFile. Если опять неудача, можно попытаться вернуть родные данные объекта через IDataObject. Рассмотрим это свойство подробнее. © Процесс запроса объекта о поддерживаемых им свойствах называется интерфейсными переговорами (хотя это достаточно простые переговоры). Процесс позволяет произвольному клиенту динамически (в период выполнения) определять наибольшее число интерфейсов, которые реализует объект из множества интерфейсов, которые клиент знает, как использовать. ¨ Пусть человек знает французский и английский язык. На них с ним можно общаться. Если он выучит немецкий, он объявит об этом, и с ним можно будет общаться по-немецки. © Процесс, при котором компоненты дополняют возможности и свойства, оставаясь при этом совместимыми со своими старыми версиями, и называется корректная эволюция содержимого во времени. Эта идея не только мощная, но и очень эффективная, так как переговоры происходят на уровне интерфейсов, а не функций, что требовало бы больше ресурсов. Когда объект модифицирован для поддержки нового интерфейса, тот же клиент, без всякой рекомпиляции, нового распространения или других изменений, автоматически получает преимущество от дополнительного интерфейса. Т.о., это действительно компонентное программное обеспечение: компоненты оцениваются независимо и сохраняют полную совместимость. Данный процесс работает также в другом направлении. Реализуя новую функциональность (дополнительный интерфейс), компоненты не теряют совместимость с существующим клиентом. Правила интерфейса QueryInterface:
3.9. Интерфейс IUnknown. Подсчет ссылок. Клиент и компонента – два независимых программных модуля. Но, т.к. компонента “обслуживает” клиента, встает вопрос: должен ли клиент сам управлять временем жизни компоненты? Т.е. должна ли компонента создаваться и разрушаться по прямому указанию клиента? Про создание мы поговорим в следующих параграфах, а пока рассмотрим вопрос разрушения. С одной и той же компонентой клиент может работать через разные интерфейсы. Если некоторый интерфейс больше не нужен и клиент компоненту удалит, естественно, сорвется и работа через другие интерфейсы. А ведь одной компонентой могут пользоваться и несколько клиентов. Можно, конечно, вообще не выгружать компоненту до конца работы клиента, но такое решение неэффективно. Обычно, клиент знает, когда он начал использовать интерфейс и когда закончил, но не знает, когда заканчивается использование всей компоненты. Поэтому имеет смысл ограничиться сообщением об окончании работы с конкретным интерфейсом – и пусть компонента сама отслеживает, когда заканчивается работа со всеми ее интерфейсами. При работе с компонентами используется техника управления памяти, известная как подсчет ссылок (=reference counting). Если говорить коротко, подсчет числа ссылок – это способ объекта управлять временем своей жизни. Подсчет ссылок работает по тому же принципу, что и управление памятью. Также как компонента освобождает память, когда память больше не нужна, объекты разрушаются, когда они больше не используются. Разница в том, что разрушение объекта – не пассивная операция: вместо прямого освобождения объекта клиент должен сказать объекту освободиться самому. Для С++ реализаций счетчик ссылок является переменной типа ULONG и называется обычно m_cRef. Этот счетчик ссылок поддерживает множество независимых указателей на интерфейс. ¨ Если имеется только один клиент объекта и этот клиент имеет два различных интерфейсных указателя на объект, то счетчик ссылок равен 2. Если есть 3 клиента, каждый с одним интерфейсным указателем на объект, счетчик ссылок равен 3. Концепция подсчета ссылок может быть выражена в двух фундаментальных правилах:
Из этих правил вытекают 2 следствия:
Эти следствия иллюстрируются в следующем коде: ¨
Т.о., по первому фундаментальному принципу учета ссылок любая функция, возвращающая указатель к интерфейсу, должна вызывать AddRef через этот интерфейс. Такими функциями являются, например, функции, создающие объект и возвращающие первый указатель к интерфейсу (CreateISomeObject в примере). Каждый раз при создании новой копии указателя необходимо вызывать AddRef через эту новую копию, так как существуют две независимые ссылки (две независимые переменные-указатели) к этому объекту. Затем, в соответствии со вторым принципом, для всех AddRef должен быть соответствующий вызов Release. Реализация Реализации AddRef() и Release() достаточно просты. ULONG __stdcall AddRef() { return ++m_cRef; } ULONG __stdcall Release() { if (--m_cRef == 0) { delete this; return 0; } return m_cRef; } Вообще, вместо прямого инкремента и декремента грамотнее использовать функции Win32 API InterlockedIncrement() и InterlockedDecrement(), которые позволяют изменять счетчики в многопотоковой модели. Оптимизация Всегда ли обязательно следовать указанным правилам вызова функций AddRef() и Release()? Оказывается, указанные правила можно оптимизировать. Оптимизация будет зависеть от пересечения времени жизни ссылок. Есть 2 случая:
К 1) В приведенном выше примере каждый экземпляр pCopy вкладывался внутри времен жизни pISome1 и pISome2. Т.е. копия жила и умирала внутри времени жизни оригинала. После вызова CreateISomeObject оба объекта имели счетчик ссылок = 1. Время жизни объекта ограничивалось этими вызовами и вызовом Release. Поэтому, т.к. время жизни известно, можно не вызывать другие AddRef и Release через копии указателей. ¨
Есть 3 случая, когда можно использовать преимущества такой оптимизации:
К 2) Времена жизни перекрываются, когда оригинальный указатель умирает после рождения копии, но до ее смерти. При этом копия может наследовать владение счетчиком ссылок: ¨
Время жизни объекта – между CreateISomeObject и pCopy-> Release. Release вызывается через оригинальный указатель на интерфейс, хотя это и другая переменная. С учетом этих оптимизаций, существует только 4 специфических случая, когда AddRef должна быть вызвана явно для новой копии указателя (и должен быть соответствующий Release, когда копия разрушается):
В любом случае, некоторый фрагмент кода должен вызывать Release для каждого AddRef на указатель. “Круговые” счетчики ссылок Такая проблема возникает, когда несколько объектов держат счетчики ссылок один на другого “по кругу”. Эта проблема требует специального управления. Во всех случаях, когда такие круговые счетчики возможны, в интерфейсы включаются некоторые функции помимо Release, которые будут заставлять один из двух объектов вызывать Release для другого. Искусственный подсчет ссылок “Искусственный подсчет” означает увеличение счетчика ссылок прямо в начале рискованного кода и уменьшения его сразу после. ¨
3.10. Библиотека СОМ Всем клиентам и компонентам СОМ приходится выполнять много типовых операций. Чтобы сделать выполнение этих операций стандартным и совместимым, СОМ предоставляет библиотеку функций. Эти функции реализуют базовые сервисы. В ОС Windows библиотека СОМ носит название OLE32.DLL. Для того, чтобы использовать сервисы, предоставляемые СОМ, приложение (будь то сервер или клиент) обязано сделать 3 вещи:
К1) СОМ определяет номер версии, состоящий из двух частей (мажорный и минорный). Когда компилируется приложение, эти номера указываются в заголовочном файле. Во время исполнения приложение должно сравнить скомпилированные номера с версией доступной библиотеки. Этот номер версии возвращается функцией CoBuildVersion(). DWORD CoBuildVersion(void) В возвращаемом двойном слове старшие 16 бит – мажорный номер версии (rmm), а младшие 16 бит – минорный номер версии (rup). Существует следующее правило: (· ) Мажорный номер версии приложения обязательно должен совпадать с мажорным номером версии библиотеки СОМ. Если не совпадают минорные номера, работа может быть продолжена, хотя какие-то специфические свойства могут быть недоступны. Приложение должно включать специальный код проверки. ¨ DWORD dwBuildVersion; dwBuildVersion=CoBuildVersion(); if (HIWORD(dwBuildVersion)!=rmm) //Ошибка: нельзя запустить, т.к. не совпадает мажорный номер версии if (LOWORD(dwBuildVersion) < rup) //Недоступны некоторые свойства //Продолжение инициализации После того, как проверка сделана, необходимо инициализировать библиотеку, используя функцию CoInitialize(). HRESULT CoInitialize(pReserved) Без инициализации ни одна функция СОМ, кроме функции проверки версии, работать не будет. Обычно, CoInitialize() вызывается лишь однажды в процессе, хотя множественные вызовы не будут ошибкой. На первую инициализацию возвращается S_OK, на последующие – S_FALSE. В случае ошибки возвращаемая величина – E_UNEXPECTED. Каждому вызову функции инициализации (даже повторному) должен соответствовать вызов функции CoUninitialize(). void CoUninitialize(void) Эта функция освобождает ресурсы, выделенные для библиотеки. Так как для одного процесса СОМ инициализируется лишь однажды, логично делать это в EXE, а не в DLL. Когда приложение создает компоненты, им также не надо инициализировать библиотеку. Если в приложении будут использоваться не только технологии СОМ, но и технологии ActiveX более высокого уровня, рекомендуется использовать для инициализации функции OLEInitialize() и OLEUninitialize() вместо функций СОМ. Дело в том, что функции СОМ они вызовут сами. 3.11. Создание компонент. Фабрика класса. Данный параграф посвящен вопросу создания компонент. Не в смысле разработки, а в смысле генерации экземпляра компоненты. Самый простой способ сгенерировать новый экземпляр – использовать специальную функцию библиотеки СОМ: HRESULT __stdcall CoCreateInstance( const CLSID& clsid, // (in) CLSID создаваемого компонента IUnknown* pIUnknownOuter, // (in) Используется для агрегирования DWORD dwClsContext, // (in) Контекст компоненты const IID& iid, // (in) IID используемого интерфейса void** ppv // (out) возвращаемый интерфейс ) Третий параметр – контекст компоненты (=контекст класса) – задает, какие компоненты нужны клиенту:
Так как многим клиентам все равно, с какой компонентой работать, то существуют мнемоники для комбинации флагов (CLSCTX_INPROC, CLSCTX_ALL, CLSCTX_SERVER). ¨ Создание экземпляра компоненты IX* pIX = NULL; HRESULT hr = :: CoCreateInstance ( CLSID_Comp1, // Это CLSID создаваемой компоненты NULL, // Агрегирования нет CLSCTX_INPROC_SERVER, // Нужен сервер “в процессе” IID_IX, // Требуется интерфейс IX (void**) &pIX // Здесь возвращается интерфейс IX ); if (SUCCEEDED (hr)) { pIX->Fx(); // Вызывается функция интерфейса IX …… pIX->Release(); // Необходимо освободить интерфейс IX } Если клиенту требуется создать один экземпляр компоненты, использования CoCreateInstance() может вполне хватить. Однако, если создается несколько экземпляров компоненты, непосредственно функцию COCreateInstance() использовать неэффективно. Дело в том, что COCreateInstance() не создает сразу саму компоненту. Она, на самом деле, создает специальный объект – фабрику класса, которая и занимается созданием нужной компоненты. Единственное, что умеет делать конкретная фабрика класса – это создавать объекты определенного CLSID. Для того чтобы создать некую компоненту через фабрику класса, клиент должен выполнить 2 шага:
к 1) В СОМ существует специальная функция, которая позволяет выполнить первый шаг – CoGetClassObject(): HRESULT __stdcall CoGetClassObject ( const CLSID& clsid, // CLSID нужной компоненты DWORD dwClsContext, // Контекст класса COSERVERINFO* pServerInfo, // зарезервировано для DCOM const IID& iid, // IID нужного интерфейса фабрики void** pv // возвращается интерфейс фабрики ) Видно, что функции COCreateInstance() и CoGetClassObject() очень похожи. НО, CoGetClassObject() возвращает интерфейс не компоненты, а фабрики. Обычно, это интерфейс IClassFactory. к 2) IClassFactory – стандартный интерфейс фабрики класса для создания компонент. Он включает всего 2 метода:
Самое главное, что в этом методе нет CLSID. Т.е. этот метод создает компоненты только конкретного класса.
Итак, функция СОМ CoCreateInstance():
Существуют 2 причины, по которым все-таки используется CoGetClassObject, а не CoCreateInstance: Поговорим немножко об особенностях реализации фабрики класса: 3.12. Поддержка лицензирования. IClassFactory2 добавляет к IClassFactory поддержку лицензирования. Существует два сценария использования компонент: Интерфейс IClassFactory2 позволяет использовать оба сценария. Для этого, кроме наследуемых, используются еще три метода: interface IClassFactory2 : IClassFactory { HRESULT GetLicInfo(…); HRESULT RequestLicKey(…); HRESULT CreateInstanceLic(…); }; Перед созданием компоненты клиент с помощью метода GetLicInfo() может определить, есть ли на машине глобальная лицензия на этот элемент. Если да, клиент может вызвать обычную IClassFactory2::CreateInstance(). Если глобальной лицензии нет, клиент обязан вместо обычной CreateInstance() вызвать CreateInstanceLic(). Этому методу требуется передача лицензионного ключа, который обычно хранится в самом приложении–клиенте. Для получения и последующего использования лицензионного ключа клиент вызывает RequestLicKey() (однако, на машине без глобальной лицензии такой вызов обычно неудачен – обычно приложение-клиент получает ключ из глобальной лицензии при своем создании на машине разработчика). Отметим еще раз, что CoCreateInstance() с IClassFactory2 не работает. 3.13. Маршалинг Как нам уже известно, в СОМ существует три типа серверов:
Когда мы говорим, что клиент имеет ссылку на двоичный интерфейс объекта, что же в действительности понимается под этим? к 1) Если речь идет о сервере в процессе, указатель, который имеет клиент, на самом деле указывает на интерфейс объекта, т.к. сам объект реализован в виде DLL. Ясно, что раз накладные расходы по переадресации вызова отсутствуют, вызов является достаточно эффективным. к 2) Локальный сервер – это независимый процесс, исполняющийся на той же машине. Следовательно, нужен механизм межпроцессных коммуникаций, т.к. прямо клиент не может поддерживать указатель на объект в другом процессе – ведь один и тот же адрес в двух разных процессах ссылается на разные физические адреса. В этом случае указатель, имеющийся у клиента, указывает на специальный объект – заместитель (=proxy), находящийся в процессе клиента. © Заместитель – это СОМ-объект, предоставляющий клиенту те же интерфейсы, что и локальный сервер, с которым собирается работать клиент. Когда клиент вызывает некоторый метод интерфейса, на самом деле, сперва вызывается код заместителя. Роль заместителя – 1) упаковать параметры метода для пересылки между границами процессов и 2) передать запрос и его параметры в процесс, в котором выполняется объект.
Детали самой передачи существенно зависят от особенностей ОС, в которой реализована СОМ. Так, в среде Windows СОМ использует механизм LPC (local procedure call). Это облегченный вариант более общего механизма – RPC (remote procedure call), поэтому его еще называют LRPC (light remote procedure call). В локальном сервере также имеется свой вспомогательный объект – заглушка (=stub). Роль заглушки – 1) распаковать параметры и 2) вызвать метод внутри объекта. Ясно, что вызов метода объекта локального сервера выполняется медленнее, чем вызов метода сервера “в процессе”. к 3) Архитектура для вызова удаленных серверов такая же, как и для вызова локальных. Только в качестве “среды передачи” используется DCOM. Для коммуникации процессов через сеть используется механизм RPC. (!) Важно, что для всех типов серверов объектов с точки зрения клиента обеспечивается прозрачность локальных и удаленных вызовов.
Теперь поподробнее поговорим о роли заместителей и заглушек. Зачем нужна упаковка и распаковка параметров? Дело в том, что в различных процессах (а тем более на различных машинах) форматы представления данных могут различаться. Например, на разных машинах могут по-разному представляться символы, числа с плавающей запятой и т.д. © Упаковка параметров вызова в стандартный формат для пересылки называется маршалингом (=marshaling), а обратная операция распаковки из стандартного формата в форму, приемлемую для принимающего процесса – демаршалингом (=unmarshaling). Т.о., основная задача заместителей и заглушек – выполнение маршалинга и демаршалинга соответственно. Откуда же берутся заместители и заглушки? Есть 2 пути – использовать стандартный (standard) или специализированный (custom) маршалинг. При использовании стандартного маршалинга разработчику ничего не нужно программировать. Просто по спецификации интерфейса на языке IDL компилятор (например, MIDL) сам производит код заместителя и заглушки. Специализированный маршалинг нужен, если разработчик хочет повысить эффективность вызовов, используя специальные знания об объекте и клиенте. Для реализации специализированного маршалинга разработчик должен сам реализовать объекты с интерфейсом IMarshal. 3.14. Язык описания интерфейсов IDL Итак, инфраструктура СОМ полностью независима от исходных средств, применяемых для создания СОМ-компонент. СОМ полностью двоичная спецификация и никакие языки программирования и стандарты не могут повлиять на фундаментальную архитектуру системы. Поэтому, “рекламируя” IDL как удобное и общепринятое средство описания интерфейсов, надо понимать, что его использование не является обязательным с точки зрения СОМ. IDL, как и UUID, и спецификация RPC, был заимствован из OSF DCE. Рассмотрим использование конструкций языка на примере: ¨ import “unknwn.idl” // Интерфейс IX [ object, uuid (32bb8323-b41b-11cf-a6bb-0080c7b2d682), pointer_default (unique) ] interface IX : IUnknown { HRESULT FxStringIn ([in, string] wchar_t* szIn); HRESULT FxStringOut ([out, string] wchar_t** szOut); } Как видно, описание интерфейса на IDL состоит как бы из 3-х частей:
Таким образом можно использовать файлы описания всех стандартных интерфейсов СОМ и ActiveX, которые поставляются вместе с компилятором MIDL. Рассмотрим некоторые ключевые слова:
Для каждого указателя, будь то т.н. “указатель верхнего уровня”, передаваемый в качестве параметра, или указатель внутри передаваемых структуры или объединения, должна быть указана опция передачи. НО, можно не ставить опции перед каждым указателем, а просто определить опцию для указателей по умолчанию. Возможны 3 опции: ref – указатель рассматривается как ссылка: 1) всегда указывает на существующий участок памяти и, следовательно, не может быть NULL 2) никогда не изменяется в течение вызова функции, т.е. до и после вызова объекта указатель указывает на клиенте на один и тот же участок памяти 3) на участок памяти, на которую указывает ссылка, не могут указывать другие указатели функции unique – уникальный указатель: 1) может быть NULL 2) может изменяться во время вызова 3) может распределять новую память на клиенте: когда указатель изменяется из NULL в ненулевой, данные, возвращаемые сервером, записываются на новое место 4) на участок памяти, на которую указывает уникальный указатель, не могут указывать другие указатели функции ptr – полнофункциональный указатель в смысле С.
Компилятор MIDL контролирует, чтобы все функции возвращали значение HRESULT. Это требование весьма полезно, т.к. при поддержке удаленных серверов любая функция может окончиться неудачно из-за сбоев сети, и должен быть способ сообщить об ошибке. Если функции нужно возвратить значение, нужно использовать выходные параметры. Перед каждым параметром функции в квадратных скобках указывается список атрибутов. Это:
При применении к файлу на языке IDL компилятора MIDL будут сгенерированы:
Если в описании будет иметься ключевое слово library, также будет сгенерирована и библиотека типа. Но об этом – в следующем параграфе. 3.15. Информация о типе. Библиотека типов. Для того, чтобы пользоваться каким-либо бытовым прибором, человеку недостаточно знать, какие кнопки и ручки существуют у этого прибора (т.е. его интерфейсы). Нужно иметь информацию о последовательности их применения, т.е. руководство пользователя. То же самое относится и к объектам ActiveX и COM: клиент не хочет тратить время на запрос у объекта всех возможных интерфейсов, для того чтобы узнать, существуют они или нет. А даже если знать, что интерфейс существует, знать его параметры и их типы, это ничего не говорит о его применении. Аналог руководства пользователя для объекта – информация о типах. Информация о типах содержит необходимые сведения об объектах и их интерфейсах: ¨
Информация о типах может описывать также пользовательские типы, такие как С-образные структуры данных, объединения (=unions), перечислители (=enumerations), а также неинтерфейсные функции, экспортируемые из модуля DLL. Фактически, все, что можно запомнить в заголовочных файлах (H), библиотеках импорта (LIB-файлы для связи с экспортируемыми функциями DLL), индексах к файлам помощи (HLP) можно запомнить и найти через информацию о типах. НО, как и руководство пользователя радиоприемника не объясняет владельцу, почему он должен захотеть увеличить громкость, так и информация о типах не говорит клиенту, почему он должен захотеть вызывать функции-члены интерфейса. И для радиоприемника, и для объекта цель использования этой информации находится вовне, обычно в голове некоторого человека. При современном развитии компьютеров только часть этой цели может быть включена в запущенный клиентский код. Более того, информация о типах наиболее полезна, когда человек может просматривать ее и говорить некоему клиенту, когда какой объект и какую функцию использовать. Увы, для компьютера это пока проблема искусственного интеллекта, а не данной лекции. С информацией о типах можно сделать много полезного, если ее включить в интерактивные средства разработки: разработчик может просматривать доступные объекты, их интерфейсы и функции-члены, т.е. заниматься визуальным программированием, а не кодированием. Кроме просмотра пользователем, информация о типах может использоваться компилятором (по аналогии с заголовочными файлами и библиотекой импорта). Так нужна ли объекту информация о типах? Будет или нет объект или компонента требовать информацию о типах зависит от того, как предполагает клиент использовать эту информацию. В случаях объектов, поддерживающих технологии Automation или ActiveX Controls, клиент будет рассчитывать на доступность информации о типах. Тем не менее, возможны и другие случаи использования информации о типах. Необходимо предвидеть будущее компонент, так как информацию о типах для компонент сейчас не очень трудно обеспечить, зато потом эти компоненты смогут достигнуть более высокого уровня интеграции с существующими средствами. Поэтому, реальный выбор за разработчиками: минимальные усилия к разработке сегодня (т.е. не обеспечивать информацию о типах) или совместимость в будущем. Библиотека типов Для того чтобы предоставить информацию о типе клиентам разработчик компоненты может создать и распространять библиотеку типа (=type library). Библиотека типа может быть создана по описанию объекта на языке IDL компилятором MIDL. ¨ Пример секции library в файле на IDL: [ uuid(3C591B22-1F13-101B-B826-00DD01103DE1), // IID_ISome object ] interface ISome : IUnknown { HRESULT DoSomething(void); } [ uuid(3C591B20-1F13-101B-B826-00DD01103DE1), // LIBID_Lines version(1.0) ] library Lines { importlib("stdole.tlb"); [ uuid(3C591B21-1F13-101B-B826-00DD01103DE1), // CLSID_Lines helpstring("Lines Class"), ] coclass Lines { [default] interface ISome; interface IDispatch; } } Итак, как видно из примера, библиотека типа имеет свой UUID, называемый LIBID. Дело в том, что СОМ рассматривает библиотеку типа тоже как объект (к этому мы еще вернемся). Кроме того, библиотека, в отличие от описанных в ней интерфейсов, может иметь собственный номер версии. За ключевым словом library следует имя библиотеки. По этому имени компилятор MIDL сгенерирует файл Lines.tlb . Оператор importlib заставляет компилятор добавлять в библиотеку ряд стандартных описаний IDL. Далее следует собственно описание объекта и перечисление его интерфейсов. Естественно, объекту присваивается GUID, который является его CLSID. Оператор coclass определяет собственно компоненту. Теперь не нужно запрашивать функцию QueryInterface для всех известных интерфейсов, чтобы узнать, какие интерфейсы поддерживает объект. Для избежания этого существует библиотека типа, и предложение coclass служит для перечисления поддерживаемых объектом интерфейсов. Сам объект идентифицируется своим CLSID, указанным в атрибуте uuid. Клиент, знающий CLSID, может просмотреть информацию о типах, легко определяя входные и выходные интерфейсы. Причем, экземпляры объекта могут создаваться, а могут и не создаваться. Без информации о типах единственным способом узнать о входных интерфейсах является QueryInterface, а единственным способом узнать о выходных интерфейсах - IConnectionPointContainer::EnumConnectionPoints. Каждый интерфейс, перечисленный в coclass, может иметь дополнительный атрибут: default. Атрибут default может быть использован для одного входного интерфейса. Default incoming interface – главный интерфейс объекта. Если интерфейс является диспинтерфейсом, клиент может получить его указатель с помощью QueryInterface(IID_IDispatch). Все другие диспинтерфейсы должны быть запрошены через их IIDs. Как создать библиотеку типов? Современные продукты создают библиотеки типов при разработке компонент автоматически, изначально же существовали только 2 пути:
Первый путь интересен исключительно разработчикам средств создания ActiveX. Здесь используется интерфейс ICreateTypeLib и ICreateTypeInf. Информация о типах может распространяться с объектом различными способами:
Распространение библиотеки типов. Созданная в результате всего библиотека типов – это двоичный файл, содержащий структуры данных типов. Когда Вы распространяете компоненту, которая зависит от информации о типах, Вы должны также поставлять и библиотеку типов. Можно:
Независимо от способа распространения библиотеки типов нужно связать библиотеку типов с CLSID каждой компоненты, ее использующей. Тогда клиенты данного CLSID смогут найти эту информацию о типах. ¨ Предположим, существует вход в реестре \ CLSID {42754580-16b7-11ce-80eb-00aa003d7352} = Acme Component 3.0 ProgID = Acme.Component.3 VersionIndependentProgID = Acme.Component Чтобы связать библиотеку типов с этим входом, нужно добавить следующий подключ (на том же уровне, что и ProgID): TypeLib = {<LIBID>} , где {<LIBID>} – уникальный атрибут для самой библиотеки. Например, если библиотеке назначен GUID a4f8a400-16b7-11ce-80eb-00aa003d7352, то вход будет показан как TypeLib = {a4f8a400-16b7-11ce-80eb-00aa003d7352} Этот GUID относится к множеству входов, которые Вы должны запомнить под секцией TypeLib реестра, которая находится на том же уровне, что и секция CLSID. \ TypeLib {<LIBID>} = <имя библиотеки типов> DIR = <путь на файл библиотеки типов; не имя файла!> HELPDIR = <путь на файлы помощи; не имя файла!> <version> <LangID> [Win16 | Win32] = <filename> <LangID> § <version> § §
Итак, сперва Вы создаете ключ с LIBID и неким читабельным именем для библиотеки. Под этим ключом запоминается информация о директории и ключи для каждой версии библиотеки типов. Это показывает, что различные версии библиотеки типов могут разделять один и тот же LIBID. Под каждым входом версии Вы создаете подключ, равный language ID, который по запросу идентифицирует национальный язык библиотеки. Затем Вы создаете другой подключ, идентифицирующий “битность” (16 или 32) библиотеки. Величина этого ключа – это любо имя TLB, EXE или DLL файлов (в которых библиотека – ресурс), либо имя составного файла, содержащего библиотеку в потоке. Загрузка и использование библиотеки типов Хотя библиотека типов – это просто файл, доступ к нему осуществляется через стандартное программное обеспечение. СОМ рассматривает библиотеку типов как объект, доступ к которому осуществляется через стандартные интерфейсы. Но для того, чтобы использовать библиотеку как объект, ее надо загрузить. Для этого используется одна из функций:
Загрузка библиотеки типов означает загрузку объекта библиотеки типов и получение первого указателя на него (указатель на интерфейс ITypeLib). Через этот интерфейс можно найти атрибуты библиотеки или получить доступ к конкретным элементам библиотеки. В то время как библиотека как целое управляется ITypeLib, каждый элемент как отдельный объект управляется через ITypeInfo. Через ITypeInfo можно найти все, что можно создать через ICreateTypeInfo. Простое представление отношения между библиотекой, несколькими элементами и их интерфейсами показано на рисунке.
Библиотека типов имеет интерфейс ITypeLib, через который можно просматривать интерфейсы ITypeInfo, представляющие различные части библиотеки. Практическим использованием информации о типах за пределами запущенных объектов являются “браузеры информации о типах”. Браузеры типовой информации могут просматривать содержимое библиотек типов и показывать доступные объекты, интерфейсы, методы и свойства конечному пользователю. Эти браузеры могут быть базисом для мощных оболочек, которые могут использовать “drop-down list boxes” и “drag and drop” в программировании пользовательских интерфейсов, существенно уменьшая число нажатий клавиш, которые должен выполнить программист.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
| Copyright ©: 2001. Все права защищены, По всем вопросам обращайтесь Лозовюк Александр или Alex Strong |