Интерфейсы
Каждый поддерживаемый объектом интерфейс, по сути дела, — контракт между этим объектом и его клиентами. Они обязуются: объект — поддерживать методы интерфейса в точном соответствии с определениями последнего, а клиент — корректно вызывать методы. Чтобы контракт был работоспособным, объект и его клиенты должны договориться о способе явной идентификации каждого интерфейса, об общем способе спецификации — или описания — методов интерфейса, а также о конкретной реализации интерфейса.
Идентификация интерфейса
У каждого интерфейса СОМ два имени. Одно из них предназначено для людей — строка символов. Другое имя сложнее — оно предназначено для использования в основном программным обеспечением. Легко воспринимаемое человеком символьное имя не является гарантированно уникальным — допускается (хотя это и не распространенная практика), чтобы это имя было одинаковым у двух интерфейсов. Имя же, используемое программами, уникально — это позволяет точно идентифицировать интерфейс.
По соглашению читабельные имена большинства СОМ-интерфейсов начинаются с буквы I (от interface). Различные технологии, основанные на СОМ, определяют интерфейсы с разными именами, но все они обычно начинаются с буквы I и пытаются хотя бы немного описать назначение интерфейса. Например, интерфейс корректировщика орфографии, описанный выше, мог бы называться ISpellChecker, а интерфейс словаря синонимов — IThesaurus.
Простые дружественные имена, вроде приведенных выше, удобны при упоминании интерфейса в разговоре или при выборе имен переменных и типов для указателей интерфейсов. Однако они не годятся, когда клиент должен точно указать, какой именно интерфейс объекта ему нужен. Например, если две группы независимо разрабатывают два разных интерфейса, может случиться, что для обоих интерфейсов будет выбрано имя ISpellChecker. Клиент, знающий только об одном из этих интерфейсов и запрашивающий у некоторого объекта указатель ISpellChecker, может получить неверный указатель, если объектом реализован в действительности другой интерфейс.
А если объекту нужно реализовать оба интерфейса, его клиенты окажутся в затруднительном положении, так как не будут в точности знать, указатель какого интерфейса они получат, запрашивая ISpellChecker.
Выход прост: создатель любого интерфейса должен присвоить ему уникальное имя — глобально уникальный идентификатор (globally unique identifier — GUID). GUID интерфейса называется идентификатором интерфейса (interface identifier — IID). GUID — это 16-байтовая величина, обычно генерируемая программой-утилитой. Каждый может запустить такую утилиту на любом компьютере и гарантированно (со всех практических точек зрения) получит GUID, который будет отличаться от всех остальных.
Проблема сводится к тому, чтобы гарантировать уникальность каждого GUID во времени и пространстве. Уникальность во времени достичигается за счет того, что каждый GUID содержит метку времени, указывающую, когда он был создан, что гарантирует отличие друг от друга всех GUID, сгенерированных на данной машине. Для обеспечения уникальности в пространстве у каждого компьютера, который может быть использован для генерации GUID, должен быть уникальный идентификатор. В качестве такого идентификатора программа генерации GUID использует уникальное значение, уже имеющееся на большинстве компьютеров: адрес платы сетевого интерфейса. Если в компьютере не установлен сетевой адаптер, то из различных случайных характеристик данной системы генерируется фиктивный идентификатор машины. Но и тогда маловероятно, что идентификаторы двух машин окажутся одинаковыми.
Хотя человеку и трудно работать с GUID, последние отлично подходят для назначения гарантированно уникальных имен интерфейсов — тех, что используются программами. Люди обычно выбирают для указания интерфейсов простые читабельные имена, а не GUID. Не стоит забывайть, однако, что у каждого интерфейса фактически два имени — читабельное и IID (который, конечно, по сути GUID) и что скомпилированное и работающее программное обеспечение практически всегда пользуется последним.
Спецификация интерфейса
Объект и клиент должны иметь заранее согласованный способ описания интерфейса, т.е. способ определения методов, из которых состоит интерфейс, а также параметров этих методов.
СОМ не предписывает, как это должно быть сделано. СОМ-объект может описывать свои интерфейсы с помощью чистого C++, или некоторого псевдо-С++, или каким-то еще способом, который может быть согласован между создателем объекта и создателями его клиентов.
Важно другое: СОМ-объект обязан точно следовать стандарту двоичного интерфейса СОМ (о нем будет рассказано далее).
И все же для определения интерфейсов удобно иметь стандартный инструмент. В СОМ такой инструмент есть — язык описания интерфейсов (Interface Definition Language — IDL). IDL СОМ является в значительной степени расширением IDL Microsoft RPC, а тот в свою очередь заимствован из IDL OSF DCE (Open Software Foundation Distributed Computing Environ- j ment).
С помощью IDL можно составить полную и точную спецификацию интерфейсов объекта СОМ. Например, ниже приведена спецификация на IDL для гипотетического интерфейса корректировщика орфографии ISpellChecker:
[ object, uuid(E7CDODOO-1827-11CF-9946-444553540000) ] interface ISpellChecker : Unknown { import "unknwn.idi" HRESULT LookUpWord([in ] OLECHAR word[31], [out] boolean * found); HRESULT AddToDictionary([in] OLECHAR word[31]): HRESULT RemoveFromOictionary([in] OLECHAR word[31]); }
Как можно заметить, IDL очень похож на C++. Спецификация интерфейса начинается со слова object, указывающего, что будут использоваться расширения, добавленные СОМ к оригинальному IDL DCE. Далее следует
IID интерфейса — некоторый GUID. В DCE, откуда была заимствована эта идея, GUID называется универсально уникальным идентификатором (universal unique identifier — UUID). Так как в основе IDL СОМ лежит IDL DCE, то в описании интерфейса используется термин UUID. Иначе говоря: UUID — всего лишь другое имя для GUID.
Далее идет имя интерфейса — ISpellChecker, за ним — двоеточие и имя другого интерфейса — IUnknown.
Такая запись указывает, что ISpellChecker
наследует все методы, определенные для IUnknown, т.е. клиент, у которого есть указатель на ISpellChecker, может также вызывать и методы IUnknown. IUnknown, как будет рассказано далее, является критически важным интерфейсом для СОМ, и все остальные интерфейсы наследуют от него. (Как было объяснено в предыдущей лекции,
СОМ поддерживает только наследование интерфейса, но не наследование реализации. Хотя объект СОМ волен при определении своего интерфейса наследовать от любого из существующих интерфейсов, этот новый объект унаследует только само определение, но не реализацию существующего интерфейса. На практике наследование интерфейсов используется в СОМ нечасто. Вместо этого объект обычно поддерживает каждый необходимый ему интерфейс по отдельности (кроме lUnknown, от которого наследуют все интерфейсы). В отличие от C++ СОМ поддерживает лишь одиночное наследование, позволяя интерфейсу наследовать только от одного предка. Множественное наследование, т.е. наследование от нескольких интерфейсов одновременно, не поддерживается. Программисты на C++ могут свободно применять множественное наследование C++ для реализации объектов СОМ, однако оно не может быть использовано для спецификации интерфейсов на IDL.)
Далее в спецификации интерфейса идет оператор import. Так как данный интерфейс наследует от IUnknown, то некоторой программе, читающей определение интерфейса, может потребоваться найти описание IDL для IUnknown. Оператор import указывает такой программе, какой файл содержит нужное описание.
Вслед за оператором import в описании интерфейса идут три метода: LookUpWord, AddToDictionary и RemoveFromDictio-nary, — а также их параметры. Все три возвращают
HRESULT — стандартное возвращаемое значение, указывающее, был ли вызов обработан успешно. Параметры в IDL могут быть сколь угодно сложными, использовать такие типы, как структуры и массивы, являющиеся производными своих аналогов в C++. Каждый параметр помечен [in] или [out].
Значения параметров [in] передаются при вызове метода от клиента объекту, тогда как значения [out] передаются в обратном направлении. (Параметры, значения которых передаются в обоих направлениях, могут быть помечены как [in, out].) Подобные метки могут помочь читателю лучше понять интерфейс, однако их основное назначение в том, чтобы программа, обрабатывающая спецификацию интерфейса, точно определила, какие данные и в каком направлении копировать.
Подобная простая спецификация — все, что необходимо для заключения контракта между объектом СОМ и его клиентом. Хотя интерфейс и не обязан задаваться именно таким способом, но следование ему может значительно облегчить жизнь разработчика. Кроме того, неплохо иметь одну стандартную схему для спецификации интерфейсов объектов СОМ.
Обратите внимание на одно важное обстоятельство, связанное со спецификациями интерфейса:
после того как интерфейс опубликован и начал где-то работать, после того как его задействовали в выпущенной версии какого-либо программного обеспечения, изменять его, по правилам СОМ, нельзя. Если создатель интерфейса хочет добавить новый метод, изменить список параметров метода или внести какие-либо другие изменения, это также недопустимо. Интерфейсы должны быть фиксированными.
Добавление новой или изменение существующей функциональности требует определения полностью нового интерфейса. Такой интерфейс может наследовать от старого, но тем не менее является совершенно отличным от него и имеет новый и другой IID. Создатель программного обеспечения, поддерживающего новый интерфейс, может продолжать поддерживать и старый, но это не является требованием. Создатель программы имеет право прекратить поддержку старого интерфейса (хотя обычно это плохая идея), но ему
категорически запрещено изменять этот интерфейс.
Реализация интерфейса
Чтобы вызвать метод, клиенту необходимо точно и подробно знать, как это делать. Спецификация интерфейса, подобная приведенной выше, описывает лишь одну важную часть процесса.
Но СОМ также определяет и другое: она задает стандартный двоичный формат, который каждый СОМ-объект должен поддерживать для каждого интерфейса. Наличие стандартного двоичного формата означает, что любой клиент может вызывать методы любого объекта независимо от языков программирования, на которых написаны клиент и объект.
Клиентский указатель интерфейса фактически является указателем на указатель внутри объекта. Последний в свою очередь указывает на таблицу, содержащую другие указатели.
Эта виртуальная таблица (viable) содержит указатели на все методы интерфейса.
Следует заметить: методы ISpellChecker изображены на рисунке как элементы 4, 5 и 6 виртуальной таблицы. Что представляют собой первые три метода? Это методы, определенные интерфейсом IUnknown. Поскольку ISpellChecker наследует от IUnknown, у клиента должна быть возможность вызова методов lUnknown через указатель на ISpellChecker. Чтобы это стало возможным, виртуальная таблица ISpellChecker должна содержать указатели на эти три метода. Фактически,
так как каждый интерфейс наследует от IUnknown, виртуальная таблица любого СОМ-ин-терфейса начинается с указателей на три метода IUnknown. Подобная двоичная структура имеет место для всех интерфейсов, поддерживаемых любым объектом.
Формат интерфейса СОМ моделирует структуру данных, генерируемую компилятором C++ для класса этого языка (класс задает тип объектов, а двоичная структура генерируется для объектов класса.). Это сходство означает, что СОМ-объекты очень легко создавать на C++. Хотя СОМ-объекты можно писать на любом языке, поддерживающем описанные стандартные двоичные структуры, в СОМ, честно говоря, имеется некоторый уклон в сторону реализации на C++ (что не должно удивлять, если учесть популярность C++).
При вызове клиентом метода интерфейса выполняется проход по описанной структуре (с помощью указателя на виртуальную таблицу извлекается указатель на метод, который в свою очередь извлекает код, фактически предоставляющий сервис) и исполняется соответствующий код. Если клиент написан на C++, этот проход невидим для программиста, поскольку C++ и так делает это автоматически. Вызов методов СОМ из программы на С несколько сложнее. Тот, кто пишет клиент на С, должен знать, что необходимо пройти по цепочке указателей, и кодировать вызов соответствующим образом. Результат в любом случае один и тот же: исполняется метод в объекте.
Содержание раздела