- •Оглавление
- •От автора
- •Введение
- •Преимущества использования компонентов
- •Адаптация приложений
- •Библиотеки компонентов
- •Распределенные компоненты
- •Требования к компонентам
- •Динамическая компоновка
- •Инкапсуляция
- •Заключительные замечания о компонентах
- •Повторное использование архитектур приложений
- •Соглашения о кодировании
- •Законченный пример
- •Взаимодействие в обход интерфейсов
- •Детали реализации
- •Теория интерфейсов, часть 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
36
Запрос интерфейса
Поскольку в СОМ все начинается и заканчивается интерфейсом, давайте и мы начнем с интерфейса, через который запрашиваются другие интерфейсы.
Клиент всегда взаимодействует с компонентом через некоторый интерфейс. Даже для запроса у компонента интерфейса используется специальный интерфейс IUnknown. Определение IUnknown, содержащееся в заголовочном файле UNKNWN.H, входящим в состав Win32 SDK, выглядит так:
interface IUnknown
{
virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) = 0; virtual ULONG __stdcall AddRef() = 0;
virtual ULONG __stdcall Release() = 0;
}
В IUnknown имеется функция с именем QueryInterface. Клиент вызывает ее, чтобы определить, поддерживает ли компонент некоторый интерфейс. В этой главе я собираюсь поговорить о QueryInterface. В гл. 4 мы рассмотрим AddRef и Release, которые предоставляют способ управления временем жизни интерфейса.
IUnknown
Мне всегда казалось забавным название IUnknown. Это единственный интерфейс, о котором знают все клиенты и компоненты, и тем не менее это «неизвестный интерфейс»*. Происхождение названия просто. Все интерфейсы СОМ должны наследовать IUnknown. Таким образом, если у клиента имеется указатель на IUnknown, то клиент, не зная, указателем на какой именно интерфейс обладает, знает, что может запросить через него другие интерфейсы.
Клиент |
|
IX |
CA |
|||||||
|
|
|
|
|
Таблица виртуальных функций |
|
||||
|
|
|
|
|
|
|
|
|
|
|
|
pA |
|
|
Указатель vtbl |
|
|
QueryInterface |
|
|
QueryInterface |
|
|
|
|
|
|
|
||||
|
|
|
|
|
|
|
|
|
|
AddRef |
|
|
|
|
|
|
|
AddRef |
|
|
|
|
|
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|
Release |
|
|
|
|
|
|
|
Release |
|
|
|
|
|
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|
Fx |
|
|
|
|
|
|
|
Fx |
|
|
|
|
|
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|
|
Рис. 3-1 Все интерфейсы СОМ наследуют IUnknown и содержат указатели на QueryInterface, AddRef и Release в первых трех элементах своих vtbl
Поскольку все интерфейсы СОМ наследуют IUnknown, в каждом интерфейсе есть функции QueryInterface, AddRef и Release — три первые функции в vtbl (см. рис. 3-1). Благодаря этому все интерфейсы СОМ можно полиморфно трактовать как интерфейсы IUnknown. Если в первых трех элементах vtbl интерфейса не содержатся указатели на три перечисленные функции, то это не интерфейс СОМ. Поскольку все интерфейсы наследуют IUnknown, постольку все они поддерживают QueryInterface. Таким образом, любой интерфейс можно использовать для получения всех остальных интерфейсов, поддерживаемых компонентом.
Поскольку все указатели интерфейсов являются также и указателями на IUnknown, клиенту не требуется хранить отдельный указатель на собственно компонент. Клиент работает только с указателями интерфейсов.
Получение указателя на IUnknown
Каким образом клиент может получить указатель на IUnknown? Мы используем функцию с именем CreateInstance, которая создает компонент и возвращает указатель на IUnknown:
IUnknown* CreateInstance();
Клиент использует CreateInstance вместо оператора new. В этой главе мы создадим простую версию данной функции, которую будем изменять на протяжении нескольких последующих глав в соответствии с нашими потребностями. В гл. 6 и 7 будет представлен «официальный» способ создания компонентов СОМ.
Теперь, когда мы знаем, как клиент получает указатель на IUnknown, давайте посмотрим, как он может использовать QueryInterface для получения других интерфейсов, а затем займемся реализацией QueryInterface.
* Unknown (англ.) — неизвестный. — Прим. перев.
37
Знакомство с QueryInterface
IUnknown содержит функцию-член QueryInterface, при помощи которой клиент определяет, поддерживается ли тот или иной интерфейс. QueryInterface возвращает указатель на интерфейс, если компонент его поддерживает; в противном случае возвращается код ошибки (тогда клиент может запросить указатель на другой интерфейс или аккуратно выгрузить компонент).
У QueryInterface два параметра:
Virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv);
Первый параметр — идентификатор интерфейса, так называемая IID-структура. Более подробно IID будут рассматриваться в гл. 6. Пока же мы будем рассматривать их как константы, задающие интерфейс. Второй параметр — адрес, по которому QueryInterface помещает указатель на искомый интерфейс.
QueryInterface возвращает HRESULT; это не описатель (handle), как может показаться по названию. HRESULT — просто 32-разрядный код результата, записанный в определенном формате. QueryInterface может возвратить либо S_OK, либо E_NOINTERFACE. Клиент не должен прямо сравнивать возвращаемое QueryInterface значение с этими константами; для проверки надо использовать макросы SUCCEEDED или FAILED. Исчерпывающее обсуждение HRESULT содержится в гл. 6.
Теперь посмотрим, как используется, а затем — как реализуется QueryInterface.
Использование QueryInterface
Предположим, что у нас есть указатель на IUnknown, pI. Чтобы определить, можно ли использовать некоторый другой интерфейс, мы вызываем QueryInterface, передавая ей идентификатор нужного нам интерфейса. Если QueryInterface отработала успешно, мы можем пользоваться указателем:
void foo(IUnknown* pI)
{
//Определить указатель на интерфейс
IX* pIX = NULL;
//Запросить интерфейс IX
HRESULT hr = pI->QueryInterface(IID_IX, (void**)&pIX);
// Проверить значение результата if (SUCCEEDED(hr))
{
// Использовать интерфейс pIX->Fx();
}
}
В этом фрагменте кода мы запрашиваем у pI интерфейс, идентифицируемый с помощью IID_IX. Определение IID_IX содержится в заголовочном файле, предоставляемом компонентом (или, что более вероятно, оно извлекается из библиотеки типа, как будет показано в гл. 13).
Обратите внимание, что pIX устанавливается в NULL перед вызовом QueryInterface. Это пример хорошего программирования с защитой от ошибок. Как мы вскоре увидим, предполагается, что для неудачного запроса QueryInterface должна устанавливать возвращаемый указатель в NULL. Однако, поскольку QueryInterface реализуется программистом компонента, в некоторых реализациях это наверняка не будет сделано. Для безопасности следует установить указатель в NULL самостоятельно.
Таковы основы использования QueryInterface. Позже мы рассмотрим некоторые более продвинутые приемы ее применения. Но сначала давайте посмотрим, как следует реализовывать QueryInterface в наших компонентах.
Реализация QueryInterface
Реализовать QueryInterface легко. Все, что нужно сделать, — это вернуть указатель интерфейса, соответствующего данному IID. Если интерфейс поддерживается, то функция возвращает S_OK и указатель. В противном случае возвращаются E_NOINTERFACE и NULL. Теперь давайте запишем QueryInterface для следующего компонента, реализуемого классом CA:
interface IX : IUnknown { /*...*/ }; interface IY : IUnknown { /*...*/ };
class CA : public IX, public IY { /*...*/ };
Иерархия наследования для этого класса и его интерфейсов показана на рис. 3-2.
38
|
IUnknown |
|
|
|
IUnknown |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
IX |
|
|
|
IY |
|
|
|
|
|
|
|
|
CA
Рис. 3-2 Иерархия наследования для приведенного выше фрагмента кода
Невиртуальное наследование
Обратите внимание, что IUnknown — не виртуальный базовый класс. IX и IY не могут наследовать IUnknown виртуально, так как виртуальное наследование приводит к vtbl, несовместимой с форматом СОМ. Ели бы IX и IY наследовали IUnknown виртуально, то первые три элемента их vtbl не были бы указателями на три функции-члена IUnknown.
Следующий фрагмент кода реализует QueryInterface для класса из приведенного выше фрагмента кода. Эта версия функции возвращает указатели на три разных интерфейса — IUnknown, IX и IY. Обратите внимание, что возвращаемый указатель на IUnknown всегда один и тот же несмотря на то, что класс CA наследует два таких интерфейса (от IX и от IY).
HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv)
{
if (iid == IID_IUnknown)
{
*ppv = static_cast<IX*>(this);
}
else if (iid == IID_IX)
{
// Клиент запрашивает интерфейс IX *ppv = static_cast<IX*>(this);
}
else if (iid = IID_IY)
{
// Клиент запрашивает интерфейс IY *ppv = static_cast<IY*>(this);
}
else
{
//Мы не поддерживаем запрашиваемый клиентом интерфейс.
//Установить возвращаемый указатель в NULL.
*ppv = NULL;
return E_NOINTERFACE;
}
static_cast<IUnknown*>(*ppv)->AddRef(); // См. гл. 4 return S_OK;
}
Здесь для реализации QueryInterface использован простой оператор if-then-else. Вы можете использовать любой другой способ, обеспечивающий проверку и ветвление. Мне случалось встречать реализации на основе массивов, хэш-таблиц и деревьев; они полезны, когда компонент поддерживает много интерфейсов. Нельзя, однако, использовать оператор case*, поскольку идентификатор интерфейса — структура, а не константа.
Обратите внимание, что QueryInterface устанавливает указатель интерфейса в NULL, если интерфейс не поддерживается. Это не только требование СОМ, это вообще полезно; NULL вызовет фатальную ошибку в клиентах, которые не проверяют возвращаемые значения. Это менее опасно, чем позволить клиенту выполнять произвольный код, содержащийся по неинициализированному указателю. Кстати, вызов AddRef в конце QueryInterface в настоящий момент ничего не делает. Реализацией AddRef мы займемся в гл. 4.
Основы приведения типов
Вы, вероятно, заметили, что QueryInterface выполняет приведение указателя this, прежде чем сохранить его в ppv. Это очень важно. В зависимости от приведения, значение, сохраняемое в ppv, может изменяться. Да-да, приведение this к указателю на IX дает не тот же адрес, что приведение к указателю на IY. Например:
* Словом case этот оператор чаще называется в языках семейства Pascal. В С и С++ соответствующий оператор называется switch, хотя ключевое слово case также используется в синтаксисе данного оператора. — Прим. перев.
39
static_cast<IX*>(this) != static_cast<IY*>(this) static_cast<void*>(this) != static_cast<IY*>(this)
или, для тех, кому привычнее старый стиль,
(IX*)this != (IY*)this (void*)this != (IY*)this
Изменение указателя this при приведении типа обусловлено тем, как в С++ реализовано множественное наследование. Более подробно об этом рассказывает врезка «Множественное наследование и приведение типов».
Перед присваиванием указателю, описанному как void, надо всегда явно приводить this к нужному типу. Интересная проблема связана с возвратом указателя на IUnknown. Можно было бы написать:
*ppv = static_cast<IUnknown*>(this); // неоднозначность
Однако такое приведение неоднозначно, поскольку IUnknown наследуют оба интерфейса, IX и IY. Таким образом,
следует выбрать, какой из указателей — static_cast<IUnknown*>(static_cast<IX*>(this)) или static_cast<IUnknown*>(static_cast<IY*>(this)) — возвращать. В данном случае выбор не существенен, поскольку реализации указателей идентичны. Однако Вы должны действовать по всей программе единообразно, поскольку указатели не идентичны — а СОМ требует, чтобы для IUnknown всегда возвращался один и тот же указатель. Это требование будет обсуждаться далее в этой главе.
Множественное наследование и приведение типов
Обычно приведение указателя к другому типу не изменяет значения. Однако для поддержки множественного наследования С++ в некоторых случаях изменяет указатель на экземпляр класса. Большинство программистов на С++ не знают об этом побочном эффекте множественного наследования. Предположим, что у нас есть С++- класс CA:
class CA : public IX, public IY { ... }
Так как CA наследует и IX, и IY, то мы можем использовать указатель на CA везде, где можно использовать указатель на IX или IY. Указатель на CA можно передать функции, принимающей указатель на IX или IY, и функция будет работать правильно. Например:
void foo(IX* pIX); void bar(IY* pIY);
int main()
{
CA* pA = new CA; foo(pA); bar(pA);
delete pA; return 0;
}
foo требуется указатель на указатель таблицы виртуальных функций IX, тогда как bar — указатель на указатель таблицы виртуальный функций IY. Содержимое таблиц виртуальных функций IX и IY, конечно же, разное. Мы не можем передать bar указатель vtbl IX и ожидать, что функция будет работать. Таким образом, компилятор не может передавать один и тот же указатель и foo, и bar, он должен модифицировать указатель на CA так, чтобы тот указывал на подходящий указатель виртуальной таблицы. На рис. 3-3 показан формат размещения объекта CA в памяти.
CA::this |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
IX Указатель vtbl |
∆ |
|
IY |
|
Таблица виртуальных |
|||||||
|
|
|
|
|
|||||||||
(IX*)CA::this |
|
|
|
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
||||
|
|
|
IY Указатель vtbl |
|
|
|
|
|
|
функций |
|
||
(IY*)CA::this |
|
|
|
|
|
|
|
|
|
||||
|
|
|
|
|
|
|
|
|
QueryInterface |
||||
|
|
|
|
|
|
|
|
||||||
|
|
|
|
Данные |
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
AddRef |
|
|
||
|
|
|
|
экземпляра для CA |
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|
|
|
IX |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Release
Fx
CA
QueryInterface
AddRef
IY
Release
Fx
Рис. 3-3 Формат памяти для класса CA, который множественно наследует IX и IY