Динамический вызов в сравнении со статическим
До сих пор говорилось о том, что СОМ основан на клиентских программах, имеющих на этапе разработки предварительную информацию об определении интерфейса. Это достигается либо через заголовочные файлы C++ (для клиентов C++), либо через библиотеки типов (для клиентов Java и Visual Basic). В общем случае это не представляет трудностей, так как программы, написанные на этих языках, перед употреблением обычно проходят фазу какой-либо компиляции. Некоторые языки не имеют такой фазы компиляции на этапе разработки и вместо этого распространяются в исходном коде с тем, чтобы интерпретироваться во время выполнения. Вероятно, наиболее распространенными среди таких языков являются языки сценариев на базе HTML (например, Visual Basic Script, JavaScript), которые выполняются в контексте Web-броузера или Web-сервера. В обоих этих случаях текст сценариев вкладывается в его исходном виде в файл HTML, а окружающая исполняемая программа выполняет текст сценариев "на лету", по мере анализа HTML. С целью обеспечить разнообразную среду программирования эти окружения позволяют сценариям вызывать методы СОМ-объектов, которые могут создаваться в самом тексте сценария или где-нибудь еще в HTML-потоке (например, какой-либо управляющий элемент, который является также частью Web- страницы). В таких средах в настоящее время невозможно использовать библиотеки типов или другие априорные средства для снабжения машины времени выполнения (runtime engine) описанием используемых интерфейсов. Это означает, что объекты сами должны помогать интерпретатору переводить исходный текст сценариев в содержательные вызовы методов.
Для того чтобы объекты быть использованы из интерпретирующих сред типа Visual Basic Script и JavaScript, СОМ определяет интерфейс, выражающий функциональность интерпретатора. Этот интерфейс называется IDispatch и определяется следующим образом:
[object, uuid(00020400-0000-0000-C000-000000000046)] interface IDispatch : IUnknown { // structure to model a list of named parameters // структура для моделирования списка именованных параметров typedef struct tagDISPPARAMS { [size_is(cArgs)] VARIANTARG * rgvarg; [size_is(cNamedArgs)] DISPID * rgdispidNamedArgs; UINT cArgs; UINT cNamedArgs; } DISPPARAMS;
// can the object describe this interface? // может ли объект описать этот интерфейс? HRESULT GetTypeInfoCount([out] UINT * pctinfo);
// return a locale-specific description of this interface // возвращаем специфическое для данной локализации описание этого интерфейса HRESULT GetTypeInfo( [in] UINT itInfo, // reserved, m.b.z. // зарезервировано, должно равняться нулю [in] LCID lcid, // locale ID // код локализации [out] ITypeInfo ** ppTInfo); // put it here! // помещаем это здесь!
// resolve member/parameter names to DISPIDs // преобразовываем имена членов/параметров в DISPID HRESULT GetIDsOfNames( [in] REFIID riid, // reserved, must be IID_NULL // зарезервировано, должно равняться IID_NULL [in, size_is(cNames)] LPOLESTR * rgszNames, // method+params // метод + параметры [in] UINT cNames, // count of names // количество имен [in] LCID lcid, // locale ID // локальный ID [out, size_is(cNames)] DISPID * rgid // tokens of names // маркеры имен );
// access member via its DISPID // обращаемся к члену через его DISPID HRESULT Invoke( [in] DISPID id, // token of member // маркер члена [in] REFIID riid, // reserved, must be IID_NULL // зарезервировано, должно равняться IID_NULL [in] LCID lcid, // locale ID // локальный ID [in] WORD wFlags, // method, propput, or propget? // метод propput или propget? [in,out] DISPPARAMS * pDispParams, // logical parameters // логические параметры [out] VARIANT * pVarResult, // logical result // логический результат [out] EXCEPINFO * pExcepInfo, // IErrorInfo params // параметры IErrorInfo [out] UINT * puArgErr // used for type errors // использовано для ошибок типа );
Когда машина сценариев впервые пытается обратиться к объекту, она использует QueryInterface для запроса интерфейса IDispatch этого объекта. Если объект отклоняет запрос QueryInterface, то машина сценариев этот объект использовать не может. Если же объект успешно возвращает свой интерфейс IDispatch машине сценариев, то машина будет использовать метод GetIDsOfNames этого объекта для перевода имен методов и свойств в маркеры.
Эти маркеры формально называются DISPID и являются эффективно синтаксически разобранными (parsed) целыми числами, которые единственным образом идентифицируют свойство или метод. После преобразования имени метода или свойства в маркер машина сценариев потребует запуска именованного метода/свойства через метод IDispatch::Invoke данного объекта. Отметим, что поскольку IDispatch::Invoke принимает значения параметров операции в виде массива именованных типов VARIANT с использованием структуры DISPPARAMS, то диапазон поддерживаемых типов параметров ограничен возможностью записи в один VARIANT.
Интерфейсы на базе IDispatch (часто называемые dispinterface — диспинтерфейс, или диспетчерский интерфейс) логически эквивалентны обычному интерфейсу СОМ. Основное различие состоит в методах вызова на практике логических операций интерфейса. В случае обычного интерфейса СОМ вызовы методов основываются на статике, на априорном знании сигнатуры методов интерфейса. В случае диспинтерфейса вызовы методов основаны на текстовых представлениях ожидаемой сигнатуры вызовов методов. Если вызывающая программа правильно угадывает сигнатуру метода, то вызов может быть правильно диспетчеризован. Если же вызывающая программа неправильно угадывает сигнатуру метода, то диспетчеризовать вызов, возможно, не удастся. Если для параметров метода используются неверные типы данных, то преобразование их в нужные является делом объекта (если это вообще возможно).
Простейший способ выразить диспинтерфейс на IDL — это использовать ключевое слово dispinterface:
[uuid(75DA6450-DD0F-11d0-8C58-0880C73925BA)] dispinterface DPrimeManager { properties: [id(1), readonly] long MinPrimeOnMachine; [id(2)] long MinPrime; methods: [id(3)] long GetNextPrime([in] long n); }
Этот синтаксис вполне читабелен; однако он предполагает, что вызывающая программа будет всегда обращаться к свойствам и методам объекта через IDispatch. История показала, что по мере развития программных сред этапа разработки и выполнения они часто становятся способными использовать обычные интерфейсы СОМ.
Для обеспечения того, чтобы обращение к диспинтерфейсу было успешным и в будущих средах подготовки сценариев, как правило, лучше моделировать интерфейс как двойственный, или дуальный (dual interface).
Двойственные интерфейсы являются обычными интерфейсами СОМ, наследующими от IDispatch. Поскольку IDispatch является базовым интерфейсом, то он абсолютно совместим с полностью интерпретируемыми клиентами сценариев. В то же время этот интерфейс совместим вверх со средами, которые могут непосредственно связываться со статически определенным интерфейсом СОМ. Ниже приведено IDL-определение для двойственного варианта интерфейса DPrimeManager:
[object, dual, uuid(75DA6450-DD0F-11d0-8C58-0080C73925BA)] interface DIPrimeManager : IDispatch { [id(1), propget] HRESULT MinPrimeOnMachine( [out, retval] long *pval); [id(2), propput] HRESULT MinPrime([in] longval); [id(2), propget] HRESULT MinPrime([out, retval] long *pval); [id(3)] long GetNextPrime([in] long n); }
Заметим, что этот интерфейс наследует IDispatch, а не IUnknown. Также отметим, что данный интерфейс имеет атрибут [dual]. Этот атрибут заставляет сгенерированную библиотеку типов включить в себя диспетчерский вариант интерфейса, который совместим со средами, не поддерживающими двойственные интерфейсы. Атрибут [dual] относится к категории атрибутов [oleautomation] и также заставляет сгенерированную библиотеку типов добавлять ключи реестра для универсального маршалера во время выполнения RegisterTypeLib.
Если интерфейс определен как двойственный, то реализация методов IDispatch является тривиальной. Дело в том, что синтаксический анализатор библиотеки типов реализует два из четырех методов IDispatch. Если двойственный интерфейс был определен заранее, объекту необходимо на этапе инициализации просто загрузить библиотеку типов:
class PrimeManager : DIPrimeManager { LONG m_cRef; // СОМ reference count // счетчик ссылок СОМ ITypeInfo *m_pTypeInfo; // ptr. to type desc. // указатель на описание типов // IUnknown methods... // методы IUnknown...
// IDispatch methods... // методы IDispatch...
// IPrimeManager methods... // методы IPrimeManager... PrimeManager(void) : m_cRef(0) { ITypeLib *ptl = 0; HRESULT hr = LoadRegTypeLib(LIBID_PrimeLib, 1, 0, 0, &ptl); assert(SUCCEEDED(hr)); hr = ptl->GetTypeInfoOfGuid(IID_DIPrimeManager, &m_pTypeInfo); ptl->Release(); } virtual PrimeManager(void) { m_pTypeInfo->Release(); } };
Имея приведенное выше определение класса, метод GetTypeInfo просто возвращает описание данного интерфейса:
STDMETHODIMP PrimeManager::GetTypeInfo (UINT it, LCID lcid, ITypeInfo **ppti) { assert(it == 0 && ppti != 0); (*ppti = m_pTypeInfo)->AddRef(); return S_OK; }
Если бы объект поддерживал несколько локализованных библиотек типов, то реализации следовало бы использовать параметр LCID, чтобы решить, какое описание типа нужно возвратить. Соответствующая реализация GetTypeInfoCount еще проще:
STDMETHODIMP PrimeManager::GetTypeInfoCount(UINT *pit) { assert(pit != 0); *pit = 1; // only 0 or 1 are allowed // допускаются только 0 или 1 return S_OK; }
Единственными допустимыми значениями счетчика являются нуль (это означает, что данный объект не содержит описаний своего интерфейса) и единица (это означает, что данный объект содержит описания своего интерфейса). Даже если объект поддерживает несколько локализованных описаний типа, результирующий счетчик остается равным единице.
Методы GetTypeInfo и GetTypeInfoCount фактически являются вспомогательными. Истинным ядром интерфейса IDispatch являются методы GetIDsOfNames и Invoke. Реализация GetIDsOfNames направляет вызов в машину синтаксического анализа библиотеки типов, встроенную в СОМ:
STDMETHODIMP PrimeManager::GetIDsOfNames(REFIID riid, OLECHAR **pNames, UINT cNames, LCID lcid, DISPID *pdispids) { assert(riid == IID_NULL); return m_pTypeInfo->GetIDsOfNames(pNames, cNames, pdispids); }
Поскольку библиотека типов содержит все имена методов и соответствующие им DISPID, реализация не представляет труда для синтаксического анализатора.
Метод Invoke реализован аналогичным образом:
STDMETHODIMP PrimeManager::Invoke( DISPID id, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pd, VARIANT *pVarResult, EXCEPINFO *pe, UINT *pu) { assert(riid == IID_NULL); void *pvThis = static_cast<DIPrimeManager*>(this); return m_pTypeInfo->Invoke(pvThis, id, wFlags, pd, pVarResult, pe, pu); }
Первым параметром ITypeInfo::Invoke является указатель на интерфейс. Тип этого интерфейса должен быть таким же, как интерфейс, который описан в информации о типах. Когда передаваемые аргументы корректно синтаксически преобразованы в стек вызова (call stack), синтаксический анализатор будет вызывать текущие методы через этот интерфейсный указатель. Рис. 7.6 иллюстрирует последовательность вызовов для сред подготовки сценариев, которые осуществляют вызовы через двойственные интерфейсы.