COM. Агрегация и нотификация вообще и для Delphi в частности



Автор: Виталий Маматов
Специально для Королевства Delphi

Вступление.

К написанию данной статьи меня подтолкнула моя недавняя пробежка по вопросам относительно COM. Значительное их число сводилось к непониманию принципов организации агрегирования и нотификации. И, дабы лишний раз не утруждать себя напрасно, было решено, дать такой развёрнутый ответ с примером и сразу для всех.

Про литературу.

Самой полезной книжкой по технологии COM для меня стала неброская книжонка А.Кобирниченко "Visual Studio 6. Искусство программирования". Для примера два одинаковых понятия из тоже хорошей книжки Елмановой и Трепалина "Delphi 4 технология COM" но несколько путаной:
Apartment:

Елманова:
"Можно вызывать методы объекта только из того потока, где объект был создан. При этом одновременно можно создать несколько отличающихся объектов в разных потоках, но каждый объект обязан вызываться только из того потока, где был создан".

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

Теоретические экскурсы в данной статье, в основном, основаны на книге Кобирниченко.

Часть первая: Теория.

Агрегация.

При агрегировании внешний объект не реализует сам интерфейсы имеющиеся у внутреннего. Вместо этого он передаёт своему клиенту указатель указатель непосредственно на интерфейс внутреннего. Клиент напрямую общается с внутренним объектом. Взамен появляются требования к внутреннему объекту связанные с реализацией IUnknown внутреннего объекта. Все вызовы IUnknown внутреннего объекта должны делегироваться методам IUnknown внешнего объекта.

Нотификация.

Помимо методов реализующих входящие интерфейсы вызываемые клиентом, Объект может объявить исходящие интерфейсы, реализация которых возложена на самого клиента.

Исходящие интерфейсы являются расширением принципа уведомления, реализованного в составных документах всё уведомление в которых построено на IAdviseSink с ограниченным набором событий. Установление соединения на основе этого интерфейса требует всего одного вызова IOleObject::SetAdise.

При использовании точек соединения нужно четыре вызова: QueryInterface для получения IConnectionPointContainer, затем FindConnectionPoint для получения нужной точки соединения, затем Advise для передачи указателя на IUnknown исходящего интерфейса и, наконец, QueryInterface со стороны клиента для получения самого исходящего интерфейса. Вся эта деятельность, особенно в случае DCOM, может занять значительное время. Собственно по этому сама Microsoft рекомендует организовывать уведомление на основе собственных интерфейсов, похожих на IadviseSink, а не на основе точек соединения.

После такого введения, я думаю, вы уже готовы взять в руки инструмент Исследователя - IDE Delphi. В нашем случае ;).

Часть вторая: Махровая практика.

Агрегирование:

После тщательных поисков по Дельфийскому хелпу в данной предметной области, мною было обнаружено следующее: "TAggregatedObject is used as part of an aggregate that has a single controlling Iunknown" И приписка: "Note: For more information about aggregation, controlling objects, and interfaces, see the Inside OLE, second edition, by Kraig Brockschmidt" Ну, второе нам сейчас ни к чему, а вот с первым следует ознакомиться поближе.

Итак, вот он:


TAggregatedObject = class
private
FController: Pointer;
function GetController: IUnknown;
protected
{ IUnknown }
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
constructor Create(Controller: IUnknown);
property Controller: IUnknown read GetController;
end;
constructor TAggregatedObject.Create(Controller: IUnknown);
begin
FController := Pointer(Controller);
end;
function TAggregatedObject.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
Result := IUnknown(FController).QueryInterface(IID, Obj);
end;

и т.д. В общем ясен перец.

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

В ATL эта проблема решается применением шаблонов классов. В результате чего получается, что все методы реализованные в шаблоне _как_бы_ виртуальные. Это здорово придумано, берёшь любой метод, перекрываешь его и никаких гвоздей. Только надо учитывать, что после сборки, на этане выполнения, никакие фокусы с полиморфными вызовами у вас не пройдут.

Однако, вернёмся к нашим баранам. Просматривая, в некотором унынии, предков нашего обожаемого TAutoObject была обнаружена следующая забавная конструкция:


TComObject = class(TObject, IUnknown, ISupportErrorInfo)
..
protected
{ IUnknown }
function IUnknown.QueryInterface = ObjQueryInterface;
..
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
..
public
function ObjQueryInterface(const IID: TGUID; out Obj): HResult; virtual;
stdcall;
..
end;

Это явно не спроста, что же получается, уважаемая Borland, виртуализируем QueryInterface, сами забавляемся полученным результатом, а простым бедным программерам ни слова? Некрасиво!

Ну, думаю, с этим моментом также всё ясно, перекрываем ObjQueryInterface и дело в шляпе. Пошли дальше.

Нотификация:

Каждый школьник знает, что приём и передача нотификационных сообщений в COM производится через интерфейс IconnectionPointContainer. Дочитав MSDN до этого места, большинство программеров, тут же всё бросают и начинают реализовывать свою нотификацию на основе этого интерфейса. Но мы не так наивны, мы пойдём другим путём. На самом деле, реализовать собственную нотификацию, гораздо проще, чем это можно подумать. Работает как во внутренних, так и в локальных серверах, а заодно и в удалённых. Впрочем последнее лично не проверял. Идея: см. IAdviseSink, и мой пример по его мотивам.

Ну вот, теперь настало время которого вы так долго ждали, рассмотрим пример.

Пример.

Пример представляет из себя два проекта в одной группе. Внутренний сервер и клиент к нему. Для тех кто начал только отсюда напоминаю: Сервер надлежит регистрировать. Обычно для таких примеров выбирают что-нибудь абсолютно бесполезное. Я же, бесполезные вещи перестал писать одновременно с окончанием института, совпало так. Посему надлежит сделать что-нибудь полезное, но простое. Полезное, потому как смотри выше, а простое, потому как денег мне за это не платят. Пускай это будет таймер, а что, вещь в хозяйстве необходимая, редко какое приложение обходится без таймера. Полагаю, будет вполне естественно назвать его XTimer. Сказано, сделано. Отныне, я надеюсь, вы навсегда забудете где у вас находиться таймер на палитре компонентов и будете пользоваться только моим Aggregated XTimer. Далее, наш, во всех отношения полезный XTimer надо использовать как-то полезно. Для чего полезно можно использовать таймер? Правильно, для анимации. Что так же было реализовано. Картинка для анимации взята из SDK DirectX7. И последнее: в данном примере вы ни найдете ни одного комментария, так как комментировать там особо и нечего. Само документируемый код в чистом виде ;-). Также надеюсь у вас не возникнет впечатления, что использовано слишком много компонент. Засим всё.

Скачать проект: agrSample.zip (112.8 K)


Далее: DCOM permissions »»