Что такое "технология COM" и как с ней бороться?№16
COM в эпоху неолита
Ну
вот, теперь можно сказать - теоретически мы
знаем все базовые структуры и необходимые
механизмы для того, чтобы организовать
взаимодействие двоичных компонентов. Знаем
почему они требуются, из каких соображений
они именно такие, а не какие-то другие. Самое
время сочинить первый пример - проверить
изложенную теорию практикой. И мы в этой рассылке его
сочиним.
Сразу
хочу сказать - это будет очень примитивный
пример. Его, строго говоря, и законченной
программой назвать язык не повернётся. В
нём будут использованы только те принципы,
которые мы всесторонне изучили в
предыдущих выпусках рассылки. Но этот
пример для нас будет очень важен - из
исследования этого примера должны
появиться мысли, чего ещё не хватает для
полноценного компонентного взаимодействия.
Если сравнивать наше нынешнюю программу с
живописью, то она будет соответствовать
чему-то вроде наскального рисунка - до
современной живописи ещё далеко, но
основные принципы изобразительного
искусства уже заметить можно. Назовём этот
пример "неолитическим".
Общий
замысел примера. Делаем два разных
программных проекта и в каждом собираем
исполняемый модуль. Один будет клиент,
другой - сервер. Организуем их
взаимодействие - клиент запросит ссылку на
объект сервера и будет вызывать его методы. Если наша
теория верна, то между ними действительно будет
требоваться всего только один файл в исходном
тексте - описание интерфейсов, которыми
взаимодействуют между собой клиент и
сервер. Этот файл должен быть одинаков для
обоих проектов. Все остальные файлы - должны
получиться сугубо локальными для каждого модуля. Сервер вообще
не должно интересовать, кто у него клиент, а
клиент не должен знать "что у сервера
внутри". После сборки модулей "приводим
их в соприкосновение" и ... всё должно
работать, т.е., по крайней мере - не
обрушиваться, а демонстрировать, что
изложенные механизмы-то работают.
Допущения.
Мы пока не сможем воспользоваться для нашего
примера функциями операционной системы.
Они требуют "настоящего COM", а у нас для
него пока ещё кое-чего не хватает. Поэтому
функцию операционной системы CoCreateInstance,
которая по запросу клиента возвращает
ссылку на объект сервера мы сами
эмулируем в составе клиента. Мы знаем, что
она должна делать (см. рассылку № 10 "У
кого и как добиться получения COM объекта")
и ничего лишнего мы не добавим.
Ход
конструирования. Поймём, чего мы хотим. Мы
хотим, чтобы клиент вызвал объект сервера.
При этом клиент будет вызывать метод
объекта сервера и показывать результаты её работы
нам, а сервер будет исполнять эту работу по
запросу клиента. Первое, что мы в таком
случае должны определить - как будут
взаимодействовать клиент и сервер. Т.е.
самым первым шагом мы должны
сконструировать именно интерфейс между клиентом и
сервером. Не объекты, не их реализации, а -
именно интерфейс. Я предлагаю в качестве примера
простой интерфейс - метод Show, который
заставит сервер показать какой-то
транспарант и метод Sound, который заставит
сервер произвести какой-то звук. Этот
интерфейс описывается простым классом:
class
NeoInterface{
public:
int
Show (HWND hWhere);
int
Sound();
};
Для
того, чтобы это был действительно интерфейс,
как мы ранее выяснили, его нужно сделать
виртуальным чисто абстрактным классом:
class
NeoInterface{
public:
virtual
int Show (HWND hWhere) = 0;
virtual
int Sound() = 0;
};
Далее,
в составе сервера определим два
статических типа, которые он будет в
состоянии предоставлять клиенту - тип Banzai и
тип Hello, оба этих типа будут произведены от
одного и того же интерфейса. Поэтому методы,
которые они покажут (на профессиональном
сленге COM-программистов надо сказать - "экспонируют")
клиенту у них будут одинаковы, но делать эти
методы будут несколько разные действия:
объект типа Banzai будет показывать
транспарант с надписью "Банзай", а
объект типа Hello - с надписью "Хелло".
Соответственно, и звуки которые будет
производить метод Sound в составе разных
объектов тоже будут разными.
Клиент
создаст (запросит сервер о создании) по
одному экземпляру объектов каждого типа и
вызовет их методы с демонстрацией нам того, что
методы работают.
Как
было рассмотрено в рассылке №14 "Клиенту -
клиентово, а серверу - серверово" мы
перенумеруем интерфейс (абстрактный тип) и
статические типы, причём сделаем это "цивилизованно"
- перенумеруем их GUIDами, а не обычными
номерами. Для нас сейчас это неважно - чем,
но, если невзначай можно воспользоваться
"плодами цивилизации", то почему бы и
не продемонстрировать, как можно их
использовать?
Сложные
места. Самое сложное место - эмуляция
функции API CoCreateInstance в составе клиента. Для
того, чтобы нам ее эмулировать мы просто
явно загружаем DLL сервера в адресное
пространство клиента по явному же её имени. В
этом нет отступления от "принципов COM"
описанных в рассылках №№ 5 и
6 "С чего
начинается COM", поскольку иначе мы должны
бы были написать ещё и кусочек операционной
системы. В данном случае надо понимать, что
наш эмулятор, на самом-то деле "наш"
только потому, что эмулятор. Система делает
те же действия, но делает это "у себя
внутри" в составе функции CoCreateInstance и нам
не показывает. Поэтому явная загрузка DLL "не
считается" - мы избавимся от неё как
только нам удастся из наших объектов
сделать "настоящие объекты COM".
Готовый
код примера можно выкачать здесь. Пример
написан на чистом C++ без применения MFC и
ATL и
должен работать везде. Пример состоит из
двух проектов: NeoClnt - проекта клиента и NeoSrv -
проекта сервера. Их следует
последовательно собрать, а затем
скопировать модуль NeoSrv\Debug\NeoSrv.dll в каталог
NeoClnt\Debug и запустить модуль NeoClnt.exe . Модуль
покажет диалог с четырьмя кнопками - вызов
методов объекта Banzai и вызов методов объекта
Hello.
Соберите
модули. Исследуйте их функционирование.
Изучите исходные тексты - они хорошо
откомментированы "по месту", поэтому
почему проект сделан именно так, а не иначе
здесь говорить излишне - комментарии "почему
так" будут выпущены следующей рассылкой. Запустите
dumpbin.exe и
исследуйте таблицу EXPORTS модуля NeoSrv.dll
Обратите
внимание, что нам действительно удалось
сделать компонентное приложение. Модуль
NeoSrv.dll не экспортирует никаких функций,
кроме DllGetClassObject - методы наших объектов
вызываются без линковки. Обратите
особое внимание,
что при сборке модуля NeoClnt.exe нам не
потребовалась библиотека NeoSrv.lib - если бы у
нас статически линковались вызовы из DLL нам
бы она требовалась, а клиент без нее не мог
бы быть собран. И она - была бы разной
для разных версий DLL, её приходилось бы
всякий раз перелинковывать к клиенту.
Обратите внимание, что вы совершенно
свободны в изменении содержимого NeoSrb.dll без
пересборки клиента - до тех пор, пока вы не
трогаете описание интерфейса,
использованного при создании клиента, и,
естественно, пока вы не изменяете нумерацию статических типов
и самого интерфейса.
Вам нужно только всякий раз помещать новую
версию DLL на то место, где располагалась
версия старая - дефект "живописи эпохи
неолита".
А
ведь к "настоящему COM" мы ещё и не
приступили! Для настоящего COM нам не хватает
ещё чуть-чуть... чего именно мы установим
после анализа того, что обнаружили в данном
примере. Анализ - в следующей рассылке.