Что такое "технология COM" и как с ней бороться?№19
Функции твои неизбежны, имя твоё неизвестно
Итак, решение о том, что ещё не хватает нашему
объекту сервера для того, чтобы с ним мог
нормально обращаться клиент - найдено.
Остались некоторые детали оформления этого
решения в точную программную конструкцию. В
прошлой рассылке мы установили, что нам
требуется аппарат приведения типов
указателей на интерфейсы и аппарат
подсчёта ссылок на объект, с встроенным "самоликвидатором".
Они могут быть доступны клиенту только
посредством методов объекта, которые
вызывает клиент и эти самые методы должны
быть либо в составе каждого интерфейса,
реализуемого объектом, либо - оформлены
отдельным специальным интерфейсом. И
абсолютно каждый объект, какого бы рода и
племени он ни был - обязан реализовывать эту
функциональность.
Поскольку
экспонировать эти функции можно только как
интерфейс, то и предмет обсуждения: что
лучше - оформить их в виде отдельного
интерфейса, обязанность экспонировать
который вменять любому объекту (в числе
всех других интерфейсов объекта) или же -
добавлять эту функциональность к каждому
интерфейсу, экспонируемому объектом?
Определимся, о чём мы говорим. Первый метод будет
называться QueryInterface - он должен принимать
IID другого интерфейса, экспонируемого данным
же объектом, и возвращать указатель на этот интерфейс.
Второй метод будет называться AddRef - он не
имеет параметров, а каждый его вызов
приводит к продвижению счётчика ссылок
объекта вперед на единицу. Третий метод - Release. Его задача - обратная
AddRef, а когда
счётчик ссылок достигнет нуля Release же
вызовет и delete this.
Почему
вместо одного метода по управлению
счётчиком ссылок мы придумали два? Хотя бы
потому, что код вызова метода без
параметров - короче. Пусть на несколько
байтов, но эти несколько байтов будут в
клиенте встречаться всюду, где у нас размножается
указатель. И суммарная добавка к коду может
быть большой.
Итак,
эти три метода:
QueryInterface
AddRef
Release
мы
можем оформить в отдельный интерфейс. Либо -
мы можем прописывать в состав каждого
другого интерфейса. Что лучше? И почему?
Допустим,
эта функциональность выведена в совершенно
отдельный интерфейс X, а все другие
интерфейсы этого же объекта её не имеют. Что
произойдёт? А произойдёт вот что - если при
создании объекта мы попросим у сервера
вернуть нам указатель на интерфейс X, то,
владея этим указателем, мы легко получим
указатели и на все другие интерфейсы
объекта - QueryInterface же находится в составе
интерфейса X. Но вот если мы у сервера
попросим вернуть любой другой интерфейс
этого же объекта, то так с этим интерфейсом
и останемся - в этом-то интерфейсе нет QueryInterface. Это вынуждает всякий раз
запрашивать не нужный нам интерфейс, а
именно X, и потом из него уже производить
нужный нам указатель. Налицо двойная работа
на стороне клиента - при получении
указателя на интерфейс.
Если
же эту функциональность помещать внутрь
всякого интерфейса, который экспонирует
объект, то у каждого интерфейса будут
заниматься три дополнительных ячейки Vtbl, но
зато нам не нужно будет выполнять никакой
двойной работы на стороне клиента, всё
управление объектом можно осуществить
посредством его любого интерфейса. И как-то
кажется, что второй способ значительно удобнее... со
стороны клиента, разумеется - повторно-то
используется именно сервер.
Но
ведь так можно построить и такой объект, у
которого не будет ни одного "полезного"
интерфейса? Можно. А вот эта служебная
функциональность в любом случае должна
быть. И
можно будет размножать указатель, а потом -
уничтожить объект. А всё что сверх того -
определяется исключительно существом
решаемой программистом задачи.
Описанная
функциональность настолько фундаментальна,
что без нее "вообще ничего не работает" - мы
ведь и задумались над ней потому, что в
нашей реализации компонентного
взаимодействия не хватало очень
существенных фрагментов. Можно ли её
реализовать иначе? В деталях - да, в сущности
- нет. Ведь причина наличия этой
функциональности в составе объекта - философская. Если бы у нас компилятор
знал точный статический тип
объекта и тип этот был один и для клиента и
для сервера, то конечно, компилятор мог бы
реализовать и правильный вызов new и
правильный вызов delete, и сам компилятор мог
бы преобразовывать указатели на тип... Но,
фактически, это означает, что и клиент и
сервер должны располагаться в контексте
одного и того же проекта - а мы как раз имеем
совершенно обратные "начальные условия".
У нас и клиент и сервер обязательно должны
располагаться в разных проектах, в разных
контекстах. У нас ведь - программирование из
двоичных компонент.
Именно
в силу этого обстоятельства нам сначала
понадобилось часть таблиц времени
компиляции встроить в сам объект, так чтобы
они сохранялись и во время выполнения (Vtbl) (см
рассылку №15 "Введение
в теорию компиляции. И выведение из неё..."), а теперь нам требуется
нагрузить объект и такими функциями, как
управление временем жизни и приведение
типа. И избежать этого мы не можем - либо мы
сами, либо компилятор.
Забавно,
что этого не понимают критики типа "Windows -
мастдай, а COM - суксь", когда они ссылаются
на то, что, де "COM - лишний код", которого
в "нормальном правильном проекте" нет.
Есть, есть такой код в проекте, только здесь
мы его включаем явно и "своими руками",
а там это делает компилятор, которому вовсе
не обязательно переносить содержимое своих
таблиц в код времени исполнения - часть
такого кода может быть просто вставлена "по
месту вызова" и совершенно прозрачно для
программиста. COM действительно включает в
себя много дополнительного кода, но
ведь и проблемы, которые решает COM не
решаются ни одним компилятором.
Нужно
особо заметить - что хотя мы и изучаем COM, то,
что сейчас сформулировано имеет
философскую природу. А, значит, в том или
ином виде может быть найдено и в реализации CORBA и должно
быть вообще в любой двоично-компонентной
технологии. "Обёртка" этого может быть
разной, а вот сущность - одинакова.
В
COM эта действительно фундаментальная
сущность называется "интерфейс IUnknown",
что в вольном переводе может звучать как
"неизвестный интерфейс" и вызывает по
меньшей мере некоторое недоумение - какой
же это неизвестный интерфейс, если его
наличие гарантировано в любом объекте?
Более того, двоичный компонентный объект
будет объектом COM в том и только в том
случае, если он реализует, по меньшей мере,
интерфейс IUnknown. Если такого интерфейса нет -
это не объект COM, хотя, как мы видели в
примере №1 объект может быть "двоично-компонентным"
и без использования COM.
С
этим интерфейсом нам волей-неволей
придётся познакомиться очень уж близко - он
есть "альфа и омега" всех интерфейсов,
именно он определяет специальное поведение
объекта. А сейчас пока что отметим - любой
интерфейс COM должен быть унаследован от
интерфейса IUnknown. Должно быть понятно почему
- именно IUnknown в составе любого интерфейса
обеспечивает управление временем жизни
объекта и приведение типа указателя. И для
этого не нужно никаких дополнительных
затрат клиента.
Собственно,
только введение в состав объектов нашего
предыдущего примера реализации IUnknown и
отделяло наши объекты от превращения в "настоящие
объекты COM". Но для того, чтобы двигаться
дальше нам надо точно познакомиться со
спецификацией - что есть "интерфейс COM"
в C++ и как
он описывается. И об этом - следующая
рассылка.