Скачиваний:
100
Добавлен:
01.05.2014
Размер:
1.56 Mб
Скачать

Простейший пример использования

Общий замысел примера. Делаем два разных программных проекта и в каждом собираем исполняемый модуль. Один будет клиент, другой - сервер. Организуем их взаимодействие - клиент запросит ссылку на объект сервера и будет вызывать его методы. Если наша теория верна, то между ними действительно будет требоваться всего только один файл в исходном тексте - описание интерфейсов, которыми взаимодействуют между собой клиент и сервер. Этот файл должен быть одинаков для обоих проектов. Все остальные файлы - должны получиться сугубо локальными для каждого модуля. Сервер вообще не должно интересовать, кто у него клиент, а клиент не должен знать "что у сервера внутри". После сборки модулей "приводим их в соприкосновение" и ... всё должно работать, т.е., по крайней мере - не обрушиваться, а демонстрировать, что изложенные механизмы-то работают.

Допущения. Мы пока не сможем воспользоваться для нашего примера функциями операционной системы. Они требуют "настоящего COM", а у нас для него пока ещё кое-чего не хватает. Поэтому функцию операционной системыCoCreateInstance, которая по запросу клиента возвращает ссылку на объект сервера мы сами эмулируем в составе клиента. Мы знаем, что она должна делать и ничего лишнего мы не добавим.

Ход конструирования. Поймём, чего мы хотим. Мы хотим, чтобы клиент вызвал объект сервера. При этом клиент будет вызывать метод объекта сервера и показывать результаты её работы нам, а сервер будет исполнять эту работу по запросу клиента. Первое, что мы в таком случае должны определить - как будут взаимодействовать клиент и сервер. Т.е. самым первым шагом мы должны сконструировать именно интерфейс между клиентом и сервером. Не объекты, не их реализации, а - именно интерфейс. Я предлагаю в качестве примера простой интерфейс - метод Show, который заставит сервер показать какой-то транспарант и методSound, который заставит сервер произвести какой-то звук. Этот интерфейс описывается простым классом:

classNeoInterface

{

public:

intShow(HWND hWhere);

intSound();

};

Для того, чтобы это был действительно интерфейс, как мы ранее выяснили, его нужно сделать виртуальным чисто абстрактным классом:

class NeoInterface

{

public:

virtual int Show(HWND hWhere) = 0;

virtual intSound() = 0;

};

Далее, в составе сервера определим два статических типа, которые он будет в состоянии предоставлять клиенту - тип Banzaiи типHello, оба этих типа будут произведены от одного и того же интерфейса. Поэтому методы, которые они экспонируют у них будут одинаковы, но делать эти методы будут несколько разные действия: объект типаBanzaiбудет показывать транспарант с надписью "Банзай", а объект типаHello- с надписью "Хелло". Соответственно, и звуки которые будет производить методSoundв составе разных объектов тоже будут разными.

Клиент создаст (запросит сервер о создании) по одному экземпляру объектов каждого типа и вызовет их методы с демонстрацией нам того, что методы работают.

Мы перенумеруем интерфейс (абстрактный тип) и статические типы, причём сделаем это "цивилизованно" - перенумеруем их GUIDами, а не обычными номерами. Для нас сейчас это неважно - чем, но, если невзначай можно воспользоваться плодами цивилизации, то почему бы и не продемонстрировать, как можно их использовать?

Сложные места. Самое сложное место - эмуляция функции APICoCreateInstanceв составе клиента. Для того, чтобы нам ее эмулировать мы просто явно загружаемDLLсервера в адресное пространство клиента по явному же её имени. В этом нет отступления от "принциповCOM", поскольку иначе мы должны бы были написать ещё и кусочек операционной системы. В данном случае надо понимать, что наш эмулятор, на самом-то деле "наш" только потому, что эмулятор. Система делает те же действия, но делает это "у себя внутри" в составе функцииCoCreateInstanceи нам не показывает. Поэтому явная загрузкаDLL"не считается" - мы избавимся от неё как только нам удастся из наших объектов сделать "настоящийCOM".

Код примера можно скачать здесь. Пример написан на чистомC++без примененияMFCилиATLи должен работать везде. Пример состоит из двух проектов:NeoClnt- проекта клиента иNeoSrv- проекта сервера. Их следует последовательно собрать, а затем скопировать модульNeoSrv\Debug\NeoSrv.dllв каталогNeoClnt\Debugи запустить модульNeoClnt.exe. Модуль покажет диалог с четырьмя кнопками - вызов методов объектаBanzaiи вызов методов объектаHello.

Соберите модули. Исследуйте их функционирование. Изучите исходные тексты - они хорошо откомментированы "по месту".

Обратите внимание, что нам действительно удалось сделать компонентное приложение. Модуль NeoSrv.dllне экспортирует никаких функций, кромеDllGetClassObject- методы наших объектов вызываются без линковки. Обратите особое внимание, что при сборке модуляNeoClnt.exeнам не потребовалась библиотекаNeoSrv.lib- если бы у нас статически линковались вызовы изDLLнам бы она требовалась, а клиент без нее не мог бы быть собран. И она - была бы разной для разных версийDLL, её приходилось бы всякий раз перелинковывать к клиенту. Обратите внимание, что вы совершенно свободны в изменении содержимогоNeoSrb.dllбез пересборки клиента - до тех пор, пока вы не трогаете описание интерфейса, использованного при создании клиента, и, естественно, пока вы не изменяете нумерацию статических типов и самого интерфейса. Вам нужно только всякий раз помещать новую версиюDLLна то место, где располагалась версия старая.

А ведь к "настоящему COM" мы ещё и не приступили! Для настоящегоCOMнам не хватает ещё чуть-чуть... чего именно мы установим после анализа того, что обнаружили в данном примере.

Мы сочинили небольшой пример, иллюстрирующий функционирование базовых механизмов взаимодействия двоичных объектов. Надеюсь, что теперь эту тему можно закрыть - она должна быть абсолютно понятна. Теперь проанализируем наше творение с позиций, а что же именно мы сделали и почему оно работает именно так?

Во-первых, мы объявили интерфейс:

class NeoInterface

{

public:

virtual int __stdcall Show(HWND hWhere) = 0;

virtual int __stdcallSound() = 0;

};

Во-вторых, мы объявили реализацию этого интерфейса в виде "нормального" класса:

class CBanzai: public NeoInterface

{

public:

int __stdcall Show(HWND hWhere);

int __stdcallSound();

};

В-третьих, мы сделали и реализацию методов этого класса:

int CBanzai::Show(HWND hWhere)

{

...

return0;

}

Чем написанное отличается от "обычного программирования"? Пока что - почти ничем. Только одним - нам в числе предков обязательно нужно иметь абстрактный класс, потому, что нам нужно в конце-концов получить vtbl. В остальном реализация объекта в проекте сервера ну ничем не отличается от обычного проектного программирования. Методы всё так же получают параметры, как их получали бы и "обычные методы". И обещанное сбылось - если как-то изменять конструкцию классаCBanzaiне затрагивая класса абстрактного, тоvtblне изменится, а клиенту - абсолютно всё равно, что там у сервера есть ещё, т.е. вызовы и такого модифицированного объекта будут возможны без перекомпиляции клиента.

Мы реализовали два разных статических типа на базе одного интерфейса. Могли бы сделать и больше статических типов - интерфейс полностью "развязан" от его реализации. К обсуждению этого мы ещё вернемся немного ниже - в рассмотрении конструкции клиента. А пока что всё - очень похоже на привычное "попроектное программирование". Ничего интересного.

Интересное начинается в реализации самого сервера. DllMain- стандартная точка входа вDLLкоторая у нас никак не "нагружена". Весь шарм нашего сервера, который и определяет его свойства, находится в реализации функцииDllGetClassObject . Это - "стандартная функция", т.е. её прототип точно определен системой и у всехCOM-серверов будет один и тот же. Хотя объекты, реализуемые этими серверами и экспонируемые ими интерфейсы - абсолютно разные. Всё изящество кроется в том, что знание статического типа не выходит за пределы этой функции - привести указатель "неизвестного типа" к указанному абстрактному можно и на клиенте, главное - в сервере правильно привести "известный статический" к указанному клиентом абстрактному. Что строчки кода

static_cast<NeoInterface *>(&oBanzai);

static_cast<NeoInterface *>(new CHello);

и делают. Поэтому тип указателя ppvвполне может бытьvoid- даже при изменении интерфейса между клиентом и сервером способ "как запрашивать и передавать объект" не меняется. А эта неизменность и есть основа того, что наш клиентский эмулятор можно заменить системной функциейCoCreateInstance- прототипDllGetClassObjectникогда меняться не будет. И как вызов нашего сервера клиентом не потребовал линковки, так она никогда больше никогда и не потребуется.

Следующее интересное обстоятельство мы ранее тоже упоминали - система запрашивает у сервера только ссылку на объект. Как она образуется - дело сервера. Оно и продемонстрировано - объект типа CBanzaiявляется статическим, а объект типаCHello- динамическим объектами. Один "существует всегда", а второй всякий раз изготавливается в новом экземпляре, когда поступает запрос выдать ссылку на него. И - никакой разницы выше самой функцииDllGetClassObject. Клиент всегда работает только с указателем.

Объект идентифицируется парой "CLSID-IID". Обработка тривиальна -CLSIDпараметра функции мы сравниваем с константой известного серверуCLSIDстатического типа, и если "попали" - знаем, что делать. Мы там также проверяем иIIDинтерфейса... У нас-то всего один интерфейс, но ведь если мы его проверяем, то теоретически возможно, чтобы объект имел и несколько интерфейсов? Верно - такой механизм позволяет, чтобы один объект сервера экспонировал более одного интерфейса, т.е. (это - в очень тонких деталях - не совсем точное утверждение, т.е. это возможно, но реализуется не "в лоб") механизм позволяет использовать все преимущества множественного наследования вC++.

Механизм получения указателя на объект надёжен - если сервер не обнаружит ничего подходящего для предъявленной ему пары CLSID-IID, то он просто вернетNULL- указатель на объект получить не удалось, а, значит, клиент "будет знать". Да и код возврата функцииDllGetClassObjectтоже можно задействовать для более точной диагностики, что всё-таки случилось внутри сервера во время выполнения запроса клиента.

Посмотрим, что мы делаем на клиенте. Мы вначале запрашиваем указатель на объект:

::CoGetClassObjectEmulator(CLSID_Banzai, IID_NeoInterface, (void **) &pBanzai);

а потом вызываем методы объекта так, как вызывали бы методы и "родного" для клиента объекта полученного оператором new:

pBanzai->Show(hwndDlg);

т.е. клиенту безразлично - как устроен сервер. При желании можно проверять и значение, которое возвратит метод - всё как для "обычного объекта". Обещанное и здесь сбылось.

Более того, вызов методов по ссылке pBanzaiотличается от вызова методовpHelloтолько семантически - если "указатели перепутать", то изменится только видимый результат - синтаксически-то вызовы методов разных объектов одинаковы, ведь оба разных статических типа построены на базе одного интерфейса.

Но изложенное вызывает и по меньшей мере два вопроса. Первый - для объекта типа CBanzaiвсё время возвращается ссылка на один и тот же объект. А при получении ссылки на объект типаCHelloмы используем операторnew, т.е. всякий раз занимаем кусок динамической памяти процесса. А где этот объект освобождается и кем? Мы-то его просто бросаем, но существование такого недостатка признать приемлемым никак нельзя. И второй - если у нас "статический тип" и "абстрактный тип" - разные, то как, получив указатель в клиент знать, что это за указатель?

Ответа ни на первый, ни на второй вопрос в нашем примере не найти. Никто этот объект не освобождает. Создаёт - да. И то, пока был найден способ его создать - сколько пришлось поломать голову. Нужно сказать, что и уничтожение объекта может отнять столько же усилий - желающим уничтожить объект в клиенте путем вызова deletepHelloкомпилятор скажет то же самое, что он говорил когда мы так попытались вызвать операторnew.

И второй вопрос имеет под собой ту же причину - статический тип (и указатель на него) на стороне сервера и абстрактный тип (и указатель на него) на стороне клиента - разные сущности. Разные не просто по семантике, но и по численному значению. А, стало быть, мы не только не можем удалять абстрактные типы, но, строго говоря, и от всяких сравнений адресов объектов толку немного. Гораздо меньше, чем позволяют указатели "родных" объектов. Их можно использовать только в одном качестве - пользуясь ими как "базой" от них можно правильно отсчитывать смещение методов и методы эти вызывать. Но этого для полноценного обращения с объектом - явно недостаточно. Иными словами, нам нужно решить очередную порцию проблем, теперь уже обратных тем, которые стояли перед нами при создании объекта.

Итак, по результатам предшествующих тестов, у нас "всё работает". У нас теперь другая проблема, у нас - "не перестаёт работать". Мы обнаружили, что наша механика страдает тяжёлым недостатком - предложенный механизм создания объектов настолько обезличивает их, что их невозможно уничтожить по окончании их использования. Если сервер выдал клиенту ссылку на свой статический объект, то в этом нет никакой проблемы - по завершении сервера все объекты сервера будут уничтожены автоматически. Но если сервер создал экземпляр объекта динамически оператором new, то этот объект не может уничтожить никто. Его не может уничтожить клиент - в контексте клиента такой объект неизвестен и вызовdeleteна стороне клиента невозможен. Но его не может уничтожить и сервер. Даже, если бы мы создали специальную функцию внутри сервера, которую бы вызывали для уничтожения такого объекта (совершенно аналогично подходу, который мы использовали для создания), то всё равно указатель на статический тип, который сервер использует, чтобы создать новый объект, не равен указателю на тот абстрактный тип, который сервер возвращает клиенту. Между ними стоит операция приведения типа, которая, в общем случае, даже численное значение указателя изменяет, не говоря про его семантику.

Поэтому, когда клиент передаст такой указатель (а другого указателя на стороне клиента нет) обратно серверу, то и сам сервер не сможет разобраться - приведение типов указателя необратимо. Поэтому и на стороне сервера операция deleteтак же бессмысленна.

Однако, если наша технология претендует на право на жизнь, то эту проблему в числе первых решить нужно.

Здесь самое время вспомнить, что хотя ссылка на объект и выдаётся клиенту, хотя клиент обращается по этой преобразованной ссылке абсолютно правильно, всё равно объект-то создаётся сервером и существует в контексте сервера. Иными словами - хотя выдаваемый адрес преобразуется, первоначальный адрес от этого существовать не перестаёт. И пусть сам модуль сервера его может специально нигде и не запоминать, но объект-то должен знать свой указатель this? Объект помнит свой указательthis, объект существует в контексте сервера, этого вполне достаточно, чтобы объект мог сам вызвать в отношении себя операторdelete. Вот так -delete this. И вызов этот будет вполне корректен - сама операция удаления объекта выполняется не объектом (кодdeleteобъекту не принадлежит), объектом она только вызывается. Образно говоря, как "последнее издыхание" объект вполне может вызвать операцию самоуничтожения и "ничего ему за это не будет".

Проблема сразу переходит в иную плоскость - если понятно "что", то теперь нужно придумать "как". Как дать понять объекту, что его услуги больше не нужны? Самый простой путь - предусмотреть в составе объекта специальный метод. Это будет самый обычный метод, который будет обязан реализовывать любой объект и экспонировать его любому клиенту. Когда клиент завершит свои операции с объектом он вызовет этот метод и... А когда клиент завершит свои операции с объектом?

Операция изготовления объекта действительно выполняется за время жизни объекта ровно один раз. И операция его уничтожения - тоже. Но программа может получить ссылку на объект в переменную "a", потом присвоить её переменной "b", потом передать ещё куда-нибудь... Так когда клиент закончит свои операции с объектом? Когда это будет очевидно по смыслу (семантике) выполняемых операций или когда все эти переменные перестанут существовать? Мне кажется, что второе - надёжнее. Ведь компилятор-то гораздо точнее программиста может помнить, когда та или иная переменная выходит за область видимости, т.е. становится недоступной и разрушается. Только вот получается, что теперь и клиент должен как-то уведомлять сервер о том, что происходит внутри клиента с переданной ему ссылкой на объект? Самопроизвольно-то сервер этого узнать не в состоянии.

Решая одну проблему получили две - и метод самоуничтожения надо сделать и метод уведомления надо сделать. А нельзя ли обе этих задачи совместить? Этим бы мы сэкономили метод, упростили бы клиентский код, уменьшили бы вероятность ошибок. И эта задача только кажется парадоксальной, на самом деле - совместить можно. Ведь в "уведомлении об использовании", которое клиент должен посылать серверу сервер совершенно не интересует где и как клиентом используется ссылка. Сервер интересует только факт - "ссылка используется". В таком случае сервер должен только вести простой счётчик ссылок - как только придёт сигнал "ссылка используется" сервер прибавит туда единицу, как только придет сигнал "ссылка перестала использоваться" сервер вычтет оттуда единицу... а как только счётчик ссылок достигнет нуля это как раз и будет означать, что объект-то больше никому и не нужен - если все ссылки на него потеряны, то как кто-нибудь сможет к этому объекту обратиться? И тогда сервер должен уничтожить такой объект. Поскольку мы ранее выяснили, что delete this- вполне корректная операция, то и ведение счётчика ссылок вполне можно доверить самому объекту. А клиенту - вменить в обязанность всякий раз уведомлять объект о том, что клиент размножил число ссылок на этот объект или, напротив, сократил.

Это - очень изящное решение. При запросе первой ссылки, т.е. при первоначальном создании объекта, единицу в такой счетчик можно и самому серверу вписать (сомнительно, чтобы клиент запрашивал сервер о создании объекта, а потом не сохранял переданную ссылку на него), а в дальнейшем клиент будет управлять временем жизни объекта сам, т.е. посредством специального метода объекта. Никаких проблем "какого типа был уничтожаемый объект" в данном случае не возникает в принципе - какого он типа сам объект хорошо знает. Ничего передавать серверу не нужно.

Итак, будем считать, что одну проблему мы решили - всякий объект должен иметь в своем составе либо специальный метод, либо пару методов, вызывая которые клиент изменял бы счетчик ссылок объекта соответственно числу фактических экземпляров указателя на клиентской стороне. Одна лишняя операция при присваивании, зато потом - никаких проблем.

Вспомним проблему вторую, тоже связанную с указателем - объект сервера (статический тип) может экспонировать более одного интерфейса (абстрактного типа, посредством которого к нему клиент осуществляет доступ). Не запрещено, во всяком случае. И это - разные интерфейсы. И это - разные указатели. А объект (статический тип) - один. Интересно, сколько счетчиков ссылок должен вести объект и как в них не запутаться?

Эта проблема - посложнее первой. Посложнее тем, что хоть мы и говорим "объект должен иметь специальный метод, изменяющий счетчик ссылок" мы тем не менее отчётливо понимаем, что иным способом, как в составе интерфейса экспонировать такой метод клиенту невозможно. Выходит, что либо такой метод располагается в составе одного, специального интерфейса, который (интерфейс) будет обязан экспонировать любой объект, либо такой метод должен присутствовать в составелюбого интерфейса, экспонируемого объектом.

Кроме того, мы сталкиваемся и с совершенно особой проблемой - допустим, мы точно знаем, что у объекта Xреализуются два интерфейсаYиZ. Когда мы создаём такой объект, то сервер по нашему желанию в состоянии привести указатель на статический тип к любому абстрактному - либоY, либоZ. И выдать его нам. А что делать, если мы уже имеем указатель наY, а нам понадобилсяZтого же самого объекта? Имели бы сам первоначальный указатель - привели бы легко. Но один абстрактный тип непонятно каким (на стороне клиента) образом не приводится к другому. Такую операцию тоже должен выполнять сервер. Скорее даже - по тем же причинам, по которым мы делегируем ведение счётчика ссылок объекту, - сам объект этого сервера.

И этого тоже нетрудно достичь - нужно только предусмотреть специальный метод. По уже называвшимся выше причинам этот метод должен быть либо в составе специального интерфейса, либо в составе каждого интерфейса - кто знает из какого интерфейса нам понадобится ссылка на какой другой интерфейс?

В сущности, обе сформулированные проблемы мы решили - нам нужен аппарат подсчёта ссылок, управляемый клиентом посредством специального метода и нам нужен особый метод, который бы позволял имея ссылку на один интерфейс объекта получать ссылку на его другой интерфейс.