
- •Оглавление
- •От автора
- •Введение
- •Преимущества использования компонентов
- •Адаптация приложений
- •Библиотеки компонентов
- •Распределенные компоненты
- •Требования к компонентам
- •Динамическая компоновка
- •Инкапсуляция
- •Заключительные замечания о компонентах
- •Повторное использование архитектур приложений
- •Соглашения о кодировании
- •Законченный пример
- •Взаимодействие в обход интерфейсов
- •Детали реализации
- •Теория интерфейсов, часть II
- •Интерфейсы не изменяются
- •Полиморфизм
- •Что за интерфейсом
- •Таблица виртуальных функций
- •Указатели vtbl и данные экземпляра
- •Множественные экземпляры
- •Разные классы, одинаковые vtbl
- •Запрос интерфейса
- •IUnknown
- •Получение указателя на IUnknown
- •Знакомство с QueryInterface
- •Использование QueryInterface
- •Реализация QueryInterface
- •А теперь все вместе
- •Правила и соглашения QueryInterface
- •Вы всегда получаете один и тот же IUnknown
- •Вы можете получить интерфейс снова, если смогли получить его раньше
- •Вы можете снова получить интерфейс, который у Вас уже есть
- •Вы всегда можете вернуться туда, откуда начали
- •Если Вы смогли попасть куда-то хоть откуда-нибудь, Вы можете попасть туда откуда угодно
- •QueryInterface определяет компонент
- •Вы не можете воспользоваться всеми знаниями сразу
- •Работа с новыми версиями компонентов
- •Когда нужно создавать новую версию
- •Имена версий интерфейсов
- •Неявные соглашения
- •Управление временем жизни
- •Подсчет ссылок
- •Подсчет ссылок на отдельные интерфейсы
- •Реализация AddRef и Release
- •Когда подсчитывать ссылки
- •Оптимизация подсчета ссылок
- •Правила подсчета ссылок
- •Амуниция пожарного, резюме
- •Создание компонента
- •Экспорт функции из DLL
- •Загрузка DLL
- •Разбиваем монолит
- •Тексты программ
- •Связки объектов
- •Негибкое связывание, резюме
- •HRESULT
- •Поиск HRESULT
- •Использование HRESULT
- •Определение собственных кодов ошибки
- •GUID
- •Зачем нужен GUID?
- •Объявление и определение GUID
- •Сравнение GUID
- •Передача GUID по ссылке
- •Реестр Windows
- •Организация Реестра
- •Редактор Реестра
- •Необходимый минимум
- •Другие детали Реестра
- •ProgID
- •Саморегистрация
- •Категории компонентов
- •OleView
- •Некоторые функции библиотеки COM
- •Инициализация библиотеки COM
- •Управление памятью
- •Преобразование строк в GUID
- •Резюме
- •CoCreateInstance
- •Прототип CoCreateInstance
- •Использование CoCreateInstance
- •Контекст класса
- •Листинг кода клиента
- •Но CoCreateInstance недостаточно гибка
- •Фабрики класса
- •Использование CoGetClassObject
- •IClassFactory
- •CoCreateInstance vs. CoGetClassObject
- •Фабрики класса инкапсулируют создание компонентов
- •Реализация фабрики класса
- •Использование DllGetClassObject
- •Общая картина
- •Листинг кода компонента
- •Последовательность выполнения
- •Регистрация компонента
- •Несколько компонентов в одной DLL
- •Повторное применение реализации фабрики класса
- •Выгрузка DLL
- •Использование DllCanUnloadNow
- •LockServer
- •Резюме
- •Включение и агрегирование
- •Включение
- •Агрегирование
- •Сравнение включения и агрегирования
- •Реализация включения
- •Расширение интерфейсов
- •Реализация агрегирования
- •Магия QueryInterface
- •Неверный IUnknown
- •Интерфейсы IUnknown для агрегирования
- •Создание внутреннего компонента
- •Законченный пример
- •Слепое агрегирование
- •Агрегирование и включение в реальном мире
- •Предоставление информации о внутреннем состоянии
- •Моделирование виртуальных функций
- •Резюме
- •Упрощения на клиентской стороне
- •Smart-указатели на интерфейсы
- •Классы-оболочки C++
- •Упрощения на серверной стороне
- •Базовый класс CUnknown
- •Базовый класс CFactory
- •Использование CUnknown и CFactory
- •Резюме
- •Разные процессы
- •Локальный вызов процедуры
- •Маршалинг
- •DLL заместителя/заглушки
- •Введение в IDL/MIDL
- •Примеры описаний интерфейсов на IDL
- •Компилятор MIDL
- •Реализация локального сервера
- •Работа примера программы
- •Нет точек входа
- •Запуск фабрик класса
- •Изменения в LockServer
- •Удаленный сервер
- •Что делает DCOMCNFG.EXE?
- •Но как это работает?
- •Другая информация DCOM
- •Резюме
- •Новый способ общения
- •Старый способ общения
- •Использование IDispatch
- •Параметры Invoke
- •Примеры
- •Тип VARIANT
- •Тип данных BSTR
- •Тип данных SAFEARRAY
- •Библиотеки типа
- •Создание библиотеки типа
- •Библиотеки типа в Реестре
- •Реализация IDispatch
- •Генерация исключений
- •Маршалинг
- •Что Вы хотите сделать сегодня?
- •Потоковые модели COM
- •Потоки Win32
- •Подразделение
- •Разделенные потоки
- •Свободные потоки
- •Маршалинг и синхронизация
- •Реализация модели разделенных потоков
- •Автоматический маршалинг
- •Ручной маршалинг
- •Настало время написать программу
- •Пример с разделенным потоком
- •Реализация модели свободных потоков
- •Пример со свободным потоком
- •Оптимизация маршалинга для свободных потоков
- •Информация о потоковой модели в Реестре
- •Резюме
- •Программа Tangram
- •Tangram в работе
- •Детали и составные части
- •Клиентский EXE-модуль
- •Компонент TangramModel
- •Компоненты TangramGdiVisual и TangramGLVisual
- •Компоненты TangramGdiWorld и TangramGLWorld
- •Что демонстрирует пример
- •Файлы IDL
- •Файл DLLDATA.C
- •Циклический подсчет ссылок
- •Не вызывайте AddRef
- •Используйте явное удаление
- •Используйте отдельный компонент
- •События и точки подключения
- •IEnumXXX

11 глава
Диспетчерские интерфейсы и автоматизация
Как гласит пословица, «есть много способов содрать шкуру с кошки». Поскольку я никогда не пытался обдирать кошек и не нахожу в этом особого смысла, я предпочитаю говорить: «Есть много способов причесать кошку». Полагаю, что большинству кошек моя версия понравится больше. Один мой друг из Ла Гранде, штат Джорджия, использует другую версию этой фразы: «Есть много способов пахнуть как скунс». Если верить его матери, большинство этих способов ему известно. Все это говорит о том, как много можно придумать способов перефразировать поговорку.
В этой главе Вы увидите, что есть и много способов коммуникации между клиентом и компонентом. В предыдущих главах клиент использовал интерфейс СОМ для работы с компонентом напрямую. В этой главе мы рассмотрим Автоматизацию (в прошлом OLE Автоматизацию) — другой способ управления компонентом. Этот способ использует такие приложения, как Microsoft Word и Microsoft Excel, а также интерпретируемые языки типа Visual Basic и Java.
Автоматизация облегчает интерпретируемым языкам и макроязыкам доступ к компонентам СОМ, а также облегчает написание самих компонентов на этих языках. В Автоматизации делается упор на проверку типов во время выполнения за счет снижения скорости выполнения и проверки типов во время компиляции. Но если Автоматизация проста для программиста на макроязыке, то от разработчика на С++ она требует гораздо больше труда. Во многих случаях Автоматизация заменяет код, генерируемый компилятором, кодом, который написан разработчиком.
Автоматизация — не пристройка к СОМ, а надстройка над нею. Сервер Автоматизации (Automation server) — это компонент СОМ, который реализует интерфейс IDispatch. Контролер Автоматизации (Automation Controller)
— это клиент СОМ, взаимодействующий с сервером Автоматизации через интерфейс IDispatch. Контролер Автоматизации не вызывает функции сервера Автоматизации напрямую. Вместо этого он использует методы интерфейса IDispatch для неявного вызова функций сервера Автоматизации.
Интерфейс IDispatch, как и вся Автоматизация, разрабатывался для Visual Basic — чтобы его можно было использовать для автоматизации таких приложений, как Microsoft Word и Microsoft Excel. В конце концов из
Visual Basic вырос Visual Basic for Applications — язык для Microsoft Office. Подмножество Visual Basic for Applications — Visual Basic Scripting Edition (VBScript) — можно использовать для автоматизации элементов управления на страницах Web. Версия 5.0 Microsoft Developer Studio использует VBScript в качестве своего макроязыка.
Практически любой сервис, который можно представить через интерфейсы СОМ, можно предоставить и при помощи IDispatch. Из этого следует, что IDispatch и Автоматизация — это не менее (а может быть, и более) широкая тема, чем СОМ. Поскольку эта книга посвящена все-таки СОМ, мы рассмотрим Автоматизацию только частично. Но и этого все еще большая область: IDispatch, disp-интерфейсы, дуальные интерфейсы, библиотеки типа, IDL, VARIANT, BSTR и многое другое. По счастью, именно эти вопросы наиболее важны при программировании Автоматизации на С++.
Давайте начнем обдирать — то есть я хочу сказать причесывать — эту кошку, начиная с головы; посмотрим, чем работа через IDispatch отличается от работы через интерфейсы СОМ.
Новый способ общения
Что делает IDispatch столь замечательным интерфейсом СОМ? Дело в том, что IDispatch предоставляет клиентам и компонентам новый способ общения между собой. Вместо предоставления нескольких собственных интерфейсов, специфичных для его сервисов, компонент может обеспечить доступ к этим сервисам через один стандартный интерфейс, IDispatch.

180
Прежде чем подробно рассматривать IDispatch, давайте разберемся, как он может поддерживать столь много функций; для этого мы сравним его со специализированными интерфейсами СОМ (которые он может заменить).
Старый способ общения
Давайте еще раз кратко рассмотрим прежний метод, используемый клиентами для управления компонентами. Возможно, Вас уже тошнит от этого, но клиент и компонент все же взаимодействуют через интерфейсы. Интерфейс представляет собой массив указателей на функции. Откуда клиенту известно, какой элемент массива содержит указатель на нужную функцию? Код клиента включает заголовочный файл, содержащий описание интерфейса как абстрактного базового класса. Компилятор считывает этот заголовочный файл и присваивает индекс каждому методу абстрактного базового класса. Этот индекс — индекс указателя на функцию в абстрактном массиве. Затем компилятор может рассматривать следующую строку кода:
pIX->FxStringOut(msg);
как
(*(pIX->pvtbl[IndexOfFxStringOut]))(pIX, msg);
где pvtbl — это указатель на ytbl данного класса, а IndexOfFxStringOut — индекс указателя на функцию FxStringOut в таблице указателей на функции. Все это происходит автоматически — Вы этого не знаете или, в большинстве случаев, Вас это не беспокоит.
Вам придется побеспокоиться об этом при разработке макроязыка для своего приложения. Макроязык будет гораздо мощнее, если сможет использовать компоненты СОМ. Но каким образом макроязык получит смещения функций в vtbl? Я сомневаюсь, что Вы захотите писать синтаксический анализатор С++ для разбора заголовочного файла интерфейса СОМ.
Когда макроязык вызывает функцию компонента СОМ, у него есть три элемента информации: ProgID компонента, реализующего функцию, имя функции и ее аргументы. Нам нужен простой способ, чтобы интерпретатор макроязыка мог вызывать функцию по ее имени. Именно для этого и служит IDispatch.
IDispatch, или «Я диспетчер, ты диспетчер…»*
Говоря попросту, IDipatch принимает имя функции и выполняет ее. Описание IDipatch на IDL, взятое из файла OAIDL.IDL, приведено ниже:
interface IDispatch : IUnknown
{
HRESULT GetTypeInfoCount([out] UINT * pctinfo);
HRESULT GetTypeInfo([in] UINT iTInfo, [in] LCID lcid,
[out] ItypeInfo ** ppTInfo);
HRESULT GetIDsOfNames( [in] REFIID riid,
[in, size_is(cNames)] LPOLESTR * rgszNames, [in] UINT cNames,
[in] LCID lcid,
[out, size_is(cNames)] DISPID * rgDispId); HRESULT Invoke([in] DISPID dispIdMember,
[in] REFIID riid, [in] LCID lcid, [in] WORD wFlags,
[in, out] DISPPARAMS * pDispParams, [out] VARIANT * pVarResult,
[out] EXCEPINFO * pExcepInfo, [out] UINT * puArgErr);
};
Наиболее интересны в этом интерфейсе функции GetIDsOfNames и Invoke. Первая принимает имя функции и возвращает ее диспетчерский идентификатор, или DISPID. DISPID — это не GUID, а просто длинное целое (LONG), идентифицирующее функцию. DISPID не уникальны (за исключением данной реализации IDipatch). У каждой реализации IDipatch имеется собственный IID (некоторые называют его DIID).
* В оригинале «IDispatch, You Dispatch, We Dispatch». — Прим. перев.

181
Для вызова функции контроллер автоматизации передает ее DISPID функции-члену Invoke. Последняя использует DISPID как индекс в массиве указателей на функции, что очень похоже на обычные интерфейсы СОМ. Однако сервер Автоматизации не обязан реализовывать Invoke именно так. Простой сервер Автоматизации может использовать оператор switch, который выполняет разный код в зависимости от значения DISPID. Именно так реализовывали оконные процедуры, прежде чем стала популярна MFC.
У оконных процедур и IDispatch::Invoke есть другие общие черты. Как окно ассоциируется с оконной процедурой, так и сервер Автоматизации ассоциируется с функцией IDispatch::Invoke. Microsoft Windows посылает оконной процедуре сообщения; контроллер автоматизации посылает IDispatch::Invoke разные DISPID. Поведение оконной процедуры определяется получаемыми сообщениями; поведение Invoke — получаемыми
DISPID.
Способ действий IDispatch::Invoke напоминает и vtbl. Invoke реализует набор функций, доступ к которым осуществляется по индексу. Таблица vtbl — массив указателей на функции, обращение к которым также идет по индексу. Но если vtbl работает автоматически за счет магии компилятора С++, то Invoke работает благодаря тяжкому труду программиста. Однако в С++ vtbl статические, и компилятор работает только во время компиляции. Если программисту С++ необходимо порождать vtbl во время выполнения, он предоставлен самому себе. С другой стороны, легко создать универсальную реализацию Invoke, которая сможет «на лету» адаптироваться для реализации самых разных сервисов.
Disp-интерфейсы
У реализации IDispatch::Invoke есть еще одно сходство с vtbl. Обе они определяют интерфейс. Набор функций,
реализованных с помощью IDispatch::Invoke, называется диспетчерским интерфейсом (dispatch interface) или,
короче, disp-интерфейсом (dispinterface). По определению, интерфейс СОМ — это указатель на массив указателей на функции, первыми тремя из которых являются QueryInterface, AddRef и Release. В соответствии с более общим определением, интерфейс — это набор функций и переменных, посредством которых взаимодействуют две части программы. Реализация IDispatch::Invoke определяет набор функций, посредством которых взаимодействуют сервер и контроллер Автоматизации. Как нетрудно видеть, функции, реализованные Invoke, образуют интерфейс, но не интерфейс СОМ.
На рис. 11-1 диспетчерский интерфейс представлен графически. Слева изображен традиционный интерфейс СОМ
— IDispatch реализованный при помощи vtbl. Справа показан disp-интерфейс. Центральную роль в dispинтерфейсе играют DISPID, распознаваемые IDispatch::Invoke. На рисунке показана одна из возможных реализаций Invoke и GetIDsOfNames: массив имен функций и массив указателей на функции, индексируемые DISPID. Это только один способ. Для больших disp-интерфейсов GetIDsOfNames работает быстрее, если передаваемое ей имя используется в качестве ключа хеш-таблицы.
|
Интерфейс IDispatch |
Disp-интерфейс |
|||
IDispatch* |
pVtbl |
&QueryInterface |
DISPID |
Имя |
|
pIDispatch |
|
1 |
"Foo" |
||
|
|
|
|||
|
|
&AddRef |
|
||
|
|
|
2 |
"Bar" |
|
|
|
&Release |
функция |
||
|
|
3 |
"FooBar" |
||
|
|
&GetTypeInfoCount |
GetDsOfNames |
||
|
|
|
|
Указатель |
|
|
|
&GetTypeInfo |
|
|
|
|
|
DISPID |
на |
||
|
|
&GetDsOfNames |
функцию |
||
|
|
&Invoke |
функция |
1 |
&Foo |
|
|
2 |
&Bar |
||
|
|
|
Invoke |
||
|
|
|
|
3 |
&FooBar |
Рис. 11-1. Disp-интерфейсы реализуются с помощью IDispatch и не являются интерфейсами СОМ. На этом рисунке представлена только одна из возможных реализаций IDispatch::Invoke.
Конечно, для реализации IDispatch::Invoke можно использовать и интерфейс СОМ (рис. 11-2).
Дуальные интерфейсы
На рис. 11-2 представлен не единственный способ реализации disp-интерфейса при помощи интерфейса СОМ. Другой метод, показанный на рис. 11-3, состоит в том, чтобы интерфейс СОМ, реализующий IDispatch::Invoke, наследовал не IUnknown, а IDispatch. Так реализуют интерфейсы, называемые дуальными интерфейсами (dual interface). Дуальный интерфейс — это disp-интерфейс, все члены которого, доступные через Invoke, доступны и напрямую через vtbl.

182
|
Интерфейс IDispatch |
Disp-интерфейс |
|||
IDispatch* |
pVtbl |
&QueryInterface |
DISPID |
Имя |
|
pIDispatch |
|
1 |
"Foo" |
||
|
|
|
|||
|
|
&AddRef |
|
||
|
|
|
2 |
"Bar" |
|
|
|
&Release |
функция |
||
|
|
3 |
"FooBar" |
||
|
|
&GetTypeInfoCount |
GetDsOfNames |
||
|
|
|
|
|
|
|
|
&GetTypeInfo |
Интерфейс FooBar |
||
|
|
&GetDsOfNames |
функция |
pVtbr |
&Foo |
|
|
&Invoke |
|||
|
|
Invoke |
|
&Bar |
|
|
|
|
|
|
|
|
|
|
|
|
&FooBar |
Рис. 11-2 Реализация IDispatch::Invoke с помощью интерфейса СОМ.
|
Интерфейс FooBar наследует |
Disp-интерфейс |
|||
|
интерфейсу IDispatch |
||||
|
|
|
|
||
IDispatch* |
pVtbl |
&QueryInterface |
DISPID |
Имя |
|
pIDispatch |
|
1 |
"Foo" |
||
|
|
|
|||
|
|
&AddRef |
|
||
|
|
|
2 |
"Bar" |
|
|
|
&Release |
функция |
||
|
|
3 |
"FooBar" |
||
|
|
&GetTypeInfoCount |
GetDsOfNames |
||
|
|
|
|
|
|
|
|
&GetTypeInfo |
|
|
|
|
|
&GetDsOfNames |
функция |
|
|
|
|
&Invoke |
|
|
|
|
|
Invoke |
|
|
|
|
|
&Foo |
|
|
|
|
|
&Bar |
|
|
|
|
|
&FooBar |
|
|
|
Рис. 11-3. Дуальный интерфейс — это интерфейс СОМ, который наследует IDispatch. Доступ к членам такого интерфейса возможен и через Invoke, и через vtbl.
Дуальные интерфейсы предпочтительны для реализации disp-интерфейсов. Они позволяют программистам на С++ работать через vtbl; такие вызовы не только легче реализовать на С++, но они и быстрее выполняются. Макро- и интерпретируемые языки также могут использовать сервисы компонентов, реализующих дуальные интерфейсы, применяя Invoke вместо вызова через vtbl. Программа на Visual Basic может работать с дуальным интерфейсом как с disp-интерфейсом, так и через vtbl. Если Вы объявили тип переменной Visual Basic как Object, то работа идет через disp-интерфейс:
Dim doc As Object
Set doc = Application.ActiveDocument doc.Activate
Если переменная имеет тип конкретного объекта, то Visual Basic выполняет вызов через vtbl:
Dim doc As Document
Set doc = Application.ActiveDocument
Doc.Activate
Однако если что-то выглядит слишком хорошо, чтобы быть правдой, — вероятно, так оно и есть. Наверное, Вы удивитесь, узнав, что у дуальных интерфейсов есть недостатки. С точки зрения Visual Basic, их и нет. Но с точки зрения контроллера Автоматизации, написанного на С++, их несколько. Основной из них — ограничения на типы параметров.
Прежде чем обсудить ограниченность набора типов, допустимых для параметров disp-интерфейсов и дуальных интерфейсов, рассмотрим, как вызывается disp-интерфейс на С++.