netlib.narod.ru | < Назад | Оглавление | Далее > |
Давайте в общих чертах познакомимся с составной объектной моделью (COM — Component Object Model) и работой COM-интерфейсов.
Интерфейс представляет собой набор функций, объединенных общим назначением. Интерфейсные функции напоминают функции классов C++, за тем исключением, что функции интерфейса только определяются, но не реализуются. Можно считать их чем-то вроде плана для класса C++, который вы только собираетесь написать.
COM-объектом называют фрагмент кода, реализующий один или несколько интерфейсов. COM-объекты могут быть чрезвычайно простыми (например, объекты классов C++ со статической компоновкой) или чрезвычайно сложными (программа, работающая на сервере на другом краю Земли). Если вы представляете себе работу библиотек динамической компоновки (DLL), то по словам моего коллеги Дейла Роджерсона «COM-объекты при программировании на C++ играют ту же роль, что и DLL при программировании на C».
Любой COM-объект в обязательном порядке должен поддерживать интерфейс с именем IUnknown, обеспечивающий два базовых свойства COM-объектов: подсчет обращений и способность запрашивать другие интерфейсы. При помощи интерфейса IUnknown можно определить, какие еще интересующие вас интерфейсы поддерживаются объектом. Поясню сказанное на примере. Предположим, мы только что создали трехмерный объект средствами механизма визуализации и теперь хотим изменить его положение в макете. Поскольку нужная функция для изменения положения присутствует в интерфейсе IDirect3DRMFrame, желательно выяснить, поддерживается ли этот интерфейс созданным объектом, и, если результат проверки окажется положительным, — вызвать соответствующую функцию IDirect3DRMFrame для изменения положения объекта. Для определения того, поддерживается ли тот или иной интерфейс данным объектом, следует вызвать функцию IUnknown::QueryInterface:
HRESULT hr; IDirect3DRMFrame* pIFrame = NULL; hr = pIUnknown->QueryInterface(IID_IDirect3DRMFrame, (void**)&pIFrame);
Если вызов функции был успешным, следовательно, объект поддерживает интерфейс IDirect3DRMFrame и вы можете им пользоваться:
pIFrame->SetPosition(2, 4, 5);
Когда функция QueryInterface возвращает указатель на IDirect3DRMFrame, она также увеличивает на единицу счетчик обращений объекта. Следовательно, при завершении работы с указателем необходимо снова уменьшить значение счетчика обращений:
pIFrame->Release(); pIFrame = NULL;
Присваивать указателю NULL необязательно. Я делаю это лишь для того, чтобы отладчик смог перехватить любую попытку повторного использования указателя после освобождения интерфейса, на который он ссылался. Если вы любите макросы (лично я их не люблю), то всегда можете написать макрос для одновременного освобождения интерфейса и присвоения указателю NULL:
#define RELEASE(p) ((p)->Release(); (p) = NULL;)
ПРИМЕЧАНИЕ |
Я не люблю пользоваться макросами, потому что они скрывают конкретную программную реализацию. Мне кажется, что удобства от использования макросов за долгие годы отладки так и не оправдали тех хлопот, которые я имел с ними. |
Интерфейс IUnknown является базовым для всех остальных COM-интерфейсов, так что при наличии указателя на любой интерфейс можно вызвать QueryInterface для любого интерфейса, которым вы хотите пользоваться. Например, если у нас уже имеется указатель на интерфейс IDirect3DRMFrame (pIFrame) и необходимо выяснить, поддерживается ли интерфейс IDirect3DRMMesh объектом, на который ссылается указатель, проверка может выглядеть следующим образом:
HRESULT hr; IDirect3DRMMesh* pIMesh = NULL; hr = pIFrame->QueryInterface(IID_IDirect3DRMMesh, (void**)&pIMesh); if (SUCCEEDED(hr)) { // Использовать интерфейс для работы с сетками int i = pIMesh->GetGroupCount(); pIMesh->Release(); pIMesh = NULL; }
Это исключительно мощное средство, поскольку при наличии любого интерфейсного указателя на любой COM-объект можно определить, поддерживает ли данный объект тот интерфейс, которым вы хотите пользоваться. Единственное, чего нельзя сделать — получить список всех интерфейсов, поддерживаемых объектом.
ПРИМЕЧАНИЕ |
Все имена COM-интерфейсов начинаются с префикса I, по которому их можно отличить от классов C++ или других объектов. Я не стал пояснять этот факт в тексте, поскольку счел его достаточно очевидным, но потом решил, что, возможно, кто-то из читателей недоумевает по этому поводу. Префикс I также напомнит о том, что после завершения работы с интерфейсом необходимо вызвать Release. |
Кроме того, любой интерфейс или COM-объект может наследовать функции и свойства от другого интерфейса или целой группы интерфейсов. Однако выяснить это программными средствами невозможно; приходится смотреть на определение интерфейса. Например, если заглянуть в заголовочный файл d3drmobj.h в DirectX 2 SDK, вы увидите, что интерфейс IDirect3DRMFrame является производным от IDirect3DRMVisual. Следовательно, IDirect3DRMFrame заведомо поддерживает все функции интерфейса IDirect3DRMVisual. IDirect3DRMVisual является производным от IDirect3DRMObject, который в свою очередь порожден от IUnknown. Следовательно, интерфейс IDirect3DRMFrame поддерживает все функции IDirect3DRMFrame, а также все функции интерфейсов IDirect3DRMVisual, IDirect3DRMObject и IUnknown.
ПРИМЕЧАНИЕ |
Все интерфейсы механизма визуализации имеют префикс IDirect3D. Интерфейсы с префиксом IDirect3DRM относятся к более высокому уровню и предназначаются для работы с фреймами, фигурами, источниками света и т.д. Буквы RM являются сокращением от Retained Mode (то есть «абстрактный режим», в отличие от расположенного на более низком уровне непосредственного режима, Immediate Mode). |
На самом деле иерархия интерфейсов не так уж важна, потому что поддерживаемые объектом интерфейсы всегда можно определить функцией QueryInterface. Но если вы добиваетесь от приложения максимальной производительности, знание иерархии поможет обойтись без лишних вызовов функций.
Позвольте мне завершить этот краткий обзор COM-объектов на обсуждении того, как функции AddRef и Release интерфейса IUnknown применяются для совместного использования объектов. Предположим, мы хотим создать макет с несколькими деревьями. Описание дерева состоит из набора описаний вершин и граней, объединенных в сетку. Сетка является визуальным объектом, который можно присоединить к фрейму для того, чтобы задать его положение в макете. На самом деле одна и та же сетка может присоединяться к нескольким фреймам. Для создания нашего маленького леса понадобится одна сетка, определяющая форму дерева, и несколько фреймов для указания положений отдельных деревьев. Затем сетка присоединяется к каждому фрейму в качестве визуального элемента, для этого используется следующий вызов:
pIFrame->AddVisual(pIMesh);
Если взглянуть на код функции AddVisual в IDirect3DRMFrame, вы увидите что-нибудь в таком роде:
HRESULT IDirect3DRMFrame::AddVisual(IDirect3DRMVisual * pIVisual) { pIVisual->AddRef(); AddVisualToList(pIVisual); . . . }
Функция AddRef, входящая в интерфейс визуального элемента (визуальный интерфейс), увеличивает счетчик обращений к объекту-сетке. Зачем? Затем, что во время существования объекта-фрейма нельзя допустить уничтожения объекта, предоставляющего ему визуальный интерфейс. После уничтожения фрейма или удаления из него конкретного визуального интерфейса код фрейма освобождает объект-сетку:
pIVisual->Release();
Следовательно, после освобождения объекта-сетки последним фреймом счетчик обращений объекта упадет до нуля, и он самоуничтожится.
Какой же вывод следует сделать из всего этого? Если вы правильно обращаетесь с интерфейсами COM-объектов с помощью функций AddRef и Release, вам никогда не придется следить за тем, какие объекты присутствуют в памяти и когда их можно удалять, поскольку внутренний счетчик обращений самостоятельно справится с этой работой.
Позвольте дать пару последних рекомендаций по работе с COM-объектами. Любая функция, которая возвращает указатель на интерфейс, перед тем как вернуть управление, вызывает AddRef для увеличения счетчика обращений; после завершения работы с указателем необходимо вызвать Release, чтобы избежать ненужного хранения объектов в памяти. Если вы копируете указатель на интерфейс, вызовите AddRef для копии и освободите оба указателя функцией Release, когда надобность в них отпадет. Помните о том, что возврат указателя на интерфейс одной из ваших функций фактически равносилен его копированию. Перед тем, как возвращать указатель, не забудьте вызвать для него AddRef.
А теперь я собираюсь нарушить только что установленное правило. Если вы стопроцентно уверены в том, что делаете, то при копировании указателя можно обойтись и без вызова AddRef, однако при этом следует неуклонно следить за тем, чтобы функция Release была вызвана нужное количество раз. Лишние вызовы Release приведут к уничтожению используемого объекта, их нехватка — к непроизводительным расходам памяти. Просмотрев исходные тексты библиотеки 3dPlus, вы убедитесь, что во многих объектах C++ присутствует функция GetInterface. Она возвращает указатель на тот интерфейс, для которого данный класс C++ выступает в роли оболочки. Я сделал это из соображений удобства и производительности. Функция GetInterface не увеличивает счетчик обращений, так что при вызове какой-либо из функций GetInterface не следует вызывать Release для возвращаемого указателя.
ПРИМЕЧАНИЕ |
Книги, указанные в разделе «Библиография», содержат более подробную информацию о COM-объектах. |
netlib.narod.ru | < Назад | Оглавление | Далее > |