Что такое "технология COM" и как с ней бороться?№17
Неолитическое искусство COM
глазами искусствоведа
В
прошлом выпуске рассылки мы сочинили
небольшой пример, иллюстрирующий
функционирование базовых механизмов
взаимодействия двоичных объектов. Надеюсь,
что теперь эту тему можно закрыть - она
должна быть абсолютно понятна. В
сегодняшней рассылке мы проанализируем
наше творение с позиций а что же именно мы
сделали и почему оно работает именно так.
Во-первых,
мы объявили интерфейс:
class
NeoInterface{
public:
virtual int __stdcall Show(HWND hWhere) = 0;
virtual int __stdcall Sound() = 0;
};
Во-вторых, мы объявили
реализацию этого интерфейса в виде "нормального"
класса:
class
CBanzai: public NeoInterface{
int __stdcall
Show(HWND hWhere);
int __stdcall
Sound();
};
В-третьих, мы
сделали и реализацию методов этого класса:
int
CBanzai::Show(HWND hWhere){
...
return 0;
}
Чем написанное отличается от "обычного
программирования"? Пока что - почти ничем.
Только одним - нам в числе предков
обязательно нужно иметь абстрактный класс,
потому, что нам нужно в конце-концов
получить Vtbl. В остальном реализация
объекта в проекте сервера ну ничем не
отличается от обычного проектного
программирования. Методы всё так же
получают параметры, как их получали бы и "обычные
методы". И обещанное сбылось - если как-то
изменять конструкцию класса CBanzai не
затрагивая класса абстрактного, то Vtbl
не изменится, а клиенту - абсолютно всё
равно, что там у сервера еще есть, т.е. вызовы
и такого модифицированного объекта будут
возможны без перекомпиляции клиента.
Мы
реализовали два разных статических типа на
базе одного интерфейса. Могли бы сделать и
больше статических типов - интерфейс
полностью "развязан" от его реализации.
К обсуждению этого мы ещё вернемся немного
ниже - в рассмотрении конструкции клиента. А
пока что всё - очень похоже на привычное "попроектное
программирование". Ничего интересного.
Интересное
начинается в реализации самого сервера.
DllMain - стандартная точка входа в DLL
которая у нас никак не "нагружена". Весь шарм нашего
сервера, который и определяет его свойства,
находится в реализации функции DllGetClassObject.
Помнится, мы как-то упоминали, что это - "стандартная
функция", т.е. её прототип точно определен
системой и у
всех COM-серверов будет один и тот же. Хотя
объекты, реализуемые этими серверами и
экспонируемые ими интерфейсы - абсолютно
разные. Всё изящество кроется в том, что
знание статического типа не выходит за
пределы этой функции - привести указатель
"неизвестного типа" к указанному
абстрактному можно и на клиенте, главное - в
сервере правильно привести "известный
статический" к указанному клиентом абстрактному. Что строчки кода
*ppv = static_cast<NeoInterface *>(&oBanzai);
*ppv = static_cast<NeoInterface *>(new
CHello);
и
делают. Поэтому тип указателя ppv вполне может быть
void -
даже при изменении интерфейса между
клиентом и сервером способ "как
запрашивать и передавать объект" не
меняется. А эта неизменность и есть основа
того, что наш клиентский эмулятор можно
заменить системной функцией CoCreateInstance -
прототип DllGetClassObject никогда меняться не
будет. И как вызов нашего сервера клиентом
не потребовал линковки, так она никогда
больше никогда и не потребуется.
Следующее
интересное обстоятельство мы ранее тоже упоминали - система
запрашивает у сервера только ссылку на
объект. Как она образуется - дело сервера.
Оно и продемонстрировано - объект типа CBanzai
является статическим, а объект типа CHello -
динамическим объектами. Один "существует
всегда", а второй всякий раз
изготавливается в новом экземпляре, когда
поступает запрос выдать ссылку на него. И -
никакой разницы выше самой функции DllGetClassObject.
Клиент всегда работает только с указателем.
Объект идентифицируется парой
"CLSID-IID". Обработка тривиальна - CLSID
параметра функции мы
сравниваем с константой известного серверу
CLSID статического типа, и если "попали" -
знаем, что делать. Мы там также проверяем и
IID интерфейса... У нас-то всего один
интерфейс, но ведь если мы его проверяем, то
теоретически возможно, чтобы объект имел и
несколько интерфейсов? Верно - такой
механизм позволяет, чтобы один объект
сервера экспонировал более одного
интерфейса, т.е. (это - в очень тонких деталях
- не совсем точное утверждение, т.е. это
возможно, но реализуется не "в лоб")
механизм позволяет использовать все преимущества
множественного наследования в C++.
Механизм
получения указателя на объект надёжен -
если сервер не обнаружит ничего
подходящего для предъявленной ему пары CLSID-IID, то он просто вернет
NULL - указатель на
объект получить не удалось, а, значит,
клиент "будет знать". Да и код возврата
функции DllGetClassObject тоже можно задействовать
для более точной диагностики, что всё-таки
случилось внутри сервера во время
выполнения запроса клиента.
Посмотрим, что мы делаем на
клиенте. А делаем мы и вовсе примитивные
вещи (как построить модуль с GUI-интерфейсом
пользователя - не тема нашей рассылки).
а
потом вызываем методы объекта так, как
вызывали бы методы и "родного" для
клиента объекта полученного оператором
new:
pBanzai->Show(hwndDlg);
т.е. клиенту безразлично - как устроен
сервер. При желании можно проверять и
значение, которое возвратит метод - всё как
для "обычного объекта". Обещанное и
здесь сбылось.
Более
того, вызов методов по ссылке pBanzai
отличается от вызова методов pHello только
семантически - если "указатели
перепутать", то изменится только видимый
результат - синтаксически-то вызовы методов
разных объектов одинаковы, ведь оба разных
статических типа построены на базе одного
интерфейса.
Но
изложенное вызывает и по меньшей мере два
вопроса... Первый уже наверняка замечен
внимательным читателем. Это для объекта
типа CBanzai всё время возвращается ссылка на
один и тот же объект. А при получении ссылки
на объект типа CHello мы используем оператор
new,
т.е. всякий раз занимаем кусок
динамической памяти процесса. А где этот
объект освобождается и кем? Мы-то его просто
бросаем, но существование такой "дыры"
признать приемлемым никак нельзя... И второй
- если у нас "статический тип" и "абстрактный
тип" - разные, то как, получив указатель в
клиент знать, что это за указатель?
Ответа
ни на первый, ни на второй вопрос в нашем
примере не найти. Никто этот объект не
освобождает. Создаёт - да. И то, пока был
найден способ его создать - сколько
пришлось поломать голову. Нужно сказать,
что и уничтожение объекта может отнять
столько же усилий - желающим уничтожить объект в
клиенте путем вызова delete pHello компилятор
скажет то же самое, что он говорил в
рассылке №13 "Откуда
суть пошли интерфейсы программные?" когда мы
так попытались вызвать оператор new.
И второй вопрос имеет под собой ту
же причину - статический тип (и указатель на
него) на стороне сервера и абстрактный тип (и
указатель на него) на стороне клиента -
разные сущности. Разные не просто по
семантике, но и по численному значению. А,
стало быть, мы не только не можем удалять
абстрактные типы, но, строго говоря, и от
всяких сравнений адресов объектов толку
немного. Гораздо меньше, чем позволяют
указатели "родных" объектов. Их можно
использовать только в одном качестве -
пользуясь ими как "базой" от них можно
правильно отсчитывать смещение методов и
методы эти вызывать. Но этого для
полноценного обращения с объектом - явно
недостаточно. Иными словами, нам нужно
решить очередную порцию проблем, теперь уже
обратных тем, которые стояли перед нами при
создании объекта. Как их можно решить - в
следующей рассылке.