developing.ru - клуб программистов Здесь может быть и ваша реклама.
developing.ru >технология COM >
Михаил Безверхов
aspid@developing.ru

Неолитическое искусство COM глазами искусствоведа

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

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

class NeoInterface{ public:     virtual int __stdcall Show(HWND hWhere) = 0;     virtual int __stdcall Sound() = 0; };

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

class CBanzai: public NeoInterface{ public:     int __stdcall Show(HWND hWhere);     int __stdcall Sound(); };

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

int CBanzai::Show(HWND hWhere){...return 0;}

Чем написанное отличается от "обычного программирования"? Пока что - почти ничем. Только одним - нам в числе предков обязательно нужно иметь абстрактный класс, потому, что нам нужно в конце-концов получить 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 тоже можно задействовать для более точной диагностики, что всё-таки случилось внутри сервера во время выполнения запроса клиента.

Посмотрим, что мы делаем на клиенте. А делаем мы и вовсе примитивные вещи (как построить модуль с GUI-интерфейсом пользователя - не тема нашей рассылки).

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

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

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

pBanzai->Show(hwndDlg);

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

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

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

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

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

предыдущий выпускархив и оглавлениеследующий выпуск
Авторские права © 2001 - 2004, Михаил Безверхов
Публикация требует разрешения автора

разделы сайта

форум

технология COM

оптимизация сайтов


© 2000-2004 Клуб программистов developing.ru