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

       

Множественные интерфейсы и имена методов


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

Иерархия транспортных интерфейсов из этой главы содержит конфликт имен. В интерфейсе ICar (автомобиль) имеется метод, названный GetMaxSpeed (развить максимальную скорость). В интерфейсах IBoat (лодка) и IPlane (самолет) также имеются методы, именуемые GetMaxSpeed с идентичной сигнатурой. Это означает, что при использовании множественного наследования разработчик класса пишет метод GetMaxSpeed один раз, а компилятор и компоновщик инициализируют таблицы vtbl, совместимые с ICar, IBoat и IPlane так, чтобы они указывали только на эту реализацию.

Возможно, это вполне разумное поведение для большого числа реализации. Но что если объекту нужно было вернуть другую максимальную скорость, зависящую от интерфейса, на который был сделан запрос? Поскольку имя и сигнатуры одинаковы, то необходимо принимать неординарные меры для разрешения множественных реализации конфликтного метода. Один из возможных способов состоит в создании промежуточного класса C++, производного от интерфейса и реализующего конфликтный метод путем создания чисто виртуального вызова неконфликтного имени:

struct IXCar : public ICar { // add new non-clashing method as pure virtual // добавляем новый неконфликтный метод как чисто виртуальный virtual HRESULT STDMETHODCALLTYPE GetMaxCarSpeed(long *pval) = 0; // implement clashing method by upcalling // non-clashing implementation in derived class // реализуем конфликтный метод путем вызова // неконфликтной реализации в производном классе STDMETHODIMP GetMaxSpeed(long *pval) { return GetMaxCarSpeed(pval); } };


Допуская, что интерфейсы IBoat и IPlane подвергнуты подобной операции, можно реализовывать различные версии GetMaxSpeed простым наследованием от расширенных версий интерфейсов и переопределением неконфликтных версий каждого метода GetMaxSpeed:

class CarBoatPlane : public IXCar, public IXBoat, public IXPlane { public: // Unknown methods - методы IUnknown STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void);

// IVehicle methods - методы IVehicle // do not override GetMaxSpeed! // не подменяем GetMaxSpeed!

// ICar methods - методы ICar STDMETHODIMP Brake(void);

// IBoat methods - методы IBoat STDMETHODIMP Sink(void);

// IXPlane methods - методы IXPlane STDMETHODIMP TakeOff(void);

// upcalled from IXCar::GetMaxSpeed // вызвано из IXCar::GetMaxSpeed STDMETHODIMP GetMaxCarSpeed(long *pval);

// upcalled from IXBoat::GetMaxSpeed // вызвано из IXBoat::GetMaxSpeed STDMETHODIMP GetMaxBoatSpeed(long *pval);



// called from IXPlane::GetMaxSpeed // вызвано из IXPlane::GetMaxSpeed STDMETHODIMP GetMaxPlaneSpeed(long *pval); }

Рисунок 4.6 иллюстрирует представление этого класса и форматы таблиц vtbl. Отметим, что конфликтный метод GetMaxSpeed не реализован в этом классе. Поскольку каждый из базовых классов CarBoatPlane подменяет этот чисто виртуальный метод, то CarBoatPlane не нуждается в создании своей собственной реализации. Действительно, если бы в CarBoatPlane нужно было подменить GetMaxSpeed, то одна его реализация этого метода подменила бы версии, вызываемые из каждого базового класса, аннулировав результат использования IXCar, IXBoat и IXPlane. В силу этой проблемы данная технология годится только в тех ситуациях, когда можно быть уверенным, что класс реализации (или любые возможные производные классы) никогда не станет подменять конфликтный метод.



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


Спецификация СОМ не требует, чтобы объект был реализован как класс C++. Хотя существует весьма естественное соответствие между объектами СОМ и классами C++, базирующимися на множественном наследовании, это всего лишь одна из возможных технологий реализации. Для создания объекта СОМ может быть использована любая программная технология, производящая таблицы vtbl в нужном формате и удовлетворяющая правилам СОМ для QueryInterface. Один стандартный метод разрешения конфликтов имен состоит в реализации интерфейсов с конфликтующими именами как отдельных классов C++ и последующей компоновке целевого класса C++ из экземпляров этих отдельных классов. Для гарантии того, что каждый из этих составных элементов данных появится во внешнем мире как единый объект СОМ, часто назначается одна главная реализация QueryInterface, которой каждый составной элемент данных будет передавать функции. Следующий код демонстрирует эту технологию:

class CarPlane { LONG m_cRef; CarPlane(void) : m_cRef(0) {} public: // Main IUnknown methods // Главные методы IUnknown STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); private: // define nested class that implements ICar // определяем вложенный класс, реализующий ICar struct XCar : public ICar { // get back pointer to main object // получаем обратный указатель на главный объект inline CarPlane* This(); STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP GetMaxSpeed(long *pval); STDMETHODIMP Brake(void); };

// define nested class that implements IPlane // определяем вложенный класс, реализующий IPlane struct XPlane : public IPlane { // Get back pointer to main object // получаем обратный указатель на главный объект inline CarPlane* This(); STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP GetMaxSpeed(long *pval); STDMETHODIMP TakeOff(void); }; // declare instances of nested classes // объявляем экземпляры вложенных классов XCar m_xCar; XPlane m_xPlane; };



Использование вложенных классов не является обязательным, но оно подчеркивает, что эти подчиненные классы не имеют смысла вне контекста класса CarPlane. Рисунок 4.7 показывает двоичное размещение этого класса и размещения соответствующих vtbl.



Отметим, что имеется два определения вложенного класса, по одному для каждого реализованного им интерфейса. Это позволяет разработчику объекта обеспечить две различных реализации GetMaxSpeed:

STDMETHODIMP CarPlane::XCar::GetMaxSpeed(long *pn) { // set *pn to max speed for cars // устанавливаем *pn для максимальной скорости автомобилей }

STDMETHODIMP CarPlane::XPlane::GetMaxSpeed(long *pn) { // set *pn to max speed for planes // устанавливаем *pn для максимальной скорости самолетов }

Тот факт, что две реализации GetMaxSpeed встречаются в различных определениях вложенных классов, позволяет определить метод дважды и к тому же гарантирует то, что таблицы vtbl, соответствующие ICar и IPlane, будут иметь различные элементы для GetMaxSpeed.

Необходимо также отметить, что хотя класс CarPlane, находящийся на верхнем уровне, реализует методы IUnknown, он не наследует никакому производному от IUnknown классу. Вместо этого объекты CarPlane имеют элементы данных, которые наследуют интерфейсам СОМ. Это значит, что вместо того, чтобы использовать static_cast для вхождения в объект и нахождения определенного указателя vptr, реализация QueryInterface в CarPlane должна возвратить указатель на тот элемент данных, который реализует запрашиваемый интерфейс:

STDMETHODIMP CarPlane::QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(&m_xCar); else if (riid == IID_IVehicle) *ppv = static_cast<IVehicle*> (&m_xCar); else if (riid == IID_ICar) *ppv = static_cast<ICar*>(&m_xCar); else if (riid == IID_IPlane) *ppv = static_cast<IPlane*>(&m_xPlane); else return (*ppv = 0), E_NOINTERFACE;

((IUnknown*)(*ppv))->AddRef(); return S_OK; }



Для обеспечения идентификации объекта каждый из элементов данных CarPlane должен или воспроизвести этот код в своей собственной реализации QueryInterface, или просто передать управление главной функции QueryInterface в CarPlane. Чтобы осуществить это, необходим механизм перехода к главному объекту со стороны функции-члена составного элемента данных. Определение класса CarPlane::XCar содержит встроенную подпрограмму, которая использует фиксированные смещения для вычисления указателя this главного объекта от указателя this составного элемента данных.

inline CarPlane CarPlane::XCar::This(void) { return (CarPlane*)((char*)this // ptr to composite - указатель на композит - offsetof (CarPlane, m_xCar)); }

inline CarPlane CarPlane::XPlane::This(void) { return (CarPlane*)((char*)this // ptr to composite - указатель на композит - offsetof(CarPlane, m_xPlane)); }

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

STDMETHODIMP CarPlane::XCar::QueryInterface(REFIID r, void**p) { return This()->QueryInterface(r, p); }

STDMETHODIMP CarPlane::XPlane::QueryInterface(REFIID r, void**p) { return This()->QueryInterface(r, p); }

Такая же передача this потребуется для AddRef и Release для получения обобщенного представления о времени жизни объекта в случае составных (композитных) элементов данных.

Технология, основанная на использовании композиции для реализации интерфейсов, требует значительно больше кода, чем при простом множественном наследовании. Кроме того, качество генерированного кода, вероятно, не лучше (а возможно, и хуже), чем в случае множественного наследования. Из того факта, что классу CarPlane не понадобилось наследовать ни одному интерфейсу СОМ, следует, что композиция является разумной технологией для внесения СОМ в старые библиотеки классов.


Например, MFC (Microsoft Foundation Classes — библиотека базовых классов Microsoft) использует эту технологию. Причиной применения композиции при реализации новых классов является получение отдельных реализации метода, определенного одинаково более чем в одном интерфейсе. К счастью, стандартные интерфейсы, определяемые СОМ, очень редко создают такие конфликты, а те немногие, которые создают, почти всегда преобразуются в семантически эквивалентные функции. Для разрешения коллизий, подобных тем, что произошли в сценарии с GetMaxSpeed, композиция, вероятно, и не требуется, так как в первом приближении для преобразования двойников в уникальные объекты достаточно использования промежуточных классов. Эта методика проста, эффективна и фактически не требует дополнительного кода. Основная причина использования композиции в новом коде заключается в том, что нужно обеспечить подсчет ссылок в каждом интерфейсе.

Иногда желательно разместить ресурсы в объекте на базе уже использующихся интерфейсов. В то же время из использования множественного наследования для реализации интерфейсов СОМ следует, что в каждой таблице vtbl будет использована только одна реализация AddRef и Release. Хотя можно выявить первый запрос на заданный интерфейс и разместить ресурсы по требованию:

STDMETHODIMP QueryInterface(REFIID riid, void **ppv) { if (riid == IID_IBoat) { // allocate resource the first time through // размещаем ресурсы при первом проходе if (m_pTonsOfMemory == 0) m_pTonsOfMemory = new char[4096 * 4096]; *ppv = static_cast<IBoat*>(this); } else if ... }

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



class CarBoatPlane : public ICar, public IPlane { LONG m_cRef; char *m_pTonsOfMemory; CarBoatPlane (void) : m_cRef(0), m_pTonsOfMemory (0) {} public: // IUnknown methods - методы IUnknown STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); // IVehicle methods - методы IVehicle STDMETHODIMP GetMaxSpeed(long *pMax); // ICar methods - методы ICar STDMETHODIMP Brake(void); // IPlane methods - методы IPlane STDMETHODIMP TakeOff(void);

// define nested class that implements IBoat // определяем вложенный класс, реализующий IBoat struct XBoat : public IBoat { // get back pointer to main object // получаем обратный указатель на главный объект inline CarBoatPlane* This(); LONG m_cBoatRef; // per-interface ref count // счетчик ссылок на каждый интерфейс XBoat(void) : m_cBoatRef(0) {} STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); STDMETHODIMP GetMaxSpeed(long *pval); STDMETHODIMP Sink(void); }; XBoat m_xBoat; };

Реализация AddRef и Release из IBoat могут теперь следить за числом ссылок типа IBoat и высвободить ресурсы, когда они больше не нужны:

STDMETHODIMP_(ULONG) CarBoatPlane::XBoat::AddRef() { ULONG res = InterlockedIncrement(&m_cBoatRef); if (res == 1) { // first AddRef - первый AddRef // allocate resource and forward AddRef to object // размещаем ресурсы и пересылаем AddRef на объект This()->m_pTonsOfMemory = new char[4096*4096]; This()->AddRef(); } return res; }

STDMETHODIMP_(ULONG) CarBoatPlane::XBoat::Release() { ULONG res = InterlockedDecrement(&m_cBoatRef); if (res == 0) { // last Release - последний Release // free resource and forward Release to object // освобождаем ресурсы и пересылаем Release на объект delete [] This()->m_pTonsOfMemory; This()->Release(); } return res; }

Чтобы эта методика работала, все пользующиеся интерфейсными указателями должны придерживаться требований спецификации СОМ: функция Release должна вызываться через указатель, посредством которого вызывается соответствующая функция AddRef.


Поэтому правильной концовкой QueryInterface будет следующая:

((IUnknown*)(*ppv))->AddRef(); // use exact ptr // используем точный указатель return S_OK;

вместо такого:

AddRef(); // just call this->AddRef // только вызов this->AddRef return S_OK;

Первый вариант гарантирует, что если клиент пишет следующий правильный код

IBoat *pBoat = 0; HRESULT hr = pUnk->QueryInterface(IID_IBoat, (void**)&pBoat); if (SUCCEEDED(hr)) { hr = pBoat->Sink(); pBoat->Release(); }

то для AddRef и для Release обязательно будет использовано одно и то же значение указателя.

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

class CarBoatPlane : public ICar, public IPlane { public: struct XBoat : public IBoat { // composite QI/AddRef/Release/This() // композит из QI/AddRef/Release/This() IMPLEMENT_COMPOSITE_UNKNOWN(CarBoatPlane, XBoat, m_xBoat) STDMETHODIMP GetMaxSpeed(long *pval); STDMETHODIMP Sink(void); };

XBoat m_xBoat;

// IVehicle methods // методы IVehicle STDMETHODIMP GetMaxSpeed(long *pMax);

// ICar methods // методы ICar STDMETHODIMP Brake(void);

// IPlane methods // методы IPlane STDMETHODIMP TakeOff(void);

// standard heap-based QI/AddRef/Release // стандартные расположенные в "куче" QI/AddRef/Release

IMPLEMENT_UNKNOWN(CarBoatPlane) BEGIN_INTERFACE_TABLE(CarBoatPlane) IMPLEMENTS_INTERFACE_AS(IVehicle, ICar) IMPLEMENTS_INTERFACE(ICar) IMPLEMENTS_INTERFACE(IPlane) // macro that calculates offset of data member // макрос, вычисляющий смещение элемента данных IMPLEMENTS_INTERFACE_WITH_COMPOSITE(IBoat, XBoat, m_xBoat) END_INTERFACE_TABLE() };

В приведенном выше определении класса опущены только определения методов объекта вне QueryInterfасе, AddRef и Release.


Два новых макроса, использованных в определении класса, определяются следующим образом:

// inttable.h // (book-specific header file) // (заголовочный файл, специфический для данной книги)

#define COMPOSITE_OFFSET(ClassName, BaseName, \ MemberType, MemberName) \ (DWORD(static_cast<BaseName*>(\ reinterpret_cast<MemberType*>(0x10000000 + \ offsetof(ClassName, MemberName)))) - 0х10000000)

#define IMPLEMENTS_INTERFACE_WITH_COMPOSITE(Req,\ MemberType, MemberName) \ { &IID_##Req,ENTRY_IS_OFFSET, COMPOSITE_OFFSET(_IT,\ Req, MemberType, MemberName) },

// impunk.h // (book-specific header file) // (заголовочный файл, специфический для данной книги)

#def1ne IMPLEMENT_COMPOSITE_UNKNOWN(OuterClassName,\ InnerClassName, DataMemberName) \ OuterClassName *This() \ { return (OuterClassName*)((char*)this - \ offsetof(OuterClassName, DataMemberName)); }\ STDMETHODIMP QueryInterface(REFIID riid, void **ppv)\ { return This()->QueryInterface(riid, ppv); }\

STDMETHODIMP_(ULONG) AddRef(void) \ { return This()->AddRef(); }\ STDMETHODIMP_(ULONG) Release(void) \ { return This()->Release(); }

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


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