Скачиваний:
100
Добавлен:
01.05.2014
Размер:
1.56 Mб
Скачать

Интерфейс iUnknown

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

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

Определимся, о чём мы говорим. Первый метод будет называться QueryInterface- он должен приниматьIIDдругого интерфейса, экспонируемого данным же объектом, и возвращать указатель на этот интерфейс. Второй метод будет называтьсяAddRef- он не имеет параметров, а каждый его вызов приводит к продвижению счётчика ссылок объекта вперед на единицу. Третий метод -Release. Его задача - обратнаяAddRef, а когда счётчик ссылок достигнет нуляReleaseже вызовет иdelete this.

Почему вместо одного метода по управлению счётчиком ссылок мы придумали два? Хотя бы потому, что код вызова метода без параметров - короче. Пусть на несколько байтов, но эти несколько байтов будут в клиенте встречаться всюду, где у нас размножается указатель. И суммарная добавка к коду может быть большой.

Итак, эти три метода:

  1. QueryInterface

  2. AddRef

  3. Release

мы можем оформить в отдельный интерфейс. Либо - мы можем прописывать в состав каждого другого интерфейса. Что лучше? И почему?

Допустим, эта функциональность выведена в совершенно отдельный интерфейс X, а все другие интерфейсы этого же объекта её не имеют. Что произойдёт? А произойдёт вот что - если при создании объекта мы попросим у сервера вернуть нам указатель на интерфейсX, то, владея этим указателем, мы легко получим указатели и на все другие интерфейсы объекта -QueryInterfaceже находится в составе интерфейсаX. Но вот если мы у сервера попросим вернуть любой другой интерфейс этого же объекта, то так с этим интерфейсом и останемся - в этом-то интерфейсе нетQueryInterface. Это вынуждает всякий раз запрашивать не нужный нам интерфейс, а именноX, и потом из него уже производить нужный нам указатель. Налицо двойная работа на стороне клиента - при получении указателя на интерфейс.

Если же эту функциональность помещать внутрь всякого интерфейса, который экспонирует объект, то у каждого интерфейса будут заниматься три дополнительных ячейки Vtbl, но зато нам не нужно будет выполнять никакой двойной работы на стороне клиента, всё управление объектом можно осуществить посредством его любого интерфейса. И как-то кажется, что второй способ значительно удобнее со стороны клиента, разумеется - повторно-то используется именно сервер.

Но ведь так можно построить и такой объект, у которого не будет ни одного "полезного" интерфейса? Можно. А вот эта служебная функциональность в любом случае должна быть. И можно будет размножать указатель, а потом - уничтожить объект. А всё что сверх того - определяется исключительно существом решаемой программистом задачи.

Описанная функциональность настолько фундаментальна, что без нее "вообще ничего не работает" - мы ведь и задумались над ней потому, что в нашей реализации компонентного взаимодействия не хватало очень существенных фрагментов. Можно ли её реализовать иначе? В деталях - да, в сущности - нет. Ведь причина наличия этой функциональности в составе объекта - философская. Если бы у нас компилятор знал точный статический тип объекта и тип этот был один и для клиента и для сервера, то конечно, компилятор мог бы реализовать и правильный вызов newи правильный вызовdelete*, и сам компилятор мог бы преобразовывать указатели на тип... Но, фактически, это означает, что и клиент и сервер должны располагаться в контексте одного и того же проекта - а мы как раз имеем совершенно обратные "начальные условия". У нас и клиент и сервер обязательно должны располагаться в разных проектах, в разных контекстах. У нас ведь - программирование из двоичных компонент.

Именно в силу этого обстоятельства нам сначала понадобилось часть таблиц времени компиляции встроить в сам объект, так чтобы они сохранялись и во время выполнения (vtbl), а теперь нам требуется нагрузить объект и такими функциями, как управление временем жизни и приведение типа. И избежать этого мы не можем - либо мы сами, либо компилятор.

Нужно особо заметить - что хотя мы и изучаем COM, то, что сейчас сформулировано имеет философскую природу. А, значит, в том или ином виде может быть найдено и в реализацииCORBAи должно быть вообще в любой двоично-компонентной технологии. "Обёртка" этого может быть разной, а вот сущность - одинакова.

В COMэта действительно фундаментальная сущность называется "интерфейсIUnknown", что в вольном переводе может звучать как "неизвестный интерфейс" и вызывает по меньшей мере некоторое недоумение - какой же это неизвестный интерфейс, если его наличие гарантировано в любом объекте? Однако, если это переводить, как "интерфейс Неизвестно Кто", - всё встаёт на свои места. Более того, двоичный компонентный объект будет объектомCOMв том и только в том случае, если он реализует, по меньшей мере, интерфейсIUnknown. Если такого интерфейса нет - это не объектCOM, хотя, как мы уже видели, объект может быть "двоично-компонентным" и без использованияCOM.

С этим интерфейсом нам волей-неволей придётся познакомиться очень близко - он есть "альфа и омега" всех интерфейсов, именно он определяет специальное поведение объекта. А сейчас пока что отметим - любой интерфейс COMдолжен быть унаследован от интерфейсаIUnknown. Должно быть понятно почему - именноIUnknownв составе любого интерфейса обеспечивает управление временем жизни объекта и приведение типа указателя. И для этого не нужно никаких дополнительных затрат клиента.

Собственно, только введение в состав объектов нашего предыдущего примера реализации IUnknownи отделяло наши объекты от превращения в "настоящие объектыCOM". Но для того, чтобы двигаться дальше нам надо точно познакомиться со спецификацией - что есть "интерфейсCOM" вC++и как он описывается.

С++- очень гибкий язык (сделайте в другом языке вызов функции с переменным числом параметров или откомпилируйте функцию без пролога/эпилога?), а именно в данном случае конструирования его гибкость является скорее недостатком. Поэтому, определяя проектные конструкцииC++, предназначенные для построения конструкцийCOMпостоянно приходится помнить о том, что компилятор должен быть специально ограничен. Это - категория ошибок, причина которых очень трудно диагностируется - ведь в разных единицах компиляции порознь-то все правильно! Например, стандартным соглашением о связях для компилятораC++является__cdecl, которое предписывает вызывающей процедуре не только помещать параметры в стек перед вызовом вызываемой процедуры, но и самой очищать стек после вызова. А вызываемая процедура этого не делает. Это - единственная возможность правильно оформить вызов функции с переменным числом параметров. Во всех других языках соглашение о связях -__stdcall, которое предписывает вызываемой процедуре самой очищать стек перед завершением. Стоит написать клиента, который оформит вызов метода в__stdcall(по умолчанию для его языка), а сам метод сервера будет написан в соглашении__cdecl(тоже по умолчанию, но дляC++), как вызов метода будет приводить в разрушению стека процесса и нарушению защиты памяти. Вы сможете "с ходу" припомнить почему бы это могло быть?

Такая ситуация вообще не может быть "проконтролирована автоматически". Избежать её можно только аккуратностью кодирования всего, что экспонируется наружу модуля. Необходимо постоянно помнить, что "нормальное внешнее имя" C++- декорировано, т.е. в двоичном модуле выглядит совсем не так, как оно выглядит внутри исходного текста. Необходимо помнить, что всякий экспонируемый наружу метод должен быть написан только в соглашении о связях__stdcall. Необходимо помнить, что методы экспонируемых интерфейсов не могут быть перегружены. Эти ограничения следуют только потому, что "другие компиляторы этого не умеют", аCOM- двоичная технология.

Другой распространённой ошибкой отнимающей пропасть собственной жизни программиста является неидентичность интерфейсов, используемых при сборке клиента и сервера. Технология COMс этим научилась бороться, как именно, мы до этого ещё дойдем. Но вC++интерфейс описывается только чисто абстрактным классом. Соответственно, где-то существует файл, подаваемый компилятору, где все эти классы и перечислены - файл с определением всех интерфейсов. И это только в теории "интерфейс - никогда не изменяемая сущность". Пока интерфейс не опубликован и разрабатывается, программист, естественно, его иногда и изменяет. Изменять-то изменяет, а вот всё ли (всех клиентов) после этого перекомпилирует и пересобирает? Об этом тоже всегда следует помнить.

Поэтому точное знание "какая именно конструкция языка есть интерфейс в COM" для программиста - насущная необходимость. В языкеC++всякий интерфейс описывается структурой - структура это класс, все члены которого являютсяpublic. Не исключено и описание интерфейса самой конструкциейclass. В включаемом файле<basetyps.h>имеются такие определения конструкций для определения частей интерфейса:

#define STDMETHODCALLTYPE __stdcall

#define interface struct

#define STDMETHOD(method) virtual HRESULT STDMETHODCALLTYPE method

#define STDMETHOD_(type,method) virtual type STDMETHODCALLTYPE method

#define DECLARE_INTERFACE(iface) interface iface

#define DECLARE_INTERFACE_(iface, baseiface) interface iface : public baseiface

а в файле <wtypes.h>:

typedefLONG HRESULT;

В файле же <unknwn.h>(с небольшими сокращениями и упрощениями) известный нам интерфейс описан как:

class IUnknown

{

public:

virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void ** ppvObject) = 0;

virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;

virtual ULONG STDMETHODCALLTYPE Release(void) = 0;

};

из чего можно заключить, что стандартный интерфейс это:

struct

{

...

или

class

{

public:

...

а стандартный метод, экспонируемый интерфейсом, должен иметь атрибуты:

virtual long __stdcall<имя метода>(<<списокпараметров метода>) = 0;

Учитывая, что в среде компиляции определено макро STDMETHOD(method), методы в интерфейсе можно описывать и так:

STDMETHOD(<имяметода>)(<список параметров метода>) = 0;

Ну, и немного о типе HRESULT- это тип стандартного значения, которое должен возвращатьCOM-метод. Все методы любых интерфейсов, кроме методовIUnknown::AddRefиIUnknown::Release, обязаны возвращать значение именно этого типа. Структура сообщения о состоянии, возвращаемого черезHRESULT, тоже определена на уровне системной спецификации и мы рассмотрим эту спецификацию позднее, в соответствующей теме. Когда именно проблема возвращения кода ошибки из сервера клиенту для нас станет актуальной. А пока, в следующей статье, мы возвращаемся к основному интерфейсуCOM- к интерфейсуIUnknown.

Итак - интерфейс IUnknownявляется основным интерфейсом на котором зиждетсяCOM. Его обязан реализовывать любойCOM-объект, он обязан присутствовать в составе любого интерфейса, экспонируемого объектом. НоIUnknown- совершенно особый интерфейс, если иметь в виду не его видимость со стороны клиента, а его реализацию внутри объекта сервера.

Знакомство лучше начать с точной спецификации. Точная спецификация интерфейса IUnknownсодержится в заголовочном файлеC++с именем<unknwn.h>:

MIDL_INTERFACE("00000000-0000-0000-C000-000000000046") IUnknown

{

public:

virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid,void **ppvObject) = 0;

virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;

virtual ULONG STDMETHODCALLTYPE Release(void = 0;

};

Его определение немного сокращено за счёт конструкций языка C++для нас пока не существенных, но не выплеснул вместе с водой ребенка - файл этот не написан руками, а сгенерирован компиляторомMIDLc языкаIDL- специального языка, на котором описываются интерфейсы. Поэтому компилятор туда вставляет всё что можно и для всех возможных случаев использования этого файла. Если хотите заглянуть в оригинал сами, то вот ссылка на его "стандартное" местоположение (если соглашаться на те пути и каталоги, которые при инсталляцииVisual Studioпредлагает по умолчанию инсталлятор) - "C:\Program Files\Microsoft Visual Studio\VC98\Include\UNKNWN.H".

Интерфейс IUnknownимеет "свойGUID":

{00000000-0000-0000-C000-000000000046}

который в программах на C++адресуется ссылкойIID_IUnknown. Состоит интерфейс из трёх методов:QueryInterface,AddRef,Release- именно в таком порядке адреса их точек входа расположены вvtbl. Учитывая, что всякий интерфейс обязан начинаться сIUnknown, то, выходит, что первые три входа любойvtblзанимают эти методы и именно в таком порядке.

Зачем нужны методы IUnknown- подробно разбиралось ранее. Поэтому сейчас мы рассматриваем только как эти методы нужно реализовать. Начнем по порядку следования...

Метод QueryInterface. Предназначен для преобразования типа указателя на интерфейс - на вход принимаетсяIIDинтерфейса и адрес переменной, куда нужно поместить указатель на интерфейс, именуемый даннымIID. Если запрашиваемый интерфейс действительно реализуется данным объектом, то метод помещает указатель на него в предоставленную переменную. Если нет - метод возвращает код возвратаE_NOINTERFACEи, по хорошему, должен вернутьNULLв предоставленной переменной (чего некоторые, особенно - старые, серверы делают не всегда, к сожалению). Обратите внимание - метод принимает на входIIDлюбогоинтерфейса(произвольныйGUID), а возвращает указатель на него, если интерфейс реализуетсяобъектом. Т.е. методQueryInterfaceлюбого интерфейса данного объекта должензнать все интерфейсы, реализуемые данным объектом. Это требование не так трудно и выдержать, оно не означает, что для каждого интерфейса этого объекта нужно писать свою реализацию этого метода. Оно означает, что реализации всех интерфейсов могут воспользоваться одной реализацией методаQueryInterface- единой для всего данногоCOM-объекта.

Метод AddRef. Предназначен для продвижения вперед на единицу счетчика ссылок. Должен возвращать новое значение этого счётчика ссылок - от 1 до n, но пользоваться этим значением можно только для отладки.Microsoftсообщает, что иногда это значение может быть "нестабильно". Что означает "нестабильно"- я не знаю. Видимо имеется в виду то, что в многопоточной среде значение счётчика совершенно точно известно только самому этому счётчику, который защищён от одновременного доступа нескольких потоков сразу. А вот актуальность текущего значения счётчика, выданного в любой поток может быть сразу же уничтожена параллельным потоком. Фактически смысл этого счётчика на клиентской стороне действительно имеется только при отладке, ничего другого на этом значении построить не удаётся.

Метод Release. Предназначен для продвижения счетчика ссылок назад на единицу. Возвращает новое (после декремента) значение счётчика ссылок, которое тоже может использоваться только для отладки. При этом, если счётчик становится равным нулю - реализация метода должна освободить ресурсы, примитивно говоря - уничтожить объект.

И тут нужно обратить внимание - что AddRefиReleaseлюбого интерфейса обслуживают один и тот же счётчик ссылок - счётчик ссылок всего объекта. Т.е. реализация и этих методов может быть одной на весь объект.

А вот дальше нужно немного призадуматься. Что на самом деле означает фраза: "метод должен освободить ресурсы"? Для простых по конструкции и типовых объектов это, понятно, равносильно удалению самого объекта полученного сервером по операции new. Но для сложных объектов это может быть и не так - некоторые интерфейсы (они называютсяtear-off, "отрывными") сложный объект может реализовывать "по отдельности", т.е. не получать для них ресурсы при создании самого объекта, а получать ресурсы только тогда, когда кто-то попросит ссылку именно на этот интерфейс. В таком случае, методыAddRefиReleaseбудут вынуждены обслуживать уже два и более счётчиков ссылок - общий для всего объекта и частные именно для таких интерфейсов в составе объекта. И так же дробно, как объект получал ресурсы,Releaseбудет их и освобождать. При этом под термином "ресурсы" я понимаю не только динамическую память. Например, реализация интерфейса может открывать файл, устанавливать сетевое соединение и т.д. Эти ресурсы тоже должны освобождаться при потере всех ссылок на интерфейс.

Для некоторых объектов (а мало ли, вам понадобится и такие объекты реализовывать?), которые являются статическими объектами уровня модуля, "самоликвидатор" не нужен. Но счётчик ссылок для такого объекта вести придётся и клиент и для такого указателя будет вызывать AddRef/Release, хотя они в данном случае ничего и не делают. Принцип должен выдерживаться - клиент не знает, как реализован объект сервера. Клиент знает только интерфейс, посредством которого он взаимодействует с объектом. И должен соблюдать протокол.

В сущности, теперь мы можем написать и второй пример - пример, в котором конструктивные недостатки объектов первого примера будут исправлены программным механизмом, который мы только что изучили. Это сделает наши объекты действительно "настоящими объектами COM", но, к сожалению пока ещё не позволит избавиться от эмулятораCoCreateInstance- мало иметьCOM-объекты, надо, чтобы они обитали и в "настоящемCOM-сервере". А от "настоящегоCOM-сервера" мы пока знаем только одну из четырех реализуемых и экспонируемых им системе функций -DllGetClassObject.

Описанная выше структура интерфейса естественно решает первую проблему экспорта объектов между модулями - каждый интерфейс однозначно определяется своим GUID, имеет вполне определенный список методов с вполне определенными параметрами и условиями вызова. То есть имеется абсолютно однозначный протокол вызова методов, и это гарантирует корректность передаваемых данных и сохранность стека. При помощи интерфейсов также естественно решается проблема унифицированной передачи данных. В списках формальных параметров методов интерфейса, вообще говоря, используются не все типы данных, которые определены в данном языке программирования. Точнее сказать, могут использоваться любые типы данных, но если ссылка на данный интерфейс может быть передана в другой модуль, то список формальных параметров методов интерфейса обязан содержать только определенные типы данных - так называемые OLE Automation Datatypes. В таблице приведены эти типы данных.

Тип

Описание

__int64

байт, целое со знаком, диапазон -263+1...263-1, сопроцессорный тип

BSTR

Строка, в который для хранения каждого символа используют 2 байта. Минимальный размер - 4 байта

byte

1 байт, целое без знака, диапазон 0..255

CURRENCY

8 байт, с фиксированной запятой и четырьма знаками после запятой, диапазон -922337203685477.5808..922337203685477.5807, сопроцессорный тип

DATE

8 байт, с плавающей запятой, целая часть - число дней с 30 декабря 1899 года, дробная часть - доля от 24 часов

DECIMAL

Структура, содержит число с плавающей запятой и точность его представления (сколько имеется значимых десятичных знаков). Расшифровка - ActiveX.h

DISPPARAMS

Структура, содержит параметры вызова методов через метод Invoke интерфейса Idispatch. Расшифровка структуры приведена в модуле ActiveX.h

double

8 байт, с плавающей запятой, диапазон 5.0х10-324...1.7х10208. 15-16 знаков

EXCEPINFO

Структура, содержащая информацию об исключении. Расшифровка приведена в модуле ActiveX.h

float

4 байта, с плавающей запятой, диапазон 1.5x10-45...3.4x10387-8 знаков

GUID

Глобальный идентификатор (класса, интерфейса). Структура размером 16 байт

HRESULT

4 байта, целое число без знака, диапазон 0..4294967295

int

4 байта, целое число со знаком, диапазон -2147483648..2147483647

VARIANT

Содержит любые данные, тип может меняться динамически. Минимальный размер - 16 байт.

SAFEARRAY

Указатель на массив целых чисел, 4 байта

short

2 байта, целое со знаком, -32768.. 32767

VARIANT_BOOL

2 байта, логическая переменная (true = -l, false = 0)

Помимо перечисленных в таблице типов список формальных параметров может еще содержать ссылки на интерфейсы, определенные в модуле ActiveX, а также переменные, тип которых начинается с OLE (OLE_COLOR, OLE_XPOS и др.). Нельзя при передаче данных через интерфейсы использовать параметры типа bool - можно только VARIANT_BOOL. Нельзя использовать параметры типа char * - можно только BSTR. При работе с СОМ-объектами необходимо иметь информацию не только о том, где что-то находится, но и о том, что находится в данной области памяти.

Эти ограничения введены потому, что СОМ-объект можно реализовывать на разных языках программирования. При этом такой язык программирования обязан поддерживать вышеперечисленные типы данных. Помимо этого, любой язык может содержать свои собственные типы данных -например, bool, char в C++. Поэтому программист может с уверенностью применять вышеперечисленные типы данных, зная, что при их передаче между модулями не произойдет искажения. Язык программирования, который не поддерживает эти типы данных, не поддерживает и СОМ-технологию и не сможет получить ссылку на интерфейс.IUnknown.