Загрузка...
 
Печать

Технология COM (Component Object Model)




Intro

По данной теме написана не одна сотня книг (многие из которых на русском языке). А так как данная статья не претендует на полноту изложения, настоятельно рекомендуем прочесть хотя бы пару из них. Не факт, что всё усвоишь, но основные моменты всё равно запомнишь.
Приложение, созданное, например, на языке C++, постоянно производит вызовы функций стандартной библиотеки Visual C++ а также функций операционной системы.1 Сервисы, предоставляемые средой выполнения (в нашем случае ОС Windows) многочисленны и разнообразны. Вот далеко неполный список:
  • библиотечные функции;
  • функции программного интерфейса ОС (API);
  • средства межпроцессорной связи (например DDE);
  • компоненты, разработанные для конкретной ОС (например VBX).
COM (или Component Object Model) - это стандарт программирования, разработанный компанией Microsoft.2
  • Описывает правила создания программных компонентов, при использовании которых нет необходимости учитывать, в какой среде программирования создавался тот или иной модуль.
Т.е. неважно, что именно использовал программист - Visual C++ или Visual Basic, если результирующий двоичный модуль будет полностью соответствовать стандартам, требуемым COM.
  • Описывает архитектуру предоставления программных сервисов, скрывая реализацию этих сервисов. Это позволяет создавать более сложные программные комплексы за существенно более короткие сроки.
  • На COM основана технология создания элементов управления ActiveX.3
  • Управление другими программами через автоматизацию OLE.
  • Определяет единый стандартный способ, при помощи которого одна часть ПО может воспользоваться "услугами" другой части ПО при условии, что эти части соответствуют принятым соглашениям, которые описывает COM.
  • Определяет бинарный (двоичный) стандарт для построения компонентов приложений, а также принципов их взимоотношений друг с другом.
  • На основе COM построена библиотека DirectX.
  • Расширением спецификации COM является технология DCOM (Distributed COM - распределённая COM). Она позволяет взаимодействовать компонентам, расположенным на разных компьютерах в сети.
  • Использование компонентов COM можно рассматривать как объектно-ориентированное, но выполняющееся на уровне операционной системы. Для СОМ также справедливы такие принципы ООП, как инкапсуляция (сокрытие реального кода), полиморфизм (предоставление двумя разными объектами одинакового набора интерфейсов) и наследование.
  • Имеет расширение DCOM (Distributed COM), описывающее взаимодействие компонентов, расположенных на разных компьютерах в сети.
Фактически COM описывает архитектуру предоставления программных сервисов, скрывая реализацию этих сервисов. Это позволяет создавать более сложные программные комплексы за более короткие сроки. Способ взаимодействия, который определяет COM, снабжает программиста прочным фундаментом для создания легко расширяемого, модульного, переносимого программного обеспечения.
Использование COM-компонентов можно рассматривать как объектно-ориентированное, но выполняющееся на уровне операционной системы (ОС). Для COM также справедливы такие принципы ООП, как
  • инкапсуляция (сокрытие реального кода),
  • полиморфизм (ничто не мешает двум разным объектам пердоставлять одинаковый набор интерфейсов),
  • наследование (в какой-то степени).
Вообще, наследование, в привычном для объектно-ориентированных языков понимании (наследование реализации), в COM не поддерживается. Но поддерживается наследование интерфейсов, которое позволяет наследовать интерфейс базового класса. Чаще всего это происходит через агрегирование (aggregation), то есть, когда один компонент способен наследовать интерфейс другого компонента таким образом, что последний интерфейс может рассматриваться, как относящийся к первому компоненту. Это понятие часто встречается при работе с компонентами DirectX и означает возможность использования интерфейсов создаваемого компонента как своих собственных.
Компонентами COM-парадигмы являются бинарные (=двоичные) объекты, которые могут взаимодействовать друг с другом, используя различные интерфейсы, содержащиеся в них. Объекты могут быть легко заменены на более новые или исправленные версии, причём без внесения изменений в ранее созданные на их базе приложения.

История возникновения COM

До появления COM существовало множество разрозненных сервисов, выполняемых в среде MS Windows. Каждый из них выполнял или выполняет лишь свои узкие (или не очень) задачи. Чаще всего эти сервисы были совершенно несовместимы друг с другом и требовали от программиста знания сразу нескольких технологий (например Windows API, средства межпроцессорной связи, компоненты, разработанные для конкретной среды программирования).
Например, при статической компоновке с исполняемым модулем, запуске приложением другого приложения или вызовом программным модулем функций операционной системы, - везде используются принципиально разные механизмы доступа составных частей ПО друг к другу. Таким образом, из разрозненных частей, таких как Windows API, DDE, VBX и др., практически невозможно построить единую, устойчиво работающую систему, обеспечивающую реальный рефакторинг (повторное использование исходного кода).
С приходом COM всё изменилось. COM представляет собой универсальный способ взаимодействия программных модулей. В этой спецификации, разработанной компанией Microsoft, описаны требования, следуя которым становится возможным создание компонентов, предоставляющие свои сервисы единообразно. Причём вне зависимости от языка программирования или используемой ИСР (интегрированной среды разработки, IDE).
Модель COM определяет бинарный (двоичный) стандарт для построения компонентов приложений, а также принципов их взаимодействия друг с другом. Физическим воплощением является скомпилированный (в любой среде программирования!) двоичный модуль, полностью соответствующий требуемым стандартам.
Способ взаимодействия, который определяет COM, даёт программисту прочный фундамент для создания легко расширяемого, моудльного, переносимого программного обеспечения.
Многие службы ОС Windows основаны на использовании COM. Например, к Windows Explorer можно получить доступ через COM.

Основные понятия COM


Image
Рис. 1. Взаимодействие клиентского приложения с сервером COM


ОБЪЕКТ (или КОМПОНЕНТ) - закрытая сущность, скрывающая в себе нужную функциональность. Компонент может логически представлять собой реально существующее устройство (например, звуковая карта), либо быть некоторым абстрактным понятием.
Напрямую обратиться к объекту нельзя. Связь с ним возможна лишь при помощи интерфейсов, указатели на которые и может получить конкретная программа. Команды, которые программа может отправлять компоненту COM через интерфейс, называются МЕТОДАМИ. Каждый интерфейс содержит фиксированное число методов. Зная имя интерфейса соответствующего компонента, программа (или другой компонент) может получить указатель на этот интерфейс и, пользуясь им для вызова конкретного метода, заставить компонент выполнить действия, определённые этим методом (см Рис. 1).
В данном случае:
  • клиент - это программа, использующая возможности компонента;
  • сервер - это программный модуль, содержащий компонент COM.
Клиенту необходимо каким-то образом инициализировать запуск сервера, содержащего необходимы компонент COM (это может быть динамическая библиотека DLL или исполняемый файл EXE). После этого необходимо получить указатель на нужный интерфейс. Используя этот указатель, можно затем вызывать методы полученного интерфейса. Различают 3 типа серверов, с использованием которых реализуются COM-компоненты:
ТИП СЕРВЕРА ОПИСАНИЕ
Внутрипроцессный сервер (in-process server) Представляет собой библиотеку DLL, проецируемую на адресное пространство клиентского процесса, то есть использующую его адресное пространство.
Внепроцессный сервер (out-process server) Представляет собой исполняемый файл EXE, выполняющийся в собственном адресном пространстве, однако на том же компьютере, что и клиент.
Удалённый сервер (remote server) Выполняется на удалённом компьютере и доступен благодаря DCOM.

После того, как интерфейсы COM были созданы и опубликованы, они не должны изменяться. Требование неизменности опубликованных интерфейсов является обоснованным и защищает существующее ПО от возможных проблем: если производителю нужно добавить какую-то новую функциональность к уже существующему объекту, он просто создаст новый интерфейс, не затрагивая старые. Этот принцип также обеспечивает обратную совместимость с ранее созданными приложениями. Так например, игра, созданная под DirectX версии 6 или 7, будет работать и под управлением более поздних версий этой библиотеки (8 или 9 версии). А всё из-за того, что каждая новая версия DirectX полностью сохраняет функциональность своих предыдущих версий.
Как результат, вновь созданные программы смогут воспользоваться новейшими возможностями компонента, а старые программы сохранят свою работоспособность - ведь старый интерфейс остался без изменений и всё ещё работает. В отдельных случаях такой подход даже позволяет изменять внутреннюю реализацию старого интерфейса (хотя это делать не рекомендуется). Программа "не почувствует" изменения, так как в ответ на вызовы методов будут выполняться всё те же ранее оговорённые действия.

COM и DirectX

Компоненты DirectX, появившиеся в первых версиях этой бибилотеки, как правило, пытались скрыть свою COM-сущность (например, путём внедрения специальных функций, которые брали на себя всю работу по инициализации среды и созданию нужного интерфейса). Появившийся позднее DirectMusic и некоторые другие компоненты не имеют подобных функций, и для работы с ними требуется понимание основ используемой модели COM вцелом.
Все компоненты DirectX реализованы как внутрипроцессные серверы и физически содержатся в DLL-файле. Такой подход позволяет обеспечить самую высокую производительность. А всё из-за того, что при работе с интерфейсом отсутствуют посредники, выполняющие, например, преобразование адресов. Библиотеки DLL, содержащие компоненты DirectX, подключаются напрямую к адресному пространству процесса (выполняемого приложения).
В документации DirectX SDK прототипы методов для всех интерфейсов приведены в виде объявления методов классов. То есть указатель на экземпляр интерфейса для методов не указывается. В языках, отличных от C++, необходимо явно передавать методу указатель на его интерфейс.

Физическая реализация интерфейсов COM


Image
Рис.2 Двоичная структура интерфейса COM аналогична структуре обычного абстрактного класса C++


COM - это двоичный стандарт, обеспечивающий построение компонентов. Физически двоичная структура интерфейса COM аналогична двоичной структуре, которую компиляторы C++ генерируют для абстрактного класса C++.
Абстрактный класс (в C++) - класс, содержащий только виртуальные функции-методы (методы, для которых отсутствует реализация). Причиной создания таких методов является возможность вызывать в базовом классе методы, которые будут определены лишь в производных классах. Невозможно создать экземпляр абстрактного класса. Каждый класс, производный от абстрактного класса, должен определить свою реализацию чисто виртуальных методов. Лишь после этого появляется возможность создания экземпляров производного класса. Чисто виртуальные методы, которые производный класс переопределяет, для него становятся просто виртуальными методами.
Для каждого класса, содержащего виртуальные методы, компилятор создаёт специальную таблицу, в которой содержатся указатели на конкретные реализации виртуальных методов. Если класс переопределяет виртуальный метод базового класса, то в таблицу заносится указатель на переопределённый метод. Физически, для класса, содержащего виртуальные методы, указатель на таблицу виртуальных методов (обычно называемую vtable) хранится в первых четырёх байтах конкретного экземпляра класса.
Структура класса, содержащего виртуальные методы, приведена на Рис. 2.
Библиотека COM содержит функции, которые выполняют наиболее распространённые и общие действия по созданию компонентов и управлению интерфейсами. Пример - запуск сервера клиентом. Обрати внимание на то, что это именно функции, а не методы, принадлежащие какому-либо интерфейсу. Большинство функций библиотеки COM выполняют специфичные действия и применяются лишь в очень редких случаях. Такие функции, как правило, всегда имеют в названии префикс Co, что указывает на их принадлежность к библиотеке COM.

Инициализация COM. Функции CoInitialize и CoInitializeEx

Прежде чем начать использовать COM-объекты, необходимо сперва инициализировать саму систему COM. Интересно, что для этого применяются две разные функции, в зависимости от присутствия/отсутствия в приложении многопоточности. Для инициализации COM в однопоточном приложении вызывается функция CoInitialize:
Прототип функции CoInitialize
HRESULT CoInitialize( LPVOID pvReserved);

Для инициализации COM в многопоточном приложении вызывается функция CoInitializeEx:
Прототип функции CoInitializeEx
HRESULT CoInitializeEx (
	void *pvReserved, // NULL
	DWORD dwCoInit // Модель взаимодействия (concurrency model)
	);

При использовании функции CoInitializeEx в многопоточном приложении, во втором параметре необходимо указать т.н. режим (модель) взаимодействия для корректной работы системы COM. Параметр dwCoInit может принимать следующие значения:
ЗНАЧЕНИЕ ОПИСАНИЕ
COINIT_APARTMENTTHREADED При инициализации не создаётся нового клиентского подразделения. Все действия происходят в клиентском подразделении STA. Аналогично вызову CoInitialize.
COINIT_MULTITHREADED При инициализации создаётся новое клиентское подразделение MTA. Инициализирует поток для работы в многопоточном режиме.
COINIT_DISABLE_OLE1DDE Отключает DDE для поддержки OLE1.
COINIT_SPEED_OVER_MEMORY При создании потока COM увеличивает объём используемой памяти для улучшения быстродействия.

Самый нормальный флаг (значение параметра dwCoInit) здесь - COINIT_MULTITHREADED.4
Закрыть
noteКлиентское подразделение в COM

- граница, разделяющая синхронные и асинхронные объекты; это воображаемая линия, нарисованная вокруг объектов и потоков их клиентов, которая разделяет COM-объекты с разными потоковыми характеристиками. Главная идея подразделений состоит в том, чтобы позволить COM выстроить в очередь вызовы методов для тех объектов, которые не являются потокобезопасными (thread-safe). Если вы не укажете COM, что объект является потокобезопасным, COM не позволит более чем одному вызову за раз достичь этот объект. Если же пометить объект как потокобезопасный, COM с легкостью позволит объекту обслуживать параллельные вызовы методов из разных потоков. Более подробно об этом читаем здесь: http://rsdn.ru/article/com/apartmnt.xml(external link)

По окончании работы с COM во всех случаях вызывается функция CoUninitialize, не требующая указания каких-либо параметров:
void CoUninitialize();

Данная функция вызывается столько раз, сколько было создано систем COM. Например, при инициализации системы COM дважды, необходимо дважды вызвать завершающую функцию CoUninitialize:
// Инициализируем систему COM
CoInitialize(NULL);

// Инициализируем систему COM в многопоточном режиме
CoInitializeEx(NULL, COINIT_MULTITHREADED);

// Освобождаем COM (дважды)
CoUninitialize();
CoUninitialize();

Для большинства компонентов DirectX вызовы этих функций являются избыточными, так как их инициализация происходит внутри специальных "создающих" функций (например DirectSoundCreate). В то же время, во многих примерах DirectX SDK такая инициализация всё равно производится. Выполнять инициализацию библиотеки COM или нет, остаётся на усмотрение программиста.

Создание неинициализированного компонента. Функция CoCreatelnstance

После успешной инициализации библиотеки COM, функция CoCreateInstance создаёт неинициализированный компонент и получает интерфейс для работы с ним:
Прототип функции CoCreateInstance
STDAPI CoCreateInstance(
	REFCLSID	rclsid,	// GUID компонента
	LPUNKNOWN	pUnkOuter,	// Необходим для агрегирования
	DWORD dwClsContext,	// Контекст запуска
	REFIID riid,	// GUID интерфейса
	LPVOID *	ppv	// Адрес указателя на интерфейс
	);

Вот описание её параметров:
ПАРАМЕТР ОПИСАНИЕ
REFCLSID rclsid CLSID, ассоциированный с данными и выполняемым кодом, которые будут использоваться для создания объекта. Обычно содержит GUID создаваемого компонента.
LPUNKNOWN pUnkOuter Значение NULL указывает на то, что объект создаётся без агрегирования. В остальных случаях здесь стоит указатель на другой компонент (обычно с интерфейсом IUnknown), в который будет агрегирован вновь создаваемый компонент.
DWORD dwClsContext Флаг (или комбинация флагов), указывающий в каком контексте (каким способом) будет запускаться код создаваемого объекта. Возможные значения указаны в Таблице 1.
REFIID riid Ссылка на идентификатор интерфейса, который будет использоваться для коммуникации с объектом. Должен содержать GUID необходимого интерфейса.
LPVOID * ppv Адрес указателя на запрашиваемый интерфейс, который впоследствии и будет использоваться.

При успешном завершении функция возвращает значение S_OK.
Таблица 1. Возможные значения параметра DWORD dwClsContext функции CoCreateInstance
ЗНАЧЕНИЕ ОПИСАНИЕ
CLSCTX_INPROC_SERVER Код, который создаёт объекты данного класса и управляет ими, представляет собой DLL-библиотеку (внутрипроцессный сервер), которая выполняется в том же процессе, что и вызывающая функция, описывающая содержимое класса. Обычно применяется при использовании компонентов DirectX, так как все компоненты этой библиотеки реализованы как внутрипроцессные серверы. Поскольку все компоненты DirectX реализованы как внутрипроцессные серверы, именно этот флаг почти всегда указывается при создании игровых приложений.
CLSCTX_INPROC_HANDLER Код, который создаёт объекты данного класса и управляет ими, представляет собой внутрипроцессный хэндлер ( дескриптора1 потока в ОС). По сути это та же DLL-библиотека, которая выполняется в клиентском процессе и имплементирует структуры данного класса, выполняющиеся на стороне клиента, в то время как доступ к экземплярам этого класса осуществляется удалённо.
CLSCTX_LOCAL_SERVER Код исполняемого EXE-файла, который создаёт объекты данного класса и управляет ими, выполняется на одном и том же компьютере, но загружаются они при этом в разных процессных пространствах (процессах).
CLSCTX_INPROC_SERVER16 Устаревший, вышедший из употребления флаг.
CLSCTX_INPROC_HANDLER16 Устаревший, вышедший из употребления флаг.
CLSCTX_REMOTE_SERVER Удалённый контекст. Код LocalServer32 или LocalService, который создаёт объекты данного класса и управляет ими, запускается на другом компьютере.
CLSCTX_RESERVED1 Зарезервирован.
CLSCTX_RESERVED2 Зарезервирован.
CLSCTX_RESERVED3 Зарезервирован.
CLSCTX_RESERVED4 Зарезервирован.
CLSCTX_RESERVED5 Зарезервирован.
CLSCTX_ENABLE_CODE_DOWNLOAD Разрешает загрузку программного кода из directory service или Интернета. Не может одновременно использоваться с флагом SCTX_NO_CODE_DOWNLOAD.
SCTX_NO_CODE_DOWNLOAD Отключает загрузку программного кода из directory service или Интернета. Не может одновременно использоваться с флагом CLSCTX_ENABLE_CODE_DOWNLOAD.
CLSCTX_NO_CUSTOM_MARSHAL Запрещает использование стороннего маршалинга(external link).
CLSCTX_NO_FAILURE_LOG Отключает протоколирование ошибок во время выполнения функции CoCreateInstanceEx.
CLSCTX_DISABLE_AAA MS Windows XP и более позднии версии. Отключает использование AAA-активации (activate-as-activator - англ "активировать в качестве активатора") для данной активации. При AAA-активации любая активация на стороне сервера будет производиться от имени вызывающей функции. Отключение AAA-активации позволяет приложению, которое запущено под привелегированным аккаунтом (например LocalSystem), предотвратить использование его идентификационных данных для запуска непроверенных (недоверенных) компонентов. Библиотечные приложения, которые используют вызовы активации, должны всегда использовать данный флаг всякий раз при осуществлении вызова. Это помогает предотвратить использование библиотечных приложений для осуществления eop (escalation-of-privilege - англ. "повышение привелегий") атак. Не может одновременно использоваться с флагом CLSCTX_ENABLE_AAA.
CLSCTX_ENABLE_AAA MS Windows XP и более позднии версии. Включает использование AAA-активации (activate-as-activator - англ "активировать в качестве активатора") для данной активации. При AAA-активации любая активация на стороне сервера будет производиться от имени вызывающей функции. Включение данного флага позволяет приложению передавать свои идентификационные данные активированному компоненту. Переопределяет действие флага CLSCTX_DISABLE_AAA.
SCTX_FROM_DEFAULT_CONTEXT Начинает данную активацию с контекста по умолчанию в данном потоке.
CLSCTX_ACTIVATE_32_BIT_SERVER Активирует или подключается к 32-битной версии сервера. Если сервер не зарегистрирован, функция завершит работу, выдав ошибку.
CLSCTX_ACTIVATE_64_BIT_SERVER Активирует или подключается к 64-битной версии сервера. Если сервер не зарегистрирован, функция завершит работу, выдав ошибку.
CLSCTX_ENABLE_CLOAKING MS Windows Vista и более поздние версии. При указании данного флага, для активации запроса, созданного потоком, COM использует impersonationn token (от англ. "маркер олицетворения(external link)") потока, если таковой имеется. Когда данный флаг не указан, или поток не имеет маркера олицетворения, для активации запроса, созданного потоком, COM использует маркер процесса данного потока.
CLSCTX_APPCONTAINER Указывает на то, что активация производится для контейнера приложения. Данный флаг зарезервирован для служебного пользования и не предназначен для использования при создании приложений.
CLSCTX_ACTIVATE_AAA_AS_IU Указывает на активацию в качестве интерактивного пользователя (Interactive User activation) для серверов типа As-Activator. Например, приложение Windows Store app (где все имена процессов строго заданы и которое является приложением среднего уровня интегрированности (Medium IL app)) может использовать данный флаг для запуска COM-сервера типа As-Activator без строго заданного имени. Также данный флаг можно применять для привязки процесса к COM-серверу, запущенному ранее настольным Windows-приложением. Клиент при этом должен являться Medium IL app (приложение со средним уровнем интегрированности), должен иметь строго заданное имя (то есть поле SysAppID в его пользовательском маркере должно быть заполнено), не должен иметь нулевую сессию (session 0), и он должен быть запущен от того же имени пользователя, что указано в поле session ID его пользовательского маркера (client token). Если сервер имеет тип отличный от As-Activator или не функционирует, клиент запускает сервер с маркером сессии пользователя, взятого из пользовательского маркера. Этот маркер не будет иметь строго заданного имени. Флаг не работает для внутрипроцессных (in-process) серверов.
CLSCTX_PS_DLL Используется для загрузки Proxy/Stub DLL (DLL с серверной заглушкой). Данный флаг зарезервирован для служебного пользования и не предназначен для использования при создании приложений.


Интерфейс IUnknown

  • Является базовым интерфейсом (вообще, это класс), который наследуют любые другие интерфейсы COM.
  • Определяется в файле unknwn.h.
  • Методы, определённые для IUnknown (всего из 3), должны иметься в наличии в каждом интерфейсе.
  • Основное требование COM: 3 базовых метода, определённых для IUnknown (QueryInterface, AddRef, Release), должны присутствовать в виде первых трёх методов любого интерфейса.

Опрос интерфейсов. Метод QueryInterface

  • Применяется для обнаружения интерфейсов в COM-объектах. Включаяя их более новые версии.
  • Осуществляет контроль версий.
Именно эта фича даёт DirectX обратную совместимость. Т.е. игра, созданная под DirectX7 без труда пойдёт на компе с DirectX9 и более поздними версиями.
Там принцип примерно следующий. Ты всё ещё можешь применять старый интерфейс (здесь - версию DirectX), но для того, чтобы получить доступ к более новому интерфейсу, просто запроси его. Если новый интерфейс присутствует, то в этом случае передаётся указатель на его объект. В остальных случаях возвращается NULL (для информирования об отсутствии интерфейса или об ошибке).
  • Позволяет, имея один интерфейс компонента, запросить другой поддерживаемый интерфейс:
Прототип метода QueryInterface
HRESULT IUnknown::QueryInterface(
	REFIID iid,	// GUID (=ссылочный идентификатор, reference identifier) запрашиваемого интерфейса
	void ** ppvObject	// Адрес указателя на запрашиваемый интерфейс
	);

В случае успешного завершения работы метода, по адресу void ** ppvObject будет записан реальный указатель на запрашиваемый интерфейс. При этом метод возвратит S_OK. Иначе - возвращается E_NOINTERFACE.
Данный метод очень важен, так как осуществляет контроль версий (ведь напрямую COM контроль версий не поддерживает). В следующем примере мы обнаруживаем (obtain) интерфейс IAdd и затем опрашиваем интерфейс IAdd2:
IAdd *Add;
IAdd2 *pAdd2;

// Сперва получаем интерфейс IAdd
if(FAILED(CoCreateInstance(CLSID_MATH, NULL, CLSCTK_INPROC, IID_IAdd, (void**)&pAdd)))
{
	//Обрабатываем ошибку
}

// Опрашиваем (query) на предмет наличия интерфейса IAdd2
if(SUCCEEDED(pAdd->QueryInterface(IID_IAdd2, (void**)&pAdd2)))
{
	// Интерфейс обнаружен. Освободим интерфейс
	IAdd IAdd->Release();
}


Метод AddRef

  • Увеличивает на 1 значение счётчика ссылок на данный интерфейс:
Прототип метода AddRef
ULONG AddRef(void);


Метод Release
  • Уменьшает на 1 значение счётчика ссылок на данный интерфейс:
Прототип метода Release
ULONG Release(void);


Доп. инфо по методам QueryInterface, AddRef и Release

Методы AddRef и Release возвращают текущее значение счётчика ссылок. Программер должен следить, чтобы кол-во ссылок на данный COM-объект совпадало с числом функций Release, освобождающих ресурсы от более неиспользуемых COM-объектов ^COM-интерфейсов).
Все 3 перечисленных выше метода позволяют управлять временем существования компонента. Таким образом COM возлагает всю ответственность по удалению компонента на сам компонент. Каждый компонент COM должен поддерживать счётчик количества ссылок на себя со стороны клиентов. Если счётчик становится равен нулю, компонент COM должен себя удалить. После этого указатели на его интерфейсы становятся недействительными. Такой подход позволяет избежать ситуации, когда один из клиентов попытается удалить компонент, а второй будет продолжать с ним работать.
При создании компонента его счётчик количества ссылок принимает значение 1. Любая функция, возвращающая указатель на интерфейс, должна увеличивать значение этого счётчика вызовом AddRef. Например, именно так поступает метод QueryInterface. Поэтому для интерфейса, получаемого через QueryInterface, метод AddRef вызывать не нужно. Необходимость этого возникает лишь в случае, когда происходит копирование указателя на интерфейс в другую переменную, то есть, когда компонент не имеет возможности отследить увеличение числа ссылок.
Вообще, метод AddRef в программах, лишь использующих компоненты COM, встречается довольно редко, а иногда и вовсе отсутствует. Вызов же метода Release встречается всегда. Общее правило гласит: как только интерфейс перестаёт быть нужным, для него необходимо вызвать метод Release. Впрочем, оно соблюдается далеко не всегда.
Закрыть
noteОбрати внимание

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


Создание собственных COM-функций

Для построения своих собственных функций, необходимо сперва создать дочерний класс (объект) от объекта IUnknown и уже в него вносить дополнительные функции. Напомним, что согласно стандарту Microsoft, COM-объект не может экспонировать свои переменные. Экспонируются только функции (в рамках данного интерфеса).
Функции COM возвращают значение типа HRESULT, в котором содержится информация об успешном выполнении, либо код ошибки в случае неудачи.
Для запроса значения (любого типа) у COM-объекта необходимо передать указатель запрашиваемой переменной (он должен иметь тип WORD или DOUBLE WORD - байтовые значения здесь запрещены) в соответствующую функцию. В следующем примере мы создаём простой объект (ветвящийся от класса IUnknown), который принимает два числа, складывает (суммирует) их и возвращает результат в третью, заранее заготовленную переменную:
class IMyComObject : public IUnknown
{
public:
	HRESULT Add(long *Num1, long *Num2, long *Result);
	HRESULT IMyComObject::Add(long *Num1, long *Num2, long *Result)
	{
		// Суммирует 2 числа и сохраняет результат
		*Result = *Num1 + *Num2;
		
		// Возвращает код успешного завершения
		return S_OK;
	}

Закрыть
noteОбрати внимание

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


Идентификация компонентов и интерфейсов COM

Поскольку компоненты хранятся в DLL или EXE файлах, должен существовать способ, позволяющий операционной системе узнать, в каком конкретно модуле хранится требуемый компонент. Однако возлагать ответственность за правильную идентификацию модуля на клиента неразумно. Не исключено, что поставщик компонента захочет изменить имя файла, в котором содержится компонент, в результате чего все старые клиенты не смогут правильно выполняться. COM предлагает совершенно другой подход. Для каждого компонента клиент должен знать уникальный номер, который однозначно и полностью идентифицирует этот компонент. Для этих целей используется GUID (Globally Unique IDentifier -Глобально уникальный идентификатор), который представляет собой специальную величину длиной 16 байт (в документации Microsoft чаще можно встретить значение 128 бит, что, в принципе, одно и то же), обеспечивающую необходимую уникальность. Гарантируется, что GUID, сгенерированный на конкретном компьютере в определённое время, не имеет себе аналогов во всём мире. Для этих целей в GUID помещаются сведения о времени его генерации и уникальных особенностях компьютера, на котором он был сгенерирован (чаще всего это MAC-адрес сетевого адаптера). В каталоге с установленной MS Visual C++ 6.0 расположены две утилиты для генерации GUID:
УТИЛИТА ОПИСАНИЕ
GUIDGEN.EXE Генерирует GUID в одном из необходимых форматов и позволяет скопировать его в буфер обмена.
UUIDGEN.EXE Генерирует GUID в режиме командной стройки.

В бесплатных версиях последних выпусков MSVC++ (2008, 2010, 2012 и др.) утилита GUIDGEN расположена в каталоге "Microsoft SDKs" (в нашем случае полный путь выглядит так: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A \bin\guidgen.exe). В любом случае её нетрудно найти в Интернете. Библиотека Microsoft SDK устанавливается вместе с MSVC++ и содержит массу интересных утилит и вспомогательных компонентов. Пример GUID: 597507A3-8CB8-11d4-B079-94B45BEE2A64
GUID конкретного устройства хранится в реестре Windows в разделе HKEY_CLASSES_ROOT\CLSID. Внутри него создаются разделы, получающие имя в виде соответствующего GUID. Для каждого из таких разделов имеются подразделы, описывающие особенности компонента. Например, подраздел InprocServer32 описывает расположение DLL, содержащей компонент, и поддерживаемые им модели многопоточности.
Кроме идентификатора GUID используемого компонента программе-клиенту необходимо также знать идентификатор запрашиваемого интерфейса (напомним, что один компонент может поддерживать несколько интерфейсов). Для идентификации интерфейсов также используется GUID.
Значения GUID для компонентов и интерфейсов клиент (написанный на C/C++) узнаёт из заголовочных файлов, предназначенных для работы с нужным комопонетом. Согласно общепринятым соглашениям, имена значений GUID комопнентов начинаются с префикса CLSID_, а значения GUID интерфейсов c IID_.
Например, файл dsound.h, предназначенный для работы с DirectSound и расположенный в каталоге с установленным DirectX SDK (путь по умолчанию в нашем случае: C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Include), содержит следующие определения GUID:
// DirectSound Component GUID {47D4D946-62E8-11CF-93BC-444553540000}
DEFINE_GUID(CLSID_DirectSound, 0x47d4d946, 0x62e8, 0x11cf, 0x93, 0xbc, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0); 
// DirectSound 8.0 Component GUID {3901CC3F-84B5-4FA4-BA35-AA8172B8A09B}
DEFINE_GUID(CLSID_DirectSound8, 0x3901cc3f, 0x84b5, 0x4fa4, 0xba, 0x35, 0xaa, 0x81, 0x72, 0xb8, 0xa0, 0x9b);
DEFINE_GUID(IID_IDirectSound, 0x279AFA83, 0x4981, 0x11CE, 0xA5, 0x21, 0x00, 0x20, 0xAF, 0x0B, 0xE5, 0x60);

Макрос DEFINE_GUID позволяет использовать вместо конкретных значений GUID символьные имена, например IID_IDirectSound. Это существенно облегчает чтение исходного кода программы. Этот макрос используется во многих заголовочных файлах DirectX для определения GUID компонентов и интерфейсов. Соответственно, чтобы нужные GUID получили свои значения, необходимо определять макрос INITGUID ДО включения нужных заглловочных файлов DirectX и только внутри одного файла с исходным кодом.
Большей части программ, работающих с компонентами DirectX, потребуется знать лишь GUID нужного интерфейса, так как для всех остальных компонентов предусмотрены функции, получающие в качестве параметра GUID интерфейса и скрывающие внтури себя GUID компонента.
Однако для некоторых компонентов, например DirectMusic, потребуется знание обоих GUID.

Значения, возвращаемые функциями COM


Image
Рис. 3. Структура значений, возвращаемых функциями и методами COM


  • Обычно изучаются для того, чтобы понять каким образом COM сигнализирует об ошибках.
  • Как правило, тип возвращаемого значения COM-функций - HRESULT (32-битное знаковое значение).
В случае успешного завершения работы функции возвращается S_OK. В случае неудачи возвращается E-FAIL. Структура возвращаемого значения указана на Рис. 3. Возвращаемое значение состоит из трёх участков. Младшие 16 бит содержат собственно код возврата и полностью определяются производителем компонента. Следующие 15 бит являются кодом средства, сгенерировавшего возвращаемое значение. Компоненты DirectX определяют собственные коды средства для каждого из них. Например для DirectSound:
#define FACDS 0x878

Последний 31-й бит возвращаемого значения содержит флаг критичности, который указывает, произошла ошибка или нет. Если этот бит равен 0, то ошибки не было, а если он равен 1, то произошла ошибка. Так как 31-й бит является знаковым, то наиболее простой способ определить, произошла ошибка или нет - это сравнить возвращаемое значение с нулём: отрицательное возвращаемое значение всегда означает наличие ошибки.
Такими сравнениями как раз и занимаются макросы, определённые в WINERROR.H:
Фрагмент WINERROR.H
...
#define SUCCEEDED(Status) ((HRESULT)(Status) >>= 0)
#define FAILED(Status) ((HRESULT)(Status) <<= 0)
...

Первый из них проверяет возвращаемое значение на успешность а второй - на неудачу. Microsoft рекомендует проверять возвращаемые значения только с использованием этих макросов.
Вот пример:
if (SUCCEEDED(CoInitialize(NULL)))

Такой подход объясняется тем тем, что кодов успешного завершения функции может быть несколько. И все они будут иметь в 31-м бите значение 0 (то есть их величины окажутся неотрицательными). В примерах DirectX SDK эти макросы используются повсеместно .
Для построения возвращаемого значения из упомянутых частей используется макрос MAKE_HRESULT. Например, так он используется в DirectSound при построении возвращаемого значения:
#define MAKE_DSHRESULT(code) MAKE_HRESULT(1, _FACDS, code)
#define DS_OK	S_OK
#define DSERR_BADFORMAT	MAKE_DSHRESULT(10 0)

Здесь определяется единственный код успешного завершения, а все остальные строятся из расчёта, что они неуспешны (в макрос MAKE_HRESULT передаётся значение 1 для 31-го бита).

Источники


1. Гончаров Д., Салихов Т. DirectX 7.0 для программистов. Учебный курс (+CD). - СПб.: Питер, 2001. - 528 с.: ил.
2. Adams J. Programming Role Playing Games with DirectX, 2nd Ed. - Premier Press, 2004
3. Кальверт Ч. Базы данных в Delphi 4. - DiaSoft-Киев, 1999
4. Adams J. Programming Role Playing Games with DirectX 8.0. - Premier Press, 2002


Последние изменения страницы Понедельник 27 / Июнь, 2022 07:56:54 MSK

Последние комментарии wiki

No records to display

Search Wiki Page

Точное совпадение

Категории

|--> C#
|--> C++