- •Оглавление
- •От автора
- •Введение
- •Преимущества использования компонентов
- •Адаптация приложений
- •Библиотеки компонентов
- •Распределенные компоненты
- •Требования к компонентам
- •Динамическая компоновка
- •Инкапсуляция
- •Заключительные замечания о компонентах
- •Повторное использование архитектур приложений
- •Соглашения о кодировании
- •Законченный пример
- •Взаимодействие в обход интерфейсов
- •Детали реализации
- •Теория интерфейсов, часть 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
168
Регистрация DLL заместителя/заглушки
Обратите внимание, что код make-файла определяет символ REGISTER_PROXY_DLL при компиляции файлов DLLDATA.C и PROXY.C. В результате генерируется код, позволяющий DLL заместителя/заглушки выполнять саморегистрацию. Затем, после компоновки DLL заместителя, make-файл регистрирует ее. Тем самым гарантируется, что Вы не забудете зарегистрировать DLL заместителя. Если бы Вы забыли это сделать, то несколько часов удивлялись бы, отчего вдруг не работает программа. Я это испытал.
Что именно DLL заместителя/заглушки помещает в Реестр? Давайте рассмотрим наш пример. Убедитесь, что Вы скомпоновали программу; код make-файла автоматически регистрирует заместитель и сервер, так что Вам делать это нет необходимости. Или же запустите файл REGISTER.BAT для регистрации скомпилированной ранее версии программы.
Теперь давайте запустим старый верный REGEDIT.EXE и посмотрим на раздел Реестра:
HKEY_CLASSES_ROOT\
Interface\
{32BB8323-B41B-11CF-A6BB-0080C7B2D682}
Приведенный выше GUID — это IID интерфейса IX. В этом разделе содержится несколько записей. Самая для нас интересная — ProxyStubClsid32. В этом разделе содержится CLSID DLL заместителя/заглушки интерфейса; для интерфейсов IX, IY и IZ он совпадает. Если найдете этот CLSID в разделе HKEY_CLASSES_ROOT\CLSID, там можно обнаружить и подраздел InprocServer32, который указывает на PROXY.DLL. Как видите, интерфейсы регистрируются независимо от реализующих их компонентов (рис. 10-5).
HKEY_CLASSES_ROOT
CLSID |
|
{32BB8323-B41B-11CF-A6BB-0080C7B2D682} |
PSFactoryBuffer |
InprocServer32 |
C:\Chap10\proxy.dll |
Interface |
|
{32BB8323-B41B-11CF-A6BB-0080C7B2D682} |
IX |
|
ProxyStubClsid32 |
{32BB8323-B41B-11CF-A6BB-0080C7B2D682} |
Рис. 10-5 Структура информации, добавляемой в Реестр кодом заместителя/заглушки, сгенерированным MIDL
При помощи MIDL мы можем вызывать функции и выполнять маршалинг параметров через границы процессов — и все будет выглядеть так же, как и при вызове компонента внутри процесса.
Реализация локального сервера
Теперь пришло время рассмотреть изменения в CFactory, необходимые для поддержки серверов вне процесса. Всякий раз, пересекая границу, Вы должны быть готовы изменить свои привычки и поведение, чтобы соответствовать местным обычаям. Точно так же обслуживание компонента из EXE отличается от обслуживания компонента из DLL. Поэтому мы должны изменить CFactory, чтобы она обслуживала как компоненты в DLL, так и компоненты в EXE. Мы также внесем небольшие изменения в CUnknown. Однако код самих компонентов останется тем же самым.
В коде используется символ _OUTPROC_SERVER_, помечающий фрагменты, специфичные для локальных серверов (когда символ определен) или для серверов внутри процесса (когда он не определен). Прежде чем перейти к рассмотрению изменений в CFactory, давайте запустим пример программы.
169
Работа примера программы
При запуске клиент запросит Вас, хотите ли Вы использовать версию компонента для сервера внутри или вне процесса. Для подключения к компоненту внутри процесса клиент использует CLSCTX_INPROC_SERVER, а для подключения к компоненту вне процесса — CLSCTX_LOCAL_SERVER.
Если Вы решите использовать компонент, реализованный сервером внутри процесса, то все будет работать в точности, как в предыдущей главе. Однако если Вы выберете сервер вне процесса, программа будет работать несколько иначе. Первое, что Вы заметите, — вывод на экран теперь идет только от клиента. Это связано с тем, что компонент в другом процессе использует не то консольное окно, что клиент.
Вместо того, чтобы просто запустить клиент, сначала запустим сервер из командной строки. Дважды щелкните значок SERVER.EXE или воспользуйтесь командой start:
C:\>start server
Сервер начнет выполняться, и на экране появится его окно. Теперь запустите клиент и прикажите ему подключиться к локальному серверу. Клиент будет посылать сой вывод в новое консольное окно, а вывод локального сервера пойдет в его собственное окно.
Нет точек входа
Давайте теперь демистифицируем поведение этого примера. EXE не могут экспортировать функции. Наши серверы внутри процесса зависелт от наличия следующих экспортированных функций:
DllCanUnloadNow
DllRegisterServer
DllUnregisterServer
DllGetClassObject
Теперь нам нужна замена для этих функций. Заменить DllCanUnloadNow легко. EXE, в отличие от DLL, не является пассивным модулем — он управляет своей жизнью сам. EXE может отслеживать счетчик блокировок и , когда тот станет равным 0, выгрузить себя. Следовательно, для EXE нет необходимости реализовывать DllCanUnloadNow. Вычеркиваем ее из списка.
Следующие две функции — DllRegisterServer и DllUnregisterServer — заменить почти так же просто. EXE поддерживают саморегистрацию путем обработки параметров командной строки RegServer и UnRegServer. Все, что должен сделать наш локальный сервер, — это при получении соответствующего параметра командной строки вызвать CFactory::RegisterAll или CFactory::UnregisterAll. Пример кода, выполняющего эти действия, можно найти в файле OUTPROC.CPP. (Попутно замечу, что локальный сервер регистрирует местоположение своего EXE в разделе LocalServer32, а не в разделе InprocServer32. Вы можете заметить соответствующее изменение в файле REGISTRY.CPP.)
Таким образом, у нас осталась только DllClassObject, заменить которую несколько труднее, чем остальные функции, экспортируемые DLL.
Запуск фабрик класса
Возвращаясь к гл. 7, вспомните, что CoCreateInstance вызывает CoGetClassObject, которая вызывает DllGelClassObject. Последняя возвращает указатель на IClassFactory, который используется для создания компонента. Поскольку EXE не могут экспортировать DllGetClassObject, нужен другой способ передачи
CoGetClassObject нашего указателя на IClassFactory.
Решение, предлагаемое СОМ, — поддержка внутренней таблицы зарегистрированных фабрик класса. Когда клиент вызывает CoGetClassObject с соответствующими параметрами, СОМ сначала просматривает свою внутреннюю таблицу фабрик класса, ища заданный клиентом CLSID. Если фабрика класса в таблице отсутствует, то СОМ обращается к Реестру и запускает соответствующий модуль EXE. Задача последнего — как можно скорее зарегистрировать свои фабрики класса, чтобы их могла найти СОМ. Для регистрации фабрики класса EXE использует функцию СОМ CoRegisterClassObject. При запуске EXE обязан зарегистрировать все поддерживаемые им фабрики. Я добавил в CFactory новую стратегическую функцию-член StartFactories, которая вызывает CoRegisterClassObject для каждого компонента в массиве структур CFactoryData. Код этой функции приведен ниже.
BOOL CFactory::StartFactories()
{
CFactoryData* pStart = &g_FactoryDataArray[0]; const CFactoryData* pEnd =
&g_FactoryDataArray[g_cFactoryDataEntries – 1];
170
for(CFactoryData* pData = pStart; pData <= pEnd; pData++)
{
//Инициализировать указатель и признак фабрики класса pData->m_pIClassFactory = NULL;
pData->m_dwRegister = NULL;
//Создать фабрику класса для компонента
IClassFactory* pIFactory = new CFactory(pData);
//Зарегистрировать фабрику класса
DWORD dwRegister;
HRESULT hr = ::CoRegisterClassObject( *pData->m_pCLSID, static_cast<IUnknown*>(pIFactory), CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE,
&dwRegister);
if (FAILED(hr))
{
pIFactory->Release(); return FALSE;
}
// Запомнить информацию pData->m_pIClassFactory = pIFactory; pData->m_dwRegister = dwRegister;
}
return TRUE;
}
Данный код использует две новых переменных-члена, которые я добавил в класс CfactoryData. Переменная m_pIClassFactory содержит указатель на работающую фабрику класса для CLSID, хранящегося в m_pCLSID. Переменная m_dwRegister содержит магический признак (cookie)1 для данной фабрики.
Как видите, для регистрации фабрики класса нужно лишь ее создать и передать указатель на ее интерфейс функции CoRegisterClassObject. Значение большинства параметров CoRegisterClassObject легко понять из приведенного выше кода. Сначала идет ссылка на CLSID регистрируемого класса, за которой следует указатель на фабрику класса. Магический признак возвращается через последний параметр; он используется для отзыва фабрики класса с помощью функции CoRevokeClassObject. Третий и четвертый параметр — это флажки,
управляющие поведением CoRegisterClassObject.
Флажки для CoRegisterClassObject
Третий и четвертый параметр этой функции используются вместе, и смысл одного изменяется в зависимости от значения другого. В результате интерпретация становиться весьма запутанной.
Четвертый параметр указывает, может ли один экземпляр данного EXE обслуживать более одного экземпляра соответствующего компонента. Проще всего это понять, сравнив сервер EXE с приложением SDI (single document interface — однодокументный интерфейс). Для загрузки нескольких документов необходимо запустить несколько экземпляров такого приложения, тогда как один экземпляр приложения MDI (multiple document interface — многодокументный интерфейс) может открыть несколько документов. Если Ваш сервер EXE похож на приложение SDI, в том смысле, что он может обслуживать только один компонент, следует задать
REGCLS_SINGLEUSE и CLSCTX_LOCAL_SERVER.
Если сервер EXE может поддерживать несколько экземпляров компонента, подобно тому, как приложение MDI может открыть несколько документов, используйте REGCLS_MULTI_SEPARATE:
hr = ::CoRegisterClassObject(clsid, pUnknown, CLSCTX_LOCAL_SERVER, REGCLS_MULTI_SEPARATE,
&dwRegister);
1 Кто-то сказал мне, что «cookie» — это не термин информатики, а термин Microsoft. Я не знаю, что это такое, особенно учитывая, что большинство программ просмотра Web оставляют на вашем жестком диске файлы-«cookie». Как бы то ни было, мы в Microsoft используем этот термин для обозначения структуры данных, которая что-либо идентифицирует. Клиент запрашивает у сервера ресурс. Сервер выдает ресурс и возвращает клиенту признак («cookie»), который клиент может в дальнейшем использовать для ссылки на этот ресурс. С точки зрения клиента, «cookie» — это случайное число, смысл которого известен только серверу.
171
Возникает интересная ситуация. Предположим, что наш EXE-модуль зарегистрировал несколько компонентов. Пусть, кроме того, этому EXE необходимо использовать один из зарегистрированных им компонентов. Если соответствующая фабрика класса зарегистрирована с помощью приведенного выше оператора, то для обслуживания компонента будет запущен еще один экземпляр EXE. Очевидно, что в большинстве случаев это не столь эффективно, как мы бы хотели. Для регистрации сервера EXE как сервера своих собственных компонентов внутри процесса, объедините, как показано ниже, флаг CLSCTX_LOCAL_SERVER с флагом
CLSCTX_INPROG_SERVER:
hr = ::CoRegisterClassObject(clsid, pUnknow, CLSCTX_LOCAL_SERVER | CLSCTX_INPROC_SERVER, REGCLS_MULTI_SEPARATE,
&dwRegister);
В результате объединения флажков сервер EXE сможет самостоятельно обслуживать свои компоненты. Поскольку данный случай наиболее распространен, для автоматического включения CLSCTX_INPROC_SERVER при заданном CLSCTX_LOCAL_SERVER используется специальный флаг REGCLS_MULTIPLEUSE. Ниже приведен эквивалент предыдущего вызова:
hr = ::CoRegisterClassObject(clsid, pUnknown, CLS_LOCAL_SERVER,
REGCLS_MULTIPLEUSE, &dwRegister);
изменив пример программы, можно увидеть различие между REGCLS_MULTIPLEUSE и REGCLS_MULTI_SEPARATE. Сначала удалите информацию сервера внутри процесса из Реестра следующей командой:
regsvr32 /u server.dll
Это гарантирует, что единственным доступным сервером будет локальный. Затем запустите клиент и выберите второй вариант для активации локального сервера. Локальный сервер будет прекрасно работать. Обратите внимание, что в функциях Unit в файлах CMPNT1.CPP и CMPNT2.CPP мы создаем компонент, используя CLSCTX_INPROC_SERVER, — но ведь мы только что удалили информацию сервера внутри процесса из Реестра! Следовательно, наш EXE сам предоставляет себе внутрипроцессные версии этих компонентов.
Теперь заменим REGCLS_MULTIPLEUSE на REGCLS_MULTU_SEPARATE и CFactory::StartFactories. (Строки,
которые нужно изменить, помечены в CFACTORY.CPP символами @Multi.) Скомпонуйте клиент и сервер заново, запустите клиент и выберите второй вариант. Вызов создания компонента потерпит неудачу, так как создания внутренних компонентов нет сервера внутри процесса, а REGCLS_MULTI_SEPARATE заставляет СОМ отвергать попытки сервера самостоятельно обслуживать компоненты внутри процесса.
Остановка фабрик класса
Когда работа сервера завершается, фабрики класса следует удалить из внутренней таблицы СОМ. Это выполняется при помощи функции библиотеки СОМ CoRevokeClassObject. Метод StopFactories класса CFactory вызывает CoRevokeClassObject для всех поддерживаемых данных EXE фабрик класса:
void CFactory::StopFactories()
{
CFactoryData* pStart = &g_FactoryDataArray[0]; const CFactoryData* pEnd =
&g_FactoryDataArray[g_cFactoryDataEntries – 1];
for (CFactoryData* pData = pStart; pData <= pEnd; pData++)
{
// Прекратить работу фабрики класса с помощью магического признака.
DWORD dwRegister = pData->m_dwRegister; if (dwRegister != 0)
{
::CoRevokeClassObject(dwRegister);
}
//Освободить фабрику класса.
IClassFactory* pIFactory = pData->m_pIClassFactory; if (pIfactory != NULL)
{
pIFactory->Release();
}
}
}