- •Оглавление
- •От автора
- •Введение
- •Преимущества использования компонентов
- •Адаптация приложений
- •Библиотеки компонентов
- •Распределенные компоненты
- •Требования к компонентам
- •Динамическая компоновка
- •Инкапсуляция
- •Заключительные замечания о компонентах
- •Повторное использование архитектур приложений
- •Соглашения о кодировании
- •Законченный пример
- •Взаимодействие в обход интерфейсов
- •Детали реализации
- •Теория интерфейсов, часть 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
109
интерфейса и передавать вызовы внутреннему компоненту (рис. 8-3). Однако внешний компонент не может специализировать какие-либо функции интерфейса. После того, как внешний компонент передаст интерфейс клиенту, тот обращается к внутреннему компоненту самостоятельно. Клиент не должен знать, что он работает с двумя разными компонентами, так как это нарушит инкапсуляцию. Задача агрегирования — заставить внешний и внутренний компоненты вести себя как один компонент. Как Вы увидите далее, эта возможность достигается при помощи QueryInterface.
Внешний компонент
IX
Внутренний компонент
IY
Рис. 8-3 Когда внешний компонент агрегирует интерфейс, он передает укаатель на него непосредственно клиенту. Он не реализуется интерфейс заново для передачи вызовов внутреннему компоненту.
Сравнение включения и агрегирования
Небольшой пример пояснит различия между включением и агрегированием. Предположим, что Вы хозяин небольшой металлоремонтной мастерской. У Вас есть две работницы — Памела и Анжела. Памела работает уже давно и знает свое дело досконально. Если заказ будет выполнять Памела, достаточно просто направить клиента к ней. Анжела, напротив, новичок и у нее нет опыта работы с металлом. Когда работу поручают ей, обычно нужно дать начальные указания, а иногда и сделать вместо нее наиболее сложную часть. Вы же должны и договариваться с клиентом. После того, как Анжела закончит работу, Вы вместе с ней должны проверить результаты и, может быть, что-то подправить. Случай Памелы сходен с подходом агрегирования. Дав работу, Вы уходите со сцены. Однако в случае Анжелы Вы по-прежнему ведете все переговоры с клиентом, даете начальные указания и проверяете работу в конце. Этот вариант аналогичен включению.
На каждый агрегируемый Вами интерфейс будут, вероятно, приходиться сотни включаемых. Помните, всякий раз, когда компонент выступает в роли клиента и использует интерфейс, принадлежащий другому компоненту, первый, в некотором смысле, включает второй. С другой стороны, агрегирование — гораздо более специальный случай. Его используют тогда, когда некоторый компонент уже реализует некоторый интерфейс именно так, как это нужно Вам, и Вы передаете интерфейс компонента клиенту.
Возможны различные сочетания включения и агрегирования. Компонент может специализировать или расширять множество интерфейсов, реализованных многими разными компонентами. Некоторые интерфейсы он может включать, другие же агрегировать.
Поскольку агрегирование — это особый случай включения, мы рассмотрим включение первым.
Реализация включения
Включение компонента выполняется столь же просто, как и использование. Каталог \CHAP08\CONTAIN на прилагающемся к книге диске содержит пример кода включения. В этом примере Компонент 1 — внешний; он реализует два интерфейса: IX и IY. При этом он использует реализацию IY Компонентом 2 — внутренним, включаемым компонентом. Это в точности соответствует схеме рис. 8-2.
Клиент и внутренний компонент — практически те же самые, что и клиент с компонентом из предыдущей главы. При включении одного компонента другим ни от клиента, ни от внутреннего компонента не требуется никаких специальных действий. Они даже не знают о самом факте использования включения.
Нам остается рассмотреть только внешний компонент — Компонент 1, который включает Компонент 2. В приведенном ниже листинге 8-1 показано объявление и большая часть реализации Компонента 1. Я выделил полужирным некоторые участки кода, относящиеся к включению. Новая переменная-член m_pIY содержит указатель на интерфейс IY включаемого Компонента 2.
Код включения из CONTAIN\CMPNT1
///////////////////////////////////////////////////////////
//
// Компонент 1
//
class CA : public IX, public IY
{
110
public: |
|
|
// IUnknown |
|
|
virtual HRESULT |
__stdcall QueryInterface(const IID& iid, void** ppv); |
|
virtual ULONG |
__stdcall |
AddRef(); |
virtual ULONG |
__stdcall |
Release(); |
// Интерфейс IX
virtual void __stdcall Fx() { cout << "Fx" << endl; }
// Интерфейс IY
virtual void __stdcall Fy() { m_pIY->Fy(); }
//Конструктор
CA();
//Деструктор
~CA();
//Функция инициализации, вызываемая фабрикой класса для
//создания включаемого компонента
HRESULT __stdcall Init();
private:
//Счетчик ссылок long m_cRef;
//Указатель на интерфейс IY включаемого компонента
IY* m_pIY;
};
//
// Конструктор
//
CA::CA() : m_cRef(1), m_pIY(NULL)
{
::InterlockedIncrement(&g_cComponents);
}
//
// Деструктор
//
CA::~CA()
{
::InterlockedDecrement(&g_cComponents); trace("Самоликвидация");
// Освободить включаемый компонент if (m_pIY != NULL)
{
m_pIY->Release();
}
}
// Инициализация компонента путем создания включаемого компонента
HRESULT __stdcall CA::Init()
{
trace("Создать включаемый компонент");
HRESULT hr = ::CoCreateInstance(CLSID_Component2, NULL, CLSCTX_INPROC_SERVER, IID_IY, (void**)&m_pIY);
if (FAILED(hr))
{
trace("Не могу создать включаемый компонент"); return E_FAIL;
}
else
{
111
return S_OK;
}
}
Листинг 8-1 Компонент 1 создает экземпляр включаемого компонента и хранит указатель на интерфейс IY последнего
Давайте рассмотрим, как работает этот код внешнего Компонента 1. Новый метод под названием Init создает внутренний Компонент 2 тем же самым способом, которым создают компоненты все клиенты, — посредством вызова CoCreateInstance. При этом внешний компонент запрашивает указатель на IY у внутреннего и, в случае успеха, сохраняет его в m_pIY.
В приведенном листинге не показаны реализация QueryInterface и функций внешнего IUnknown. Она абсолютно та же, что и в случае, когда включение не используется. Когда клиент запрашивает у Компонента 1 интерфейс IY, тот возвращает указатель на свой интерфейс. Затем, когда клиент вызывает метод этого интерфейса, Компонент 1 передает вызов Компоненту 2. Это выполняет следующая строка:
virtual void Fy() { m_pIY->Fy(); }
Когда Компонент 1 самоликвидируется, его деструктор вызывает Release для указателя m_pIY, в результате чего Компонент 2 также удаляет себя.
Фабрика класса Компонента 1 мало изменилась по сравнению с фабрикой класса из предыдущей главы. Единственный новый момент — то, что функция CreateInstance вызывает после создания Компонента 1 его функцию Init. Код этой функции приведен в листинге 8-2.
Код функции CreateInstance из CONTAIN\CMPNT1
HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter, const IID& iid,
void** ppv)
{
// Агрегирование не поддерживается if (pUnknownOuter != NULL)
{
return CLASS_E_NOAGGREGATION;
}
// Создать компонент
CA* pA = new CA; if (pA == NULL)
{
return E_OUTOFMEMORY;
}
// Инициализировать компонент
HRESULT hr = pA->Init(); if (FAILED(hr))
{
// Ошибка при инициализации. Удалить компонент pA->Release();
return hr;
}
// Получить запрошенный интерфейс hr = pA->QueryInterface(iid, ppv); pA->Release();
return hr;
}
Листинг 8-2 Фабрика класса внешнего компонента вызывает для вновь созданного компонента функцию Init
Вот и все, что необходимо для реализации включения. Теперь рассмотрим, для чего включение может применяться.
Расширение интерфейсов
Одно из основных применений включения — расширение интерфейса посредством добавления кода к существующему интерфейсу.
Рассмотрим пример. Имеется класс IAirplane (Самолет), который Вы хотите превратить в IFloatPlane (Гидросамолет). Определения интерфейсов приводятся ниже:
interface IAirplane : IUnknown