- •Оглавление
- •От автора
- •Введение
- •Преимущества использования компонентов
- •Адаптация приложений
- •Библиотеки компонентов
- •Распределенные компоненты
- •Требования к компонентам
- •Динамическая компоновка
- •Инкапсуляция
- •Заключительные замечания о компонентах
- •Повторное использование архитектур приложений
- •Соглашения о кодировании
- •Законченный пример
- •Взаимодействие в обход интерфейсов
- •Детали реализации
- •Теория интерфейсов, часть 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
224
библиотеки типа нам необходимы операторы coclass и library. Операторы coclass, описывающие компоненты, помещены в отдельные файлы IDL, каждый из которых помечен суффиксом _C. Эти файлы импортируют файлы _I для используемых ими интерфейсов.
Этот подход отличает большая гибкость. Однако из каждого файла IDL получается несколько других файлов, так что их размножение сбивает с толку. Следующее соображение поможет Вам не путаться. Считайте, что _C означает CLSID, а _I — IID. Если Ваш код использует IID, необходимо включить соответствующий заголовочный файл _I. Например, IID_ITangramModel определен в MODEL_I.IDL. Если я запрашиваю IID_ITangramModel, то должен включить MODEL_I.H и скомпоновать с MODEL_I.C.
Если я создаю компонент TangramModel, мне нужен CLSID_TangramModel. Этот компонент описан в MODEL_C.IDL. Следовательно, нужно включить MODEL_C.H и скомпоновать с MODEL_C.C. Если файл IDL импортирует другой файл IDL, то в код на С++ необходимо включить заголовок для импортированного файла. Например, MODEL_I.IDL импортирует EVENTS_I.IDL. Поэтому, если Вы включаете MODEL_I.H, то нужно также включить и EVENTS_I.H.
Замечу, что суффиксы _I и _C — это мое личное соглашение. Вы можете называть эти файлы как угодно. Без суффиксов я всегда путал, что где находится. Теперь, если компилятор говорит, что не может найти CLSID, я уже знаю, что мне нужно включить и скомпоновать с файлом _C.
Файл DLLDATA.C
Компилятор MIDL не всегда генерирует новую версию DLLDATA.C. Во многих случаях Вам может потребоваться одна DLL заместителя/заглушки, которая поддерживает несколько интерфейсов. Однако эти интерфейсы определены в разных файлах IDL. Если компилятор MIDL находит существующий файл DLLDATA.C, он добавляет новые интерфейсы, а не создает новый файл. Поэтому следует периодически проверять DLLDATA.C, чтобы убедиться, что там присутствуют только те интерфейсы, которые Вам нужны.
Циклический подсчет ссылок
Когда компонент TangramGdiWorld создает компонент TangramGdiVisual, первый передает последнему указатель на интерфейс ITangramGdiWorld. TangramGdiVisual использует этот интерфейс для преобразования в экранные координаты. К сожалению, при этом создается циклическая ссылка (рис. 13-4). TangramGdiWorld указывает на
TangramGdiVisual, который указывает обратно на TangramGdiWorld.
Циклические ссылки не очень подходят для подсчета ссылок, так как результатом циклических ссылок могут быть компоненты, которые никогда не освобождаются из памяти. Например, TangramGdiWorld создает TangramGdiVisual и получает интерфейс ITangramGdiVisual, для которого вызывает AddRef. Кроме того,
TangramGdiWorld передает указатель на свой интерфейс ITangramGdiWorld компоненту TangramGdiVisual,
который также вызывает для этого указателя AddRef. Теперь счетчик ссылок как компонента TangramGdiVisual, так и компонента TangramGdiWorld равен как миниму единице.
TangramGdiWorld |
TangramGdiVisual |
Список указателей на образы |
ITangramGdiVisual |
ITangramGdiWorld |
m_pGdiWorld |
Рис. 13-4 Циклические ссылки в программе Tangram
Далее TangramGdiWorld освобождает ITangramGdiVisual в своем деструкторе, который вызывается, когда счетчик ссылок станет равным 0. Но TangramGdiVisual имеет указатель на интерфейс ITangramGdiWorld компонента TangramGdiWorld, и не освобождает этот интерфейс, пока его счетчик ссылок не станет равным 0. Результатом является взаимный захват, или «клинч». TangramGdiWorld не освободит TangramGdiVisual до тех пор, пока TangramGdiVisual не освободит TangramGdiWorld. TangramGdiVisual не менее упрям и не желает освобождать указатель, пока это не сделает TangramGdiWorld. Это очень похоже на двух баранов на мосту, ни один из которых не желает посторониться и пропустить другого.
Вы можете выбрать одно из трех решений этой проблемы: не вызывать AddRef, явно удалять компонент или использовать другой компонент.
Не вызывайте AddRef
Первое решение — самое простое. Не увеличивайте счетчик ссылок одного из интерфейсов в ссылочном цикле. Так поступает компонент TangramGdiVisual. Он не вызывает AddRef для указателя на интерфейс
ITangramGdiWorld, полученного от TangramGdiWorld. TangramGdiVisual известно, что его время существования
225
находится внутри времени существования TangramGdiWorld, и поэтому, пока он существует, обратный указатель правилен.
Эта техника используется достаточно часто, чтобы получить собственное имя. Ссылка на интерфейс, счетчик ссылок которого не был увеличен, называется слабой ссылкой (weak reference). Наличие слабой ссылки не удерживает компонент в памяти. Сильная ссылка (string reference) — это ссылка, вызывающая увеличение счетчика ссылок. Такая ссылка удерживает компонент в памяти (рис. 13.5).
Хотя этот метод проще всего, не всегда его можно использовать. Компоненту, имеющему слабую ссылку на другой компонент, необходимо знать, когда такая ссылка становится недействительной. TangramGdiVisual это не волнует, так как его время существования вложено во время существования TangramGdiWorld. Но если времена существования компонентов не вложены, необходим другой способ определять, что ссылка стала недействительной.
TangramGdiWorld |
TangramGdiVisual |
Список указателей на образы |
ITangramGdiVisual |
ITangramGdiWorld |
m_pGdiWorld |
Рис. 13-5 TangramGdiVisual поддерживает слабую ссылку на TangramGdiWorld. Слабая ссылка изображена пунктирной линией.
Используйте явное удаление
Другой способ избежать захвата — предоставить одному из компонентов (или обоим) способ явно удалять другой компонент. Вместо того, чтобы ждать пока счетчик компонента станет равным 0, один из компонентов должен уметь приказать другому освободить все имеющиеся у того указатели на интерфейсы. Для этого нужно просто создать новый интерфейс с функцией, которая удаляет компонент (рис. 13-6).
TangramGdiWorld |
|
TangramGdiVisual |
Список указателей на образы |
|
ITangramGdiVisual |
Явный разрыв ссылки |
Ликвидировать |
ILifeTime |
на этот компонент |
|
|
|
|
|
ITangramGdiWorld |
|
m_pGdiWorld |
Рис. 13-6 Ссылочный цикл можно разорвать с помощью отдельной функции, которая заставляет компонент освободить имеющиеся у него указатели, прежде чем его собственный счетчик ссылок достигнет 0
Но здесь следует быть осторожным. В реальной программе компонент, который Вы явно удаляете, может быть все еще кому-то нужен. Поэтому хорошей идеей будет реализовать еще один счетчик ссылок для истинно сильных ссылок, помимо традиционного. Пример счетчика истинно сильных ссылок — IClassFactory::LockServer. Подробнее об этом см. гл. 7 и 10. Другими примерами истинно сильных ссылок являются
IOleContainer::LockContainer и IExternalConnection::AddConnection. Эти функции предоставляют клиентам способ явно управлять временем существования компонентов.
Используйте отдельный компонент
Другой способ разорвать ссылочный цикл — использовать отдельный объект или подкомпонент, на который указывает один из компонентов в цикле. Этот подкомпонент поддерживает слабую ссылку на свой внешний объект. Схематически это показано на рис. 13-7. Здесь TangramGdiWorld управляет временем жизни TangramGdiVisual, который управляет временем жизни подкомпонента TangramGdiWorld.
Подкомпоненты — самый гибкий способ избежать циклического подсчета ссылок. Вам не нужен доступ к исходному тексту или дополнительные сведения о компонентах; не нужно ничего, кроме поддерживаемого Вами интерфейса, чтобы реализовать подкомпоненты. Вставка компонента со слабой ссылкой может устранить ссылочный цикл.
TangramGdiWorld и TangramGdiVisual не используют подкомпонент для устранения ссылочного цикла. TangramGdiVisual сам выступает в качестве подкомпонента и поддерживает на TangramGdiWorld слабую ссылку. В то же время TangramGdiVisual и TangramModel используют подкомпонент во избежание циклических ссылок при реализации точек подключения. Подробнее мы поговорим об этом в следующем разделе.