Интерфейс IUnknown

Интерфейс IUnknown и наследование интерфейсов

Доступ к COM-объектам, работа с памятью

Для того, чтобы получить доступ к методам COM-объекта, необходимо получить указатель на его интерфейс, для этого могут быть выполнены следующие действия:

1. Вызвать API-функцию, создающую объект одного типа и возвращающую указатель на интерфейс;

2. Вызвать функцию члена через интерфейс другого объекта, указатель на который уже известен;

3. Реализовать свой собственный интерфейс, через который другие объекты будут передавать указатель на свой собственный интерфейс;

4. Вызвать API-функцию, которая по данному идентификатору класса объектов, создает объект, возвращаемый указатель на интерфейс.

Для того чтобы осуществить последний вариант получения доступа к COM-объекту, необходимо точно знать и уметь уникально идентифицировать класс объектов, для этого существуют следующие способы:

1. Уникально идентифицировать по имени API-функции, создающей объект;

2. Уникально идентифицировать по позиции объекта в иерархии компоненты;

3. Уникально идентифицировать по внутреннему имени класса интерфейса;

4. Уникально идентифицировать по известному уникальному идентификатору CLSID.

Важная часть любой программы – передача параметров функциям и процедурам, технология COM – не исключение, так как необходимо передавать параметры между компонентами.

Лучше передавать параметры по значению, данный вопрос рассматривался в теме про RPC. Для передачи параметров по указателю должно существовать специальное соглашение:

1. Входной параметр ([in]) – память должна выделять вызывающая;

2. Выходной параметр ([out]) – память должна выделять вызываемая, а освобождать вызывающая;

3. Входной/выходной параметр ([in/out]) – память должна выделяться вызывающей, освобождаться также вызывающей, но при необходимости память может быть перевыделена вызываемой.

Сервис COM для распределение памяти задачи происходит через объект «распределитель», который поддерживает интерфейс malloc.

Также вводятся дополнительные правила для работы с памятью:

1. Выходной параметр ([out]) – в случае ошибки указатель должен быть установлен в NULL;

2. Входной/выходной параметр ([in/out]) – в случае ошибки, указатель можно как оставить без изменения, так и установить в NULL.

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

Наследование в стандартных COM-интерфейсах используется довольно скупо. Все интерфейсы должны быть унаследованы от интерфейса IUnknown. IUnknown содержит три жизненно важных для COM метода: QueryInterface, AddRef и Release. Все COM-объекты должны реализовать интерфейс IUnknown, поскольку он обеспечивает возможность получать поддерживаемые объектом интерфейсы с помощью QueryInterface, и управлять сроком жизни объекта через AddRef и Release.

Интерфейс IUnknown инкапсулирует 2 операции:

1. Управлением временем жизни объекта;

2. Навигация между объектами.

Высокоуровневые средства разработки (такие, как VB) полностью скрывают реализацию IUnknown, облегчая тем самым работу программиста. Низкоуровневые средства разработки (например, VC++) напротив, дают полный доступ к исходным текстам, реализующим IUnknown. На C++ или Delphi можно самостоятельно реализовать IUnknown. Но проще воспользоваться стандартными реализациями. Для C++ самый удобный, но в тоже время гибкий и очень компактный способ реализации IUnknown – воспользоваться библиотекой ATL (Active Template Library). ATL – это библиотека, главным образом состоящая из набора шаблонов C++. ATL упрощает работу с COM, предоставляя реализации для многих стандартных интерфейсов. Эта библиотека входит в поставку MS VC++ и Borland C++ Builder. В сочетании с визуальными средствами разработки этих сред ATL позволяет очень быстро создавать сложные приложения, базирующиеся на COM.

8.3.2. Получение указателя на интерфейс – QueryInterface

Метод QueryInterface (полный вид – HResult QueryInterface (REEFID riid, void ** ppv)) предоставляет интерфейс IUnknown::QueryInterface COM-объекта, который пытается получить указатель определенного интерфейса. Использование QueryInterface для COM-объекта аналогично выполнению операции приведения в управляемом коде. Вызов объекта с этим методом приводит к увеличению счетчика ссылок для указателя интерфейса до возвращения указателя. Каждый раз по окончании работы с указателем для уменьшения значения счетчика ссылок следует использовать Marshal.Release.

Если возвращаемое значение равно NOERROR, это означает что запрашиваемый интерфейс существует.

Основные правила вызова функции QueryInterface:

1. Любой вызов QueryInterface через любой интерфейс объекта должен всегда возвращать одну и ту же величину;

2. После создания объект должен поддерживать все свои интерфейсы;

3. Любая QueryInterface:

· Рефлективна – спецификация СОМ требует, чтобы запрос QueryInterface через интерфейсный указатель всегда достигал цели, если запрошенный тип соответствует типу указателя, с помощью которого произведен запрос (рис. 8.1). Это означает, что QI(A)->A всегда должен быть верным.

Рис. 8.1 Демонстрация рефлективности QueryInterface

 

· Симметрична – спецификация СОМ требует, чтобы, если запрос QueryInterface на интерфейс B удовлетворяется через интерфейсный указатель типа A, то запрос QueryInterface на интерфейс A того же самого объекта через результирующий интерфейсный указатель типа В всегда был успешным (рис. 8.2). Это значит, что если верно QI(A)->B, то также должно быть верным QI(QI(A)->B)->A.

Рис. 8.2 Демонстрация симметричности QueryInterface

 

· Транзитивна – спецификация СОМ требует также, чтобы, если запрос QueryInterface на интерфейс В удовлетворяется через интерфейсный указатель типа A, а второй запрос QueryInterface на интерфейс C удовлетворяется через указатель типа В, то запрос QueryInterface на интерфейс C через исходный указатель типа A был бы также успешным (рис. 8.3). Это означает, что если верно QI(QI(A)->B)->C, то должно быть верным и QI(A)->C.

Рис. 8.2 Демонстрация транзитивности QueryInterface

 

8.3.3. Управление временем жизни объекта – подсчет ссылок

Для каждой реализации интерфейса ведется подсчет ссылок (Reference counting). Любой код, который хочет хранить указатель (ссылку) на интерфейс, должен увеличить счетчик ссылок этого интерфейса. Сделать это можно, вызвав у интерфейса метод AddRef (Каждый COM-интерфейс унаследован от IUnknown.). Пока у объекта счетчик ссылок не равен нулю, этот объект не будет уничтожен. Чтобы уменьшить счетчик, надо вызвать метод Release того же интерфейса. COM-объект жив, пока хотя бы у одного интерфейса счетчик ссылок больше нуля. Обычно сразу после создания объекта имеется только один указатель на его интерфейс со счетчиком ссылок, установленным в единицу. Так что, если в этот момент вызвать у его интерфейса метод Release, объект автоматически уничтожится.

Есть строгие правила, описывающие, когда и кому надо вызывать AddRef и Release. Эти правила описаны в спецификации COM. Самым главным правилом является то, что если какая-нибудь функция возвращает указатель на интерфейс, то она должна увеличивать счетчик ссылок этого интерфейса. Этому правилу подчиняются даже CoCreateInstance и QueryInterface. Если же указатель передается в функцию, и ей не надо сохранять его для использования после выхода из этой функции, то она может не увеличивать счетчик ссылок на этот интерфейс.

Подсчет ссылок позволяет эффективно управлять временем жизни COM-объекта в распределенной среде. Счетчик ссылок необходим еще и потому, что СОМ является компонентной моделью, в которой компоненты могут параллельно использоваться любым количеством других компонентов. Если в такой среде применять физическое удаление, такая система не будет жизнеспособна из-за невозможности обеспечения правильного срока жизни объектов. Единственная альтернатива – сохранение объекта в живых до завершения работы сервера. При таком подходе, во-первых, пришлось бы управлять сроком жизни самого сервера, а во-вторых, это привело бы к перерасходу бесценных системных ресурсов.

В распределенных системах DCOM берет на себя управление подсчетом ссылок на удаленные объекты. Даже если клиент потерпит крах и не сможет самостоятельно освободить сделанные им ссылки, DCOM через некоторое время сделает это за него. Чтобы избежать чрезмерной услужливости DCOM, proxy объектов, создаваемые тем же DCOM, регулярно сообщают о своем существовании, производя так называемый ping. Однако есть две проблемы, которые портят радужную картину.

Первая – это то, что если следить за ссылками вручную, довольно-таки просто ошибиться и забыть сделать AddRef или Release. Лишний AddRef приведет к тому, что объект не освободится до закрытия клиентского приложения. Лишний Release приведет к появлению неверных указателей в клиентском приложении, что, в свою очередь, приводит к его краху. Как уже говорилось, сетевым подсчетом ссылок занимается сам DCOM, и проблема со ссылками в клиентском приложении не может повлиять на работоспособность сервера. В некоторых языках проблема подсчета ссылок в клиентском приложении решена путем встраивания механизма автоматического подсчета ссылок в сам язык. Так сделано в VB и Delphi. А в C++ эта проблема может быть решена средствами самого языка. Для ее решения используются три свойства языка.

1. Автоматический вызов деструктора при уничтожении переменных.

2. Автоматическое уничтожение стековых переменных.

3. Поддержка шаблонов. Используя две предыдущие возможности, можно создать классы-обертки для интерфейсов, которые автоматически вызывают Release при своем уничтожении. Но такую обертку пришлось бы писать для каждого интерфейса. Шаблоны позволяют написать одну реализацию такого класса-обертки, а компилятор сам создаст реализацию для каждого интерфейса.

В сочетании с перегружаемыми операторами классы-обертки практически полностью снимают проблему подсчета ссылок в C++. Лучшей реализацией такого класса-обертки является шаблон CComPtr<*> из библиотеки ATL. Переменная, созданная на базе такого класса-обертки, называется Smart Pointer, то есть «умный указатель». Объявив вместо простого указателя на интерфейс (например, IStak * pIStak) Smart Pointer, (CComPtr<IStak> * spIStak), можно не заботиться о правильности подсчета ссылок.

Вторая проблема – это то, что если два объекта делают ссылки друг на друга, то они попадают в deadlock, взаимно блокируя друг друга. Эта ситуация возникает нечасто, но если уж возникает, ее решение отнимает много нервов и времени. Есть два пути решения этой проблемы. Один – нарушить соглашения COM, и не делать увеличения счетчика ссылок одному из объектов. При этом время жизни объекта, которому не делается увеличение счетчика, должно быть заведомо больше, чем время, на которое требуется указатель на его интерфейс. Этот путь опасен и не всегда работает в сетевом окружении. Второй путь – создать промежуточный (третий) объект, который и подсунуть объекту, приводящему к взаимоблокировке. При этом на третий объект делается две ссылки, а на первый ссылок не делается вообще. В момент, когда первый объект уничтожается, он отпускает первый и третий. Далее уничтожается второй, и, в свою очередь, отпускает третий, что приводит к уничтожению третьего объекта. Второй способ избегания взаимоблокировок является более предпочтительным, но требует дополнительного кода. Очень плохо, что COM не взял на себя решение проблем с взаимоблокировкой.