Сущность технологии COM

       

Реализация IUnknown


Имея описанные выше образцы клиентского использования, легко видеть, как реализовать методы IUnknown. Примем предложенную выше иерархию типов Dog/Cat. Чтобы определить С++-класс, который реализует интерфейсы IPug и ICat, нужно просто добавить к списку базовых классов самые последние в иерархии наследования версии интерфейсов:

class PugCat : public IPug, public ICat

При использовании наследования компилятор C++ обеспечивает совместимость двоичного представления производного класса с каждым базовым классом. Для класса PugCat это означает, что все объекты PugCat будут содержать указатель vptr, указывающий на таблицу vtbl, совместимую с IPug. Объекты PugCat также будут содержать указатель vptr, указывающий на вторую таблицу vtbl, совместимую с ICat. Рисунок 2.5 показывает, как интерфейсы в качестве базовых классов соотносятся с представлением объектов.

Поскольку все функции-члены в СОМ-определениях интерфейса являются чисто виртуальными, производный класс должен обеспечивать реализацию каждого метода, имеющегося в любом из его интерфейсов. Методы, общие для двух или более интерфейсов (например, QueryInterface, AddRef и т. д.) нужно реализовывать только один раз, так как компилятор и компоновщик инициализируют все таблицы vtbl так, чтобы они указывали на одну реализацию метода. Таков естественный побочный эффект от использования множественного наследования в языке C++.

Следующий код является определением класса, которое создает объекты, поддерживающие интерфейсы IPug и ICat:

class PugCat : public IPug, public ICat { LONG m_cRef; protected: virtual ~PugCat(void); public: PugCat(void); // IUnknown methods // методы IUnknown STDMETHODIMP QueryInterface(REFIID riid, void **ppv); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); // IAnimal methods // методы IAnimal STDMETHODIMP Eat(void); // IDog methods // методы IDog STDMETHODIMP Bark(void); // IPug methods // методы IPug STDMETHODIMP Snore(void); // ICat methods // методы ICat STDMETHODIMP IgnoreMaster(void); };


Отметим, что в классе должен быть реализован каждый метод, определенный в любом интерфейсе, от которого он наследует, так же, как и каждый метод, определенный в любых производных (implied) базовых интерфейсах (например, IDog, IAnimal). Для создания стековых фреймов, совместимых с СОМ, необходимо использовать макросы STDMETHODIMP и STDMETHODIMP_. При ориентации на платформы Win32, использующие компилятор Microsoft C++, заголовки SDK определяют эти два макроса следующим образом:

#define STDMETHODIMP HRESULT _stdcall #define STDMETHODIMP_(type) type _stdcall

Заголовочные файлы SDK также определяют макросы STDMETHOD и STDMETHOD_, которые можно использовать при определении интерфейсов без IDL-компилятора. В серийно выпускаемом программировании на СОМ эти два макроса не нужны.

Реализация AddRef и Release чрезвычайно прозрачна. Элемент данных m_cRef отслеживает, сколько неосвобожденных интерфейсных указателей удерживают объект. Конструктор класса приводит счетчик ссылок в нулевое состояние:

PugCat::PugCat(void) : m_cRef(0) // initialize reference count to zero // устанавливаем счетчик ссылок в нуль { }



Реализация AddRef в классе фиксирует путем увеличения счетчика ссылок, что вызывающий объект продублировал указатель интерфейса. Измененное значение счетчика ссылок возвращается для целей диагностики:

STDMETHODIMP_(ULONG) AddRef(void) { return ++m_cRef; }

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

STDMETHODIMP_(ULONG) Release(void) { LONG res = --m_cRef; if (res == 0) delete this; return res; }

Для кэширования обновленного счетчика ссылок необходимо использовать временную переменную, так как нельзя обращаться к элементам данных объекта после того, как объект уже уничтожен.

Заметим, что показанные реализации Addref и Release используют собственные операторы инкремента и декремента (увеличения и уменьшения на единицу).


Для простой реализации это весьма разумно, так как СОМ не допускает более одного потока для обращения к объекту до тех пор, пока конструктор не обеспечит явный многопоточный доступ (почему и как конструктор сделает это, подробно описано в главе 5). В случае объектов, доступных в многопоточной среде, для автоматического подсчета ссылок следует использовать подпрограммы Win32 InterlockedIncrement/InterlockedDecrement:

STDMETHODIMP_(ULONG) AddRef(void) { return InterlockedIncrement(&m_cRef); }

STDMETHODIMP_(ULONG) Release(void) { LONG res = InterlockedDecrement(&m_cRef); if (res == 0) delete this; return res; }

Этот код несколько менее эффективен, чем версии, использующие собственные операторы C++. Но, вообще говоря, разумнее использовать менее эффективные варианты InterlockedIncrement / InterlockedDecrement, так как известно, что они надежны во всех ситуациях и освобождают разработчика от необходимости сохранять две версии практически одинакового кода.

Показанные выше реализации AddRef и Release предполагают, что объект может размещаться только в динамически распределяемой области памяти (в "куче") с использованием С++-оператора new. В определении класса деструктор сделан защищенной операцией для обеспечения того, чтобы ни один экземпляр класса не был определен никаким другим способом. Однако иногда желательно иметь объекты, не размещенные в «куче». Для этих объектов вызов delete в последнем вызове Release был бы гибельным. Так как единственной причиной для того, чтобы объект в первую очередь поддерживал счетчик ссылок, была необходимость вызова delete this, допустимо оптимизировать счетчик ссылок для объектов, не содержащихся в динамически распределяемой области памяти:

STDMETHODIMP_(ULONG) GlobalVar::AddRef(void) { return 2; // any non-zero value is legal // допустима любая ненулевая величина }

STDMETHODIMP_(ULONG) GlobalVar::Release (void) { return 1; // any non-zero value is legal // допустима любая ненулевая величина }

Эта реализация использует тот факт, что результаты AddRef и Release служат только для сведения и не обязаны быть точными.



При наличии реализации AddRef и Release единственным еще не реализованным методом из IUnknown остается QueryInterface. Его реализации должны отслеживать иерархию типов объекта и использовать статические приведения типов для возврата правильного типа указателя для всех поддерживаемых интерфейсов. Для определения класса PugCat, рассмотренного ранее, следующий код является корректной реализацией QueryInterface:

STDMETHODIMP PugCat::QueryInterface(REFIID riid, void **ppv) { assert(ppv != 0); // or return E_POINTER in production // или возвращаем E_POINTER в реальный продукт if (riid == IID_IPug) *ppv = static_cast<IPug*>(this); else if (riid == IID_IDog) *ppv = static_cast<IDog*>(this); else if (riid == IID_IAnimal) // cat or pug? // кот или мопс? *ppv == static_cast<IDog*>(this); else if (riid == IID_IUnknown) // cat or pug? // кот или мопс? *ppv = static_cast<IDog*>(this); else if (riid == IID_ICat) *ppv = static_cast<ICat*>(this); else { // unsupported interface // неподдерживаемый интерфейс *ppv = 0; return E_NOINTERFACE; } // if we reach this point, *ppv is non-null // and must be AddRef'ed (guideline A2) // если мы дошли до этого места, то *ppv ненулевой // и должен быть обработан AddRef'ом ( принцип A2) reinterpret_cast<IUnknown*>(*ppv)->AddRef(); return S_OK; }

Использование static_cast более предпочтительно, чем традиционные приведения типа в стиле С:

*ppv = (IPug*)this;

так как вариант static_cast вызовет ошибку этапа компиляции, если произведенное приведение типа не согласуется с существующим базовым классом.

Заметим, что в показанной здесь реализации QueryInterface при запросе на интерфейс, поддерживающийся более чем одним базовым интерфейсом (например, IUnknown, IAnimal) приведение типа должно явно выбрать более определенный базовый класс. Например, для класса PugCat такой вполне безобидно выглядящий код не откомпилируется:

if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this);

Этот код не пройдет компиляцию, поскольку такое приведение типа является неоднозначным и может соответствовать более чем одному базовому классу.


Это было показано в случае FastString и IExtensibleObject из предыдущей главы. Вместо этого реализация должна более точно выбрать тип для приведения:

if (riid == IID_IUnknown) ppv = static_cast<IDog*>(this);

или

if (riid == IID_IUnknown) ppv = static_cast<ICat*>(this);

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

1Входы в таблицу vtbl для крайнего левого базового класса не требуют, чтобы «переходник» установщика (adjuster thunk) устанавливал указатель this перед вхождением в реализацию метода. Это применимо только к компиляторам, которые используют «переходники» установщика и располагают крайний левый базовый класс в самом верху расположения объекта. Компилятор Microsoft C++ удовлетворяет этому описанию.


Содержание раздела