Что такое "технология COM" и как с ней бороться?
№37
От железа к COMпозитным материалам
В сегодняшнем номере рассылки мы построим пример №4 - наверное, последний из
"элементарных примеров". Его размер, по сравнению с примерами №№
1,2,3 увеличился почти вдвое,
что связано с тем, что мы вдвое умножили сущности, находящиеся в рассмотрении. Поскольку
то, что мы рассматривали до сих пор - азбука, я не считал возможным использовать какие-то
сервисные средства. Сейчас я начинаю над этим задумываться - код угрожающе растёт.
Но, с другой стороны, ценность этих примеров как раз в том и состоит, что они
показывают как обстоит дело внутри сервисных средств и что именно сервисные средства
инкапсулируют в себе... словом, я пока ничего не решил.
Итак, пример, который находится
здесь, иллюстрирует
"концепцию фабрики класса",
а более точно - код показывает, как могут быть взаимосвязанно реализованы
статические и нестатические аспекты типа. Построен он на базе
примера №3 - там был сделан почти настоящий COM-сервер. Я просто сделал еще
два "элементарных COM-объекта", которые к существовавшим у нас типам
Banzai и Hello стали реализовывать "фабрики класса", а сами
существовавшие у нас сущности стали "объектами экземпляров". Сейчас можно
утверждать, что в данном примере получился простой, очень даже примитивный
(на грани того, что из него больше ничего не выкинуть), но - самый настоящий
COM-сервер, реализующий два статических типа. Эффективность кода примера - не то
обстоятельство, на которое нужно сейчас обращать внимание. Во многих случаях можно
было сделать короче (чего только стоят четыре оконных процедуры в клиенте, когда
можно было обойтись двумя). Но "короче" и "очевиднее" вещи, во многом,
противоположные. Поэтому, там где можно "коротко" или "длинно"
предпочтение отдавалось "понятно", хотя в реальных программах, вы, конечно,
многое оптимизируете.
Клиент, в целом, остался тот же - к "маленькому окну" привязывается
COM-объект и посредством графического интерфейса можно наглядно выяснить
существующие отношения между объектами. Добавилось новое - в соответствии с тем, что
у нас теперь есть не только "объекты экземпляра", но и "объекты типа"
мы имеем не два, а четыре "маленьких окна" - на каждый тип есть окно
"статического типа" и окно "экземпляра". При этом "окно
экземпляра" функционально осталось точно таким же, каким оно было в примере
№3 - ведь функциональность самих объектов экземпляра не изменилась. А вот порождаются
"объекты экземпляра" теперь по-другому - через "объект статического типа".
Поэтому в "окне статического типа" показываются кнопки для вызова методов
интерфейса IUnknown (как вы помните - это совсем не тот
IUnknown, который реализован в "объекте экземпляра")
и для вызова метода CreateInstance. Тем, кто интересуется как
создать немодальный диалог и передать ему параметр и пр. - клиентская часть примера тоже
может быть полезна. К сожалению, именно такой код и составляет большую часть примера.
Код, вызывающий COM и обрабатывающий вызовы методов находится в численном
меньшинстве. Что, может быть, и не так плохо - это наглядное подтверждение,
что затраты собственно клиента на обращение с COM-объектом даже
"напрямую" не так уж и велики, как это часто кажется.
В прошлых примерах мы рассматривали интересное явление - объект, который экспонирует
интерфейс, может быть для программы-владелицы порожден и в динамической и в
статической памяти. Для программы-клиента эту разницу заметить невозможно, а
вот программа-сервер в одном случае будет "шлёпать" всякий раз новые
экземпляры объектов, а в другом всем будет выдавать одну и ту же ссылку.
Это - в чистом виде артефакт реализации, он к COM не имеет никакого отношения,
и у нас объект Banzai порождался в статической памяти, а объект Hello -
в динамической как пример того, что это возможно. Некогда нАчатая, в данном примере
эта особенность углУблена - в реализации статического типа Banzai тот элементарный
COM-объект, что реализует статические аспекты типа (фабрика класса) размещается в
динамической памяти, а сам "объект экземпляра" - в статической. Для статического
типа Hello сделано наоборот - "фабрика класса" является объектом в
статической памяти, а экземпляры она размещает в динамической. Мне не хватает ещё двух
статических типов, чтобы реализовать оставшуюся пару сочетаний, но я думаю, - как это можно
сделать уже видно. Так что правильное представление о реализации всего спектра у
вас сформИруется. Я надеюсь.
Важно, что мы, наконец, можем рассмотреть и действие функции APICoCreateInstance - именно она, получая CLSID-IID
выдаёт клиенту ссылку на экземпляр объекта. Именно она чаще всего упоминается в литературе
как "источник ссылки" на COM объект. Но функция эта - с лукавинкой. Ничего
не зная о том, что такое COM, имея о нём поверхностное представление как просто о
двоичной реализации ООП, "естественно" придти к заключению, что именно она
и есть некий аналог оператора new.
Сейчас, уже зная о COM вполне достаточно, вы никогда не придёте к такому выводу!
На самом деле, принимая на вход CLSID-IID,
CoCreateInstance последовательно вызывает
GoGetClassObject с аргументами
CLSID-IClassFactory и у полученного
"объекта типа" запрашивает ссылку на тот интерфейс, что указан IID.
"Объект типа" после этого освобождается. Для внешнего наблюдателя складывается
впечатление, что он и не создавался. Но это - не так. Если трассировать всё события,
то это можно увидеть. А наш пример эти события как раз и трассирует, так что, как
работает CoCreateInstance "изнутри" вы сможете увидеть
воочию.
О сказанном предвижу вопрос "почему?"... Всё имеет вполне рациональное объяснение.
Во-первых, в очень многих случаях "объект типа" как раз и создаётся только для
того, чтобы получить единственную ссылку на "объект экземпляра". И для большего -
не нужен. Задача типовая. Значит, переложив её на систему, можно хоть немного облегчить
клиента... Облегчение это маленькое, но ведь и клиентов, которые выполняют именно такую
последовательность действий - много. Так что совокупная экономия может быть существенной.
Во-вторых, и это пока для нас неочевидно (не дошли ещё по порядку изложения) запрос на
ссылку на экземпляр может поступить из другого процесса, а то и с другой машины.
И "перегонка" указателя на промежуточный и сугубо локальный "объект типа"
от сервера к клиенту с полномасштабным маршалированием - ровно вдвое снижает эффективность
вызова. Поэтому исполнение "типовой последовательности действий" локально опять
выгодно. Вот почему функция CoCreateInstance присутствует в
системе.
Но, конечно, если вам нужно получить не одну, а несколько ссылок на экземпляры
одного и того же статического типа, серия вызовов CoCreateInstance
становится менее выгодной, чем раздельное получение ссылки на объект типа
по CoGetClassObject и явный вызов
IClassFactory::CreateInstance.
Чем когда пользоваться должно быть понятно.
Наш пример, в той же технологии, как и пример №3 показывает транспаранты о том, что
внутри сервера произошло событие. Я удалил транспаранты, связанные с работой
DllRegisterServer и других экспортируемых функций, но оставил
транспарант, который показывает, когда удаляется блок памяти - вы должны увидеть какая
внутренняя работа совершается при вполне внешне "невинных" действиях. А как
загружается и выгружается сервер вы видели из прошлого примера.
На что следует обратить внимание! Я понимаю, что к хорошему быстро привыкаешь и
конечно мог бы совместить вызов Release с крестиком закрытия окна.
Но намеренно оставил всё так, как продолжается, начиная с примера №2 - вы должны сами
явно вызывать Release. Сделано это для того, чтобы вы могли
увидеть как связаны элементарные COM-объекты, которые реализуют статические и
нестатические аспекты типа (ещё и поэтому "фабрика класса" статического типа
Banzai сделана не "как обычно"). Например, вы можете получить ссылку на
объект типа, создать экземпляр, уничтожить объект типа и продолжать использовать
экземпляр...
Для облегчения понимания "кто есть who" в состав диалогов введены дополнительные
поля - они показывают численное значение адреса интерфейса COM-объекта, который
"привязан" к этому "маленькому окну". Сделано это очень просто -
желающие убедиться могут заглянуть в код, который обрабатывает событие WM_INITDIALOG.
Обратите внимение на то, каково будет это численное значение когда вы создаёте новый
экземпляр объекта или просто клонируете ссылку. Обратите особенное внимание - когда адрес
меняется? Например, для статического типа Hello "объект типа" реализован
в статической памяти сервера, поэтому сколько бы вы ни запрашивали ссылок на "объект
типа" сервер всё время будет возвращать одно и то же численное значение. А вот
"объект экземпляра" у этого типа реализован в динамической памяти сервера, поэтому
каждое новое создание экземпляра будет давать и новый адрес ссылки. Клонирование ссылки,
естественно, нового объекта не создаёт - поэтому при клонировании любой ссылки адрес не
будет изменяться, как бы объект ни был реализован.
Посмотрите, что произойдёт, если в любом месте у любого объекта на один раз
больше вызвать Release, нежели AddRef.
Не забывайте только, что "объект типа" статического типа Hello и
"объект экземпляра" статического типа Banzai реализованы как статические
объекты сервера, т.е. на них "Release не действует".
А со следующего выпуска мы начинаем новый, большой раздел - мы пересекаем границу процесса.