- •Оглавление
- •От автора
- •Введение
- •Преимущества использования компонентов
- •Адаптация приложений
- •Библиотеки компонентов
- •Распределенные компоненты
- •Требования к компонентам
- •Динамическая компоновка
- •Инкапсуляция
- •Заключительные замечания о компонентах
- •Повторное использование архитектур приложений
- •Соглашения о кодировании
- •Законченный пример
- •Взаимодействие в обход интерфейсов
- •Детали реализации
- •Теория интерфейсов, часть 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
10 глава
Серверы в EXE
В последний раз я был в Берлине еще до падения стены. Когда, покидая американский сектор у пропускного поста «Чекпойнт Чарли», я въезжал в Восточный Берлин, не возникало сомнений, что здесь проходит граница. Колючая проволока, автоматчики и минные поля делали ее весьма отчетливой. Но и за оборонительной линией отличия были очевидны: с восточной стороны от стены двухлитровые малолитражки изрыгали густой дым, а возле магазинов стояли длинные очереди.
Изменения ждут нас при всяком переходе границы, неважно, сколь мало отличается одна сторона от другой. Эта глава посвящена пересечению границ — главным образом, границ между разными процессами. Мы рассмотрим также пересечение границ между машинами.
Почему нам нужно выходить за границу процесса? Потому, что в некоторых случаях предпочтительнее реализовать компонент в EXE, а не в DLL. Одной из причин может стать то, что Ваше приложение уже реализовано в EXE. После небольшой доработки можно сделать доступными сервисы приложения, так что клиенты смогут автоматизировать его использование.
Если компонент и клиент находятся в разных EXE, они будут расположены и в отдельных процессах, поскольку для каждого EXE-модуля создается свой процесс. При передаче информации между такими компонентом и клиентом необходимо пересечь границу между процессами. По счастью, при этом нет нужды изменять код компонента, хотя некоторые изменения в класс CFactory, представленный в предыдущей главе, внести все же придется. Однако, прежде чем перейти к реализации, следует рассмотреть проблемы и решения, связанные с обращением к интерфейсам СОМ через границы процессов.
Разные процессы
Каждый модуль EXE исполняется в отдельном процессе. У каждого процесса есть свое адресное пространство. Логический адрес 0x0000ABBA в двух разных процессах ссылается на два разных места в физической памяти. Если один процесс передаст этот адрес другому, второй будет работать не с тем участком памяти, который предполагался первым процессом (рис. 10-1).
В то время как каждому EXE-модулю соответствует свой процесс, DLL проецируется в процесс того EXE, с которым они скомпонованы. По этой причине DLL называют серверами внутри процесса (in process), а EXE — серверами вне процесса (out of process). Иногда EXE также называют локальными серверами, чтобы отличить их от другого вида серверов вне процесса — удаленных серверов. Удаленный сервер — это сервер вне процесса, работающий на другой машине.
|
|
|
Адресное пространство |
Физическая память |
||||||||
|
Процесс 1 |
|
|
|
процесса 1 |
|||||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pFoo |
|
|
|
|
0x0000ABBA |
|
|
|
|
0x00001234 |
|
|
|
|
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Адресное пространство |
|
|
|
||||||
|
Процесс 2 |
|
|
|
процесса 2 |
|
|
|
||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pFoo |
|
|
|
|
0x0000ABBA |
|
|
|
|
0x0BAD0ADD |
|
|
|
|
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
Рис. 10-1 Один и тот же адрес в двух разных процессах ссылается на два разных участка физической памяти
В гл. 5 мы говорили, как важно то, что компонент и клиент используют общее адресное пространство. Компонент передает клиенту интерфейс. Интерфейс — это, по существу, массив указателей функций. Клиент должен иметь доступ к памяти, занимаемой интерфейсом. Если компонент находится в DLL, то доступ осуществляется легко: и компонент, и клиент находятся в одном адресном пространстве. Но если компонент и клиент находятся в разных адресных пространствах, то у клиента нет доступа к памяти процесса компонента. Если у клиента нет даже
160
доступа к памяти, связанной с интерфейсом, то он не сможет вызывать и функции этого интерфейса. В такой ситуации наши интерфейсы стали бы совершенно бесполезны.
Для того, чтобы с интерфейсом можно было работать через границы процесса, необходимо следующее: !" Процесс должен иметь возможность вызвать функцию в другом процессе.
!" Процесс должен иметь возможность передавать другому процессу данные.
!" Клиент не должен беспокоиться о том, является ли компонент сервером внутри или вне процесса.
Локальный вызов процедуры
Есть много методов межпроцессной коммуникации, включая DDE, именованные каналы и разделяемую память. Однако СОМ использует локальный вызов процедуры (local procedure call, LPC). LPC — это средство связи между разными процессами на одной и той же машине. LPC представляет собой специализированное средство связи между разными процессами в пределах одной машины, построенное на основе удаленного вызова процедуры (remote procedure call, RPC) (см. рис. 10-2).
Стандарт RPC определен OSF (Open Software Foundation) в спецификации DCE (Distributed Computing Environment) RPC. RPC обеспечивает коммуникацию между процессами на разных машинах с помощью разнообразных сетевых протоколов. Распределенная модель СОМ (DCOM), которую мы будем рассматривать далее в этой главе, использует RPC для связи по сети.
Как работает RPC? Как по волшебству. На самом деле волшебства, конечно, нет, но есть кое-что, что лишь немногим хуже, — реализация вызовов операционной системой. Операционной системе известны физические адреса, соответствующие логическому адресному пространству каждого процесса; следовательно, операционная система может вызывать функции внутри любого процесса.
EXE
Клиент
Граница процесса
EXE
Компонент
Локальный вызов процедуры
Рис. 10-2 Клиент в EXE использует механизм Win32 LPC для вызова функций компонента, реализованного в другом EXE
Маршалинг
Вызвать функцию EXE — это только полдела. Нам по-прежнему необходимо передать параметры функции из адресного пространства клиента в адресное пространство компонента. Этот процесс называется маршалингом (marshaling). В соответствии с моим словарем, глагол marshal значит «располагать, размещать или устанавливать в определенном порядке». Это слово должно быть в нашем ближайшем диктанте.
Если оба процесса находятся на одной машине, маршалинг выполняется просто. Данные одного процесса необходимо скопировать в адресное пространство другого процесса. Если процессы находятся на разных машинах, то данные необходимо преобразовать в стандартный формат, учитывающий межмашинные различия, например, порядок следования байтов в слове.
Механизм LPC способен скопировать данные из одного процесса в другой. Но для выборки параметров и посылки их в другой процесс ему требуется больше информации, чем содержится в заголовочном файле С++. Например, указатели на структуры следует обрабатывать иначе, чем целые числа. Маршалинг указателя включает в себя копирование в другой процесс структуры, на которую указатель ссылается. Однако, если указатель — это указатель на интерфейс, то область памяти, на которую он ссылается, копироваться не должна. Как видите, для выполнения маршалинга нужно сделать больше, чем просто вызвать memcpy.
Для маршалинга компонента предназначен интерфейс IMarshal. В процессе создания компонента СОМ запрашивает у него этот интерфейс. Затем СОМ вызывает функции-члены этого интерфейса для маршалинга и демаршалинга параметров до и после вызова функций. Библиотека СОМ реализует стандартную версию IMarshal, которая работает для большинства интерфейсов. Основной причиной создания собственной версии