- •Оглавление
- •От автора
- •Введение
- •Преимущества использования компонентов
- •Адаптация приложений
- •Библиотеки компонентов
- •Распределенные компоненты
- •Требования к компонентам
- •Динамическая компоновка
- •Инкапсуляция
- •Заключительные замечания о компонентах
- •Повторное использование архитектур приложений
- •Соглашения о кодировании
- •Законченный пример
- •Взаимодействие в обход интерфейсов
- •Детали реализации
- •Теория интерфейсов, часть 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
5 глава
Динамическая компоновка
Что же это получается? Еще в первой главе я говорил, как важна динамическая компоновка для построения системы из «кирпичиков». И вот мы добрались уже до пятой главы — и не только по-прежнему компонуем клиента с компонентом статически, но и располагаем их все время в одном и том же файле! На самом деле у меня были основательные причины отложить обсуждение динамической компоновки. Главная из них в том, что пока мы не реализовали полностью IUnknown, клиент был слишком сильно связан с компонентом.
Сначала компонент нельзя было изменить так, чтобы не потребовалось изменять и клиент. Затем при помощи QueryInterface мы перешли на следующий уровень абстракции и представили компонент как набор независимых интерфейсов. Раздробив компонент на интерфейсы, мы сделали первый шаг к тому, чтобы раздробить монолитное приложение. Затем нам понадобился способ управления временем жизни компонента. Подсчитывая ссылки на каждый интерфейс, клиент управляет их временем жизни, компонент же сам определяет, когда ему себя выгрузить. Теперь, когда мы реализовали IUnknown, клиент и компонент связаны не титановой цепью, а тонкой ниткой. Столь непрочная связь уже не мешает компоненту и клиенту изменяться, не задевая друг друга.
В этой главе мы попробуем поместить компонент в DLL. Обратите внимание — я не сказал, что мы собираемся сделать компонент DLL. Компонент — это не DLL, думать так значило бы слишком ограничивать концепцию компонента. DLL для компонента — сервер, или средство доставки. Компонент — это набор интерфейсов, которые реализованы в DLL. DLL — это грузовик, а компонент — груз.
Чтобы познакомиться с динамической компоновкой, мы посмотрим, как клиент создает компонент, содержащийся в DLL. Затем мы возьмем листинг 4-1 из гл. 4 и разобьем его на отдельные файлы для клиента и компонента. Разобрав полученный код, мы создадим три разных клиента и три разных компонента, использующие разные комбинации трех интерфейсов. Для чего все это? В качестве грандиозного финала мы сконструируем компанию клиентов и компонентов, где каждый сможет общаться с каждым.
Если Вы уже знакомы с DLL, то большая часть содержания этой главы Вам известна. Однако Вы можете полюбопытствовать, как я «растащу» клиент и компонент по разным файлам (раздел «Разбиваем монолит»). Надеюсь, Вам понравится сочетание разных клиентов и компонентов в разделе «Связки объектов» в конце главы.
Создание компонента
В этом разделе мы увидим, как компонент динамически компонуется клиентом. Мы начнем с клиента, создающего компонент. Это временная мера; в последующих главах мы увидим, как изолировать клиент от компонента еще сильнее.
Прежде чем запросить указатель на интерфейс, клиент должен загрузить DLL в свой процесс и создать компонент. В гл. 3 функция CreateInstance создавала компонент и возвращала клиенту указатель на интерфейс IUnknown. Это единственная функция в DLL, с которой клиент должен быть скомпонован явно. Ко всем прочим функциям компонента клиент может получить доступ через указатель на интерфейс. Таким образом, чтобы клиент мог вызывать функцию CreateInstance, ее надо экспортировать.
Экспорт функции из DLL
Экспорт функции из DLL осуществляется без проблем. Сначала необходимо обеспечить использование компоновки С (C linkage), пометив функцию как extern “C”. Например, функция CreateInstance в файле CMPNT1.CPP выгляди так:
//
// Функция создания
//
extern “C” IUnknown* CreateInstance()
{
IUnknown* pI = (IUnknown*)(void*)new CA; PI->AddRef();
66
return pI;
}
Слово extern “C” в описании нужно, чтобы компилятор С++ не «довешивал» к имени функции информацию о типе. Без extern “C” Microsoft Visual C++ 5.0 превратит CreateInstance в
?CreateInstance@@YAPAUIUnknown@@XZ
Другие компиляторы используют иные схемы дополнения имени информацией о типе. На дополненные имена нет стандарта, так что они не переносимы. Кроме того, работать с ними — изрядная морока.
Дамп экспортов
Если Вы пользуетесь Microsoft Visual C++, то при помощи DUMPBIN.EXE можете получить листинг символов, экспортированных из DLL. Следующая команда
dumpbin –exports Cmpnt1.dll
генерирует для CMPNT1.DLL такие результаты:
Microsoft (R) COFF Binary File Dumper Version 4.20.6281
Copyright (C) Microsoft Corp 1992-1996. All rights reserved.
Dump of file Cmpnt1.dll
File Type: DLL
Section contains the following Exports for Cmpnt1.dll
0 characteristics
325556C5 time date stamp Fri Oct 04 11:26:13 1996
0.00 version
1 ordinal base
1 number of functions
1 number of names
ordinal hint name
1 |
0 |
CreateInstance (00001028) |
Summary
7000 .data
1000 .idata
3000 .rdata
2000 .reloc
10000 .text
Конечно, чтобы экспортировать функцию, недостаточно пометить ее как extern “C”. Необходимо еще сообщить компоновщику, что функция экспортируется. Для этого надо создать надоедливый файл DEF. Файлы DEF так надоедливы потому, что очень легко позабыть внести в файл имя функции; если же Вы об этом забыли, компоновка этой функции будет невозможна. Из-за такой забывчивости я лишился изрядной части волос.
Создавать файлы DEF очень легко. Вы можете скопировать их из примеров и изменить несколько строк. DEF файл для CMPNT1.DLL показан в листинге 5-1.
CMPNT1.DEF
;
; Файл определения модуля для Cmpnt1
;
LIBRARY |
Cmpnt1.dll |
DESCRIPTION |
‘(c)1996-1997 Dale E. Rogerson’ |
EXPORTS
CreateInstance @1 PRIVATE
Листинг 5-1 В файле определения модуля перечислены функции, экспортированные динамически компонуемой библиотекой
67
Все, что Вам нужно сделать, — перечислить экспортируемые функции в разделе EXPORTS данного файла. При желании можно назначить каждой функции порядковый номер (ordinal number). В строке LIBRARY следует указать фактическое имя DLL.
Таковы основы экспорта функций из DLL. Теперь мы посмотрим, как загрузить DLL и обратиться к функции.
Загрузка DLL
Файлы CREATE.H и CREATE.CPP реализуют функцию CreateInstance. CreateInstance принимает имя DLL в качестве параметра, загружает эту DLL и пытается вызвать экспортированную функцию с именем CreateInstance. Соответствующий код показан в листинге 5-2.
CREATE.CPP
// |
|
// Create.cpp |
|
// |
|
#include <iostream.h> |
// Объявление IUnknown |
#include <unknwn.h> |
|
#include "Create.h" |
|
typedef IUnknown* (*CREATEFUNCPTR)();
IUnknown* CallCreateInstance(char* name)
{
// Загрузить в процесс динамическую библиотеку
HINSTANCE hComponent = ::LoadLibrary(name); if (hComponent == NULL)
{
cout << "CallCreateInstance:\tОшибка: Не могу загрузить компонент" << endl;
return NULL;
}
// Получить адрес функции CreateInstance CREATEFUNCPTR CreateInstance
= (CREATEFUNCPTR)::GetProcAddress(hComponent, "CreateInstance"); if (CreateInstance == NULL)
{
cout << "CallCreateInstance:\tОшибка: "
<<"Не могу найти функцию CreateInstance"
<<endl;
return NULL;
}
return CreateInstance();
}
Листинг 5-2 Используя LoadLibrary и GetProcAddress, клиент может динамически компоноваться с компонентом
Для загрузки DLL CreateInstance вызывает функцию Win32 LoadLibrary:
HINSTANCE LoadLibrary( |
// Имя файла DLL |
LPCTSTR lpLibFileName |
|
); |
|
LoadLibrary принимает в качестве параметра имя файла DLL и возвращает описатель загруженной DLL. Функция Win32 GetProcAddress принимает этот описатель и имя функции (CreateInstance), возвращая адрес последней:
FARPROC GetProcAddress( |
// Описатель модуля DLL |
HMODULE hModule, |
|
LPCSTR lpProcName |
// Имя функции |
} |
|
С помощью этих двух функций клиент может загрузить DLL в свое адресное пространство и получить адрес CreateInstance. Имея этот адрес, создать компонент и получить указатель на его IUnknown не составляет труда. CallCreateInstance приводит возвращенный указатель к типу, пригодному для использования, и, в соответствии со своим назначением, вызывает CreateInstance.