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

Функции твои неизбежны, имя твоё неизвестно

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

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

Определимся, о чём мы говорим. Первый метод будет называться QueryInterface - он должен принимать IID другого интерфейса, экспонируемого данным же объектом, и возвращать указатель на этот интерфейс. Второй метод будет называться AddRef - он не имеет параметров, а каждый его вызов приводит к продвижению счётчика ссылок объекта вперед на единицу. Третий метод - Release. Его задача - обратная AddRef, а когда счётчик ссылок достигнет нуля Release же вызовет и delete this.

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

Итак, эти три метода:

  1. QueryInterface
  2. AddRef
  3. Release

мы можем оформить в отдельный интерфейс. Либо - мы можем прописывать в состав каждого другого интерфейса. Что лучше? И почему?

Допустим, эта функциональность выведена в совершенно отдельный интерфейс X, а все другие интерфейсы этого же объекта её не имеют. Что произойдёт? А произойдёт вот что - если при создании объекта мы попросим у сервера вернуть нам указатель на интерфейс X, то, владея этим указателем, мы легко получим указатели и на все другие интерфейсы объекта - QueryInterface же находится в составе интерфейса X. Но вот если мы у сервера попросим вернуть любой другой интерфейс этого же объекта, то так с этим интерфейсом и останемся - в этом-то интерфейсе нет QueryInterface. Это вынуждает всякий раз запрашивать не нужный нам интерфейс, а именно X, и потом из него уже производить нужный нам указатель. Налицо двойная работа на стороне клиента - при получении указателя на интерфейс.

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

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

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

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

Забавно, что этого не понимают критики известного типа "Windows - мастдай, а COM - суксь", когда они ссылаются на то, что, де, "COM - лишний код", которого в "нормальном правильном проекте" нет. Есть, есть такой код в проекте, только здесь мы его включаем явно и "своими руками", а там это делает компилятор, которому вовсе не обязательно переносить содержимое своих таблиц в код времени исполнения - часть такого кода может быть просто вставлена "по месту вызова" и совершенно прозрачно для программиста. COM действительно включает в себя много дополнительного кода, но ведь и проблемы, которые решает COM не решаются ни одним компилятором.

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

В COM эта действительно фундаментальная сущность называется "интерфейс IUnknown", что в вольном переводе может звучать как "неизвестный интерфейс" и вызывает по меньшей мере некоторое недоумение - какой же это неизвестный интерфейс, если его наличие гарантировано в любом объекте? Однако, если это переводить, как "интерфейс Неизвестно Кто", - всё встаёт на свои места. Более того, двоичный компонентный объект будет объектом COM в том и только в том случае, если он реализует, по меньшей мере, интерфейс IUnknown. Если такого интерфейса нет - это не объект COM, хотя, как мы видели в примере №1 объект может быть "двоично-компонентным" и без использования COM.

С этим интерфейсом нам волей-неволей придётся познакомиться очень близко - он есть "альфа и омега" всех интерфейсов, именно он определяет специальное поведение объекта. А сейчас пока что отметим - любой интерфейс COM должен быть унаследован от интерфейса IUnknown. Должно быть понятно почему - именно IUnknown в составе любого интерфейса обеспечивает управление временем жизни объекта и приведение типа указателя. И для этого не нужно никаких дополнительных затрат клиента.

Собственно, только введение в состав объектов нашего предыдущего примера реализации IUnknown и отделяло наши объекты от превращения в "настоящие объекты COM". Но для того, чтобы двигаться дальше нам надо точно познакомиться со спецификацией - что есть "интерфейс COM" в C++ и как он описывается. И об этом - следующая статья.


* И опять - это не совсем пуристически точное утвержение, на которое обратил внимание один мой читатель. Созданный на основе абстрактного класса объект может быть легко уничтожен, если виртуальным у него объявить и деструктор. Тогда, в контексте клиента, вызвав delete клиент вызовет виртуальный метод самоуничтожения - деструктор, который всё сделает правильно... Это - реализация "стандартного C++". Тем не менее, в общем случае, мы не можем положиться на наличие такой особенности реализации - она сработает только в том случае, если клиент "знает", что в vtbl должна находиться ссылка и на деструктор. А клиент - не обязательно может быть написан на языке, поддерживающем конструкторы/деструкторы. Но существование такого механизма в C++ тоже затрудняет методически последовательное изложение идеи и у некоторых читателей вызывает подозрения насчёт степени компетентности автора в излагаемом предмете :)

предыдущий выпускархив и оглавлениеследующий выпуск

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

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

форум

технология COM

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


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