Ivan Andreyev
В одной из статей RSDN Magazine описывался способ перехвата методов интерфейса IUnknown. Суть этого подхода заключалась в замене указателей на функции QueryInterace, AddRef, Release в VTBL интерфейса и выполнении дополнительной обработки внутри перехватчиков.
В этой статье мы продолжим обсуждение темы перехвата вызовов методов COM-интерфейсов и познакомимся с API-функциями CoGetInterceptor, CoGetInterceptorFromTypeInfo, позволяющими забыть обо всех технических трудностях и проблемах, связанных с передачей вызова от клиента перехватчику, и от перехватчика – исходному компоненту.
Технология “перехвата” вызовов API функций, обработчиков оконных сообщений, методов COM-компонентов имеет много общего с шаблоном проектирования Proxy (Заместитель). Суть этой технологии заключается в том, что вызов клиента перенаправляется (с помощью различных технических ухищрений – замена VTBL, Proxy-объект и т.п.) сначала коду заместителя, который выполняет пред- и постобработку, а затем уже – исходному объекту. Благодаря этому можно добавлять новую функциональность, никак не изменяя ни код клиента, ни код сервера.
Очень широкое распространение технология “перехвата” получила в COM – фундаментальные принципы прозрачности местонахождения компонента (location transparency) и прозрачности типа синхронизации (concurrency transparency) реализуются именно благодаря Proxy-компонентам из инфраструктуры COM, которые имитируют для клиента исходный компонент. С появлением COM+ набор сервисов, которые реализуют перехватчики, расширился еще больше – добавились поддержка транзакций, блокировок для синхронизации доступа к компонентам, поддержка just-in-time активации, ролевая безопасность. За счет того, что эти сервисы реализуются инфраструктурой COM+ прозрачно для клиента и серверных компонентов (хотя серверные COM+-компоненты могут взаимодействовать с инфраструктурой, например, чтобы отменить или подтвердить транзакцию), клиентский код ничего не знает о том, что случится с его вызовом на сервере – будет ли он обслуживаться COM+ или обычным COM-компонентом. Аналогично, один и тот же компонент может использоваться в составе COM+-приложения.
Помимо предоставления различных сервисов перехват вызовов методов COM-компонентов позволяет решить и другие задачи, например:
протоколирование вызовов COM-компонентов;
отладка – проверка значений аргументов, контроль подсчета ссылок;
специальный маршалинг;
использование альтернативных по отношению к RPC видов транспорта для передачи COM-вызовов (MSMQ, SOAP и т.п.);
асинхронные вызовы (заместитель сохраняет информацию о вызове и производит фактический вызов исходного компонента позднее).
Рисунок 1 иллюстрирует принцип перехвата вызовов COM-компонентов, Proxy и Stub – служебные компоненты, один из которых принимает вызовы от клиента, имитируя исходный компонент, а другой – передает эти вызовы компоненту, имитируя логику работы клиента. Именно по такой схеме работает маршалинг в COM, и по такой же схеме COM+ обеспечивает дополнительные сервисы (транзакции, блокировки и т.п.) для сконфигурированных компонентов.
Рисунок 1. Принцип перехвата COM-вызова.
Как это часто случается, несмотря на простое описание технологии перехвата, ее техническая реализация очень непростое дело, в особенности, когда речь идет об универсальном перехвате.
В первой части статьи мы познакомимся с различными техническими способами перехвата вызовов.
Техника перехвата вызовов
Один из самых простых и эффективных способов перехвата вызовов методов COM-компонента заключается в создании Proxy-компонента, реализующего нужный интерфейс и перенаправляющего вызовы исходному COM-компоненту.
ПРИМЕЧАНИЕ Для COM-компонентов такой подход используется не только при перехвате вызовов, но еще и как средство повторного использования кода (code reuse), и носит название containment (включение). |
В качестве примера рассмотрим стандартную реализацию IStream на основе памяти – CreateStreamOnHGlobal. Предположим, что нам необходимо ассоциировать имя с каждым потоком IStream, созданным с помощью CreateStreamOnHGlobal. Имя потока можно получить с помощью вызова IStream::Stat, но реализация IStream на основе памяти HGlobal всегда возвращает пустое имя. Мы можем поступить следующим образом:
создать компонент-обертку, поддерживающий IStream;
перенаправлять все вызовы IStream в стандартную реализацию CreateStreamOnHGlobal;
в методе IStream::Stat указывать имя потока.
class StreamOnMemory : public CComObjectRoot, public IStream { public: BEGIN_COM_MAP(StreamOnMemory) COM_INTERFACE_ENTRY(IStream) END_COM_MAP() public: // реализация IStream STDMETHOD(Seek)(_LARGE_INTEGER dlibMove, ULONG dwOrigin, _ULARGE_INTEGER * plibNewPosition) { return m_spStm->Seek(dlibMove, dwOrigin, plibNewPosition); } // остальные методы реализованы аналогично Seek ... STDMETHOD(Stat)(tagSTATSTG * pstatstg, ULONG grfStatFlag) { HRESULT hr = m_spStm->Stat(pstatstg, grfStatFlag); if( SUCCEEDED(hr) && (grfStatFlag & STATFLAG_NONAME) == 0) { pstatstg->pwcsName = AtlAllocTaskWideString(m_name); } return hr; } private: friend HRESULT CreateStreamOnHGlobal2(HGLOBAL ,BOOL ,LPOLESTR, LPSTREAM*); HRESULT init(HGLOBAL hGlobal,BOOL fDeleteOnRelease, LPOLESTR name) { m_spStm.Release(); HRESULT hr = CreateStreamOnHGlobal(hGlobal, fDeleteOnRelease, &m_spStm); if(SUCCEEDED(hr)) { m_name = name; } return hr; } private: CComPtr<IStream> m_spStm; CComBSTR m_name; }; HRESULT CreateStreamOnHGlobal2(HGLOBAL hGlobal,BOOL fDeleteOnRelease, LPOLESTR name, LPSTREAM* ppstm) { CComObject<StreamOnMemory>* p = NULL; HRESULT hr = CComObject<StreamOnMemory>::CreateInstance(&p); if(SUCCEEDED(hr)) { CComPtr<IStream> spStm = p; hr = p->init(hGlobal, fDeleteOnRelease, name); if(SUCCEEDED(hr)) { *ppstm = spStm.Detach(); } } return hr; } |
При таком подходе нет необходимости вносить какие-либо изменения в клиентский код, работающий с указателями на интерфейс IStream.
ПРИМЕЧАНИЕ За исключением кода, создающего поток с помощью вызова CreateStreamOnHGlobal. |
Такой “частный” подход неприменим, когда количество перехватываемых интерфейсов велико, или если информация об интерфейсах и сигнатурах их методов недоступна во время компиляции и станет известна только во время выполнения программы. Например, typelib-маршалинг в COM предоставляет клиенту Proxy-компонент, поддерживающий интерфейс серверного компонента, но обеспечить реализацию этого интерфейса инфраструктура COM может только во время выполнения – на этапе компиляции неизвестно, какие интерфейсы будут использоваться для typelib-маршалинга.
Разумеется, лучше было бы реализовать универсальный перехват вызовов COM-методов. Но при этом мы столкнемся с несколькими проблемами:
заранее неизвестно количество методов в произвольном интерфейсе, т.е. структура vtbl;
неизвестны сигнатуры индивидуальных методов, входящих в интерфейс, т.е. количество и типы параметров.
Решить указанные проблемы, используя только средства языков высокого уровня, не удастся. Мы могли бы попытаться обойти отсутствие информации о сигнатурах методов путем объявления функции с переменным количеством параметров:
void f(int a, ...); |
Но такие функции используют соглашение о вызове cdecl, а методы COM-интерфейсов – stdcall.
ПРИМЕЧАНИЕ Эти соглашения о вызовах в первую очередь различаются тем, кто ответственен за удаление параметров из стека после вызова. stdcall-функции очищают стек сами, а для cdecl-функций стек очищает вызывающая функция. |
Подход ATL
В библиотеке ATL перехват вызовов используется для отладки COM-серверов. Если до включения заголовочного файла <atlbase.h> объявить символ препроцессора _ATL_DEBUG_INTERFACES (или _ATL_DEBUG_REFCOUNT), то в окне “Output” отладчика VS во время выполнения приложения будут появляться сообщения, описывающие вызовы AddRef и Release для COM-объектов, созданных с помощью ATL, текущий счетчик ссылок или IID запрашиваемого интерфейса. Ниже приведен пример таких сообщений:
QIThunk-1 AddRef:Object=0x00da4c50 Refcount = 1 CComClassFactory - IUnknown QIThunk-2 AddRef:Object=0x00da4c50 Refcount = 1 CComClassFactory - IClassFactory QIThunk-3 AddRef:Object=0x00da4e20 Refcount = 1 CFoo - IFoo QIThunk-3 AddRef:Object=0x00da4e20 Refcount = 2 CFoo - IFoo QIThunk-3 Release:Object=0x00da4e20 Refcount = 1 CFoo - IFoo QIThunk-2 Release:Object=0x00da4c50 Refcount = 0 CComClassFactory - IClassFactory QIThunk-4 AddRef:Object=0x00da4e20 Refcount = 1 CFoo - IFoo QIThunk-3 Release:Object=0x00da4e20 Refcount = 0 CFoo - IFoo QIThunk-1 Release:Object=0x00da4c50 Refcount = 0 CComClassFactory - IUnknown ATL: QIThunk-4 LEAK:Object = 0x00da4e20 Refcount = 1 MaxRefCount = 1 CFoo - IFoo |
Во время выгрузки ATL COM-сервера в окне “Output” появятся сведения об указателях на интерфейс, для которых счетчик ссылок не достиг значения 0, т.е. об утечках COM объектов.
“Магия” ATL работает благодаря перехвату вызовов методов COM-интерфейсов, в частности, AddRef, Release и QueryInterface.
Когда клиент запрашивает интерфейс у объекта с помощью QueryInterface, класс CComObject делегирует вызов базовому классу CComObjectRootBase::InternalQueryInterface, который при определенном макросе _ATL_DEBUG_INTERFACES обращается к экземпляру класса CAtlDebugInterfacesModule и вызывает у него метод AddThunk.
HRESULT AddThunk(IUnknown** pp, LPCTSTR lpsz, REFIID iid) throw() |
Результатом вызова CComObjectRootBase::InternalQueryInterface становится специальный объект-посредник QIThunk, который перехватывает AddRef, Release и QueryInterface, а все остальные вызовы делегирует исходному компоненту.
Класс CAtlDebugInterfacesModule хранит список всех активных объектов-заместителей QIThunk и в своем деструкторе выполняет отладочную печать всех объектов, чей счетчик ссылок не достиг нулевого значения.
Когда клиент отпускает последнюю ссылку на компонент, QIThunk удаляет себя из списка активных посредников в CAtlDebugInterfacesModule.
Таким образом, клиенты имеют дело не с прямым указателем на интерфейс COM-объекта, а с указателем на QIThunk, который и печатает отладочные сообщения о текущем значении счетчика ссылок и IID запрашиваемого интерфейса.
Указатель на QIThunk ведет себя в точности так же, как и указатель на обычный интерфейс. Это достигается за счет того, что vtbl класса QIThunk содержит адреса методов-перехватчиков, вызывающих исходные методы. Поскольку все интерфейсы унаследованы от IUnknown, первые три адреса vtbl содержат QueryInterface, AddRef и Release. Их реализация в QIThunk тривиальна – сигнатура методов в точности известна на этапе компиляции.
Но как быть с остальными методами интерфейса, количество и сигнатуры которых неизвестны? Для решения этой проблемы QIThunk использует универсальную функцию-перехватчик, адресом которой заполняется vtbl. Виртуальные методы объявляются в QIThunk так:
STDMETHOD(f3)(); STDMETHOD(f4)(); ... STDMETHOD(f1023)(); |
Vtbl QIThunk содержит 1024 адреса. Интерфейсы, объявляющие большее количество методов, встречаются нечасто.
Реализация этих методов задается с помощью макроса:
ATL_IMPL_THUNK(3) ATL_IMPL_THUNK(4) ... ATL_IMPL_THUNK(1023) |
Метод-перехватчик будет вызываться клиентом с заранее неизвестным количеством параметров, поэтому написать такую функцию на языке высокого уровня невозможно – не подходят ни стандартные пролог/эпилог, генерируемые компилятором C++, ни “нормальное” завершение функции вызовом инструкции ret, так как stdcall-функции должны очищать стек сами, передавая размер стека параметров в ret.
Рисунок 2. Вызов COM метода.
На рисунке 2 приведен пример дизассемблированного кода вызова метода COM-интерфейса (ссылка на который находится в pUnk) с передачей двух параметров, arg1 и arg2.
Отключить генерирование стандартного пролога и эпилога можно с помощью директивы _declspec(naked) перед определением функции. Проблема, связанная с нормальным завершением путем вызова ret, решается за счет использования другой инструкции процессора – jmp. Вместо того, чтобы вызывать исходный метод с помощью инструкции call (мы не можем подготовить стек параметров для call, так как не знаем их количество) и затем выполнить “ret n” (нам неизвестно n – количество параметров * 4) – перехватчик определяет адрес исходного метода, заменяет в стеке указатель на объект (который внутри вызова будет рассматриваться как this), к методу которого производится вызов, а затем просто “перепрыгивает” по нужному адресу с помощью jmp. После вызова jmp в стеке не остается ничего, что напоминало бы о перехватчике – настоящая функция получает нетронутый стек параметров и после ее завершения мы попадем в клиентский код, минуя перехватчик. Ниже приведен код перехватчика, реализованный с помощью ATL:
mov eax, [esp+4] // первый параметр в стеке - this cmp dword ptr [eax+8], 0 // проверяем счетчик ссылок QIThunk::m_dwRef jg goodref call atlBadThunkCall goodref: mov eax, [esp+4] // первый параметр в стеке - this mov eax, dword ptr [eax+4] // получаем переменную-член QIThunk::m_pUnk mov [esp+4], eax // заменяем this-перехватчика в стеке на m_pUnk mov eax, dword ptr [eax] // получаем vptr (указатель на vtbl) // n – порядковый номер метода в vtbl mov eax, dword ptr [eax+4*n] // получаем адрес нужного виртуального метода jmp eax // переходим в нужный метод (обратно не вернемся) |
Необходимо отметить, что подобная техника позволяет выполнить предварительную обработку в перехватчике (в случае ATL – проверка счетчика ссылок перед вызовом), но не пост-обработку. После инструкции “jmp eax” мы больше не вернемся в код перехватчика (в стеке лежит адрес возврата в клиентский код, и после ret мы попадем именно туда).
Например, мы могли бы попытаться расширить код перехватчика так, чтобы писать отладочные сообщения, если вызов метода завершился с ошибкой. Чтобы решить эту задачу, нам пришлось бы заменить адрес возврата в стеке на код перехватчика (вместо адреса возврата в клиентский код), но тогда между пред- и пост-обработкой нужно было бы где-то хранить исходный адрес возврата. Стек не подходит в качестве такого хранилища, так как он будет использоваться вызываемым методом. Один из возможных вариантов – использование TLS или динамической памяти, кроме того, доступ к этому хранилищу должен синхронизироваться для многопоточных приложений.
ПРИМЕЧАНИЕ Количество слотов TLS ограничено, а вызовы к перехватчику в одном потоке могут быть вложенными. Поэтому для хранения адресов возврата пришлось бы использовать связанный список или аналогичную структуру данных, а также обеспечить быстрые выделения/освобождения памяти для элементов списка, чтобы уменьшить влияние перехватчика на скорость выполнения приложения. |
Подход, используемый ATL для перехвата вызовов COM-объектов, сводится к следующему:
Указатель на интерфейс заменяется на перехватчик в методе CComObjectRootBase::InternalQueryInterface при вызове QueryInterface. Поэтому перехватываются только вызовы COM-объектов, разработанных с помощью ATL.
vtbl перехватчика создается путем ручного объявления большого количества (1024) виртуальных методов, имеющих одинаковую реализацию.
ПРИМЕЧАНИЕ Такое решение нельзя назвать изящным – исходные тексты QIThunk получаются большими, но, с другой стороны это наиболее эффективный способ генерации vtbl. Альтернативный способ мог бы заключаться в заполнении vtbl во время выполнения приложения: |
HRESULT __stdcall thunk(void* pthis) { return S_OK; } typedef HRESULT (__stdcall * pthunk)(void* pthis); pthunk vtbl[1024]; for(int i = 0 ; i < sizeof(vtbl) / sizeof(vtbl[0]); ++i) vtbl[i] = &thunk; pthunk* vptr = vtbl; IUnknown* pUnk = reinterpret_cast<IUnknown*>(&vptr); pUnk->AddRef(); |
Метод-перехватчик, используя ассемблер, выполняет пред-обработку (проверку счетчика ссылок) и после этого передает управление исходному методу с помощью инструкции безусловного перехода jmp.
Замена указателей в vtbl
Для отладочных целей в приведенном выше примере нам было бы достаточно перехватывать только вызовы AddRef, Release и QueryInterface. Но для перехвата всех остальных методов интерфейса, сигнатура которых неизвестна на этапе компиляции, требуется более универсальный код.
Альтернативный способ перехвата вызовов методов интерфейса заключается в том, чтобы заменить в исходной vtbl интерфейса указатели на те методы, которые мы собираемся перехватывать. Эта технология была замечательно описана в статье “Перехват методов интерфейса IUnknown”.
Если нам известны сигнатуры перехватываемых методов (как в случае с методами IUnknown), нам не потребуется универсальный перехватчик, так как вызовы всех остальных методов будут осуществляться напрямую. Такой способ имеет следующие особенности по сравнению с рассмотренным выше:
Перехватываются все вызовы через любой указатель на интерфейс, так как мы меняем исходную vtbl интерфейса.
Vtbl обычно размещается в R/O секции памяти, поэтому код установки перехватчика должен менять настройки защиты этой секции.
Нет необходимости в генерации vtbl нужного размера (мы используем исходную vtbl), в некоторых случаях нет необходимости в универсальном коде перехвата методов с неизвестной сигнатурой.
В многопоточном приложении после установки перехватчика часть вызовов может выполниться напрямую, так как некоторые потоки могли уже успеть получить адрес метода из vtbl, но еще не выполнить вызов call.
Эта технология не подходит, когда в приложении нужно перехватывать вызовы через конкретный указатель на интерфейс, а не через любые указатели на этот интерфейс, и когда нужно контролировать время жизни перехватчика.
ПРИМЕЧАНИЕ Посмотрим, например, что произойдет при выгрузке модуля перехватчика. При этом он должен восстановить исходные адреса методов в vtbl, после чего выгрузиться. В многопоточном приложении один из потоков мог успеть получить адрес метода из vtbl (который все еще указывал на перехватчик), но не успеть сделать вызов по этому адресу. Если модуль перехватчика не будет предпринимать специальных мер по синхронизации, вызов по адресу выгруженного модуля закончится AV (access violation – ошибка доступа к памяти). |
Перехватчик с постобработкой
Вернемся снова к методу перехвата, используемому в ATL. Код перехватчика позволяет с легкостью выполнить подготовку к вызову – предобработку, но затем он выполняет безусловный переход jmp в исходную функцию. Попробуем дополнить его код так, чтобы позволить выполнить постобработку после вызова.
Первая задача, которую необходимо решить – генерация vtbl перехватчика. ATL использует с этой целью макросы ATL_IMPL_THUNK, явно объявляя 1024 метода в теле класса. Рассмотрим альтернативный подход, заключающийся в динамическом создании vtbl нужного вида в runtime.
Код перехватчика должен знать порядковый номер n метода интерфейса, чтобы выполнить его вызов. Мы можем разделить весь код универсального перехватчика на 2 части – первая будет зависеть от порядкового номера перехватываемого метода (n) и будет передавать управление второй, передавая n через стек, а вторая часть будет одинаковой для всех методов.
Код первой части тривиален – мы опускаем в стек n и затем выполняем переход на тело универсального перехватчика. Мы будем использовать технику ATL (которая используется для создания оконных процедур обработки сообщений, смысл этого будет описан ниже) – создадим структуру, содержащую нужные инструкции:
#pragma pack(push, 1) struct vthunk { BYTE m_push; DWORD m_n; BYTE m_jmp; DWORD m_offset; void init(DWORD_PTR proc, int n) { m_push = 0x68; m_n = n; m_jmp = 0xE9; m_offset = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(vthunk))); FlushInstructionCache(GetCurrentProcess(), this, sizeof(vthunk)); } }; #pragma pack(pop) |
Структуру vtbl можно имитировать с помощью массива указателей на vthunk:
struct ThunkVtbl { ThunkVtbl(DWORD_PTR pthunk) { for(int i = 0; i < thunk_n; ++ i) { code[i].init(pthunk, i); vtbl[i] = reinterpret_cast<DWORD_PTR>(&code[i]); } } static const int thunk_n = 1024; DWORD_PTR vtbl[thunk_n]; vthunk code[thunk_n]; }; |
В конструкторе ThunkVtbl мы инициализируем каждый из перехватчиков vthunk порядковым номером n и адресом универсального перехватчика pthunk. Теперь массив vtbl содержит 1024 указателя на структуры vthunk, каждая из которых содержит код для вызова перехватчика:
push n jmp pthunk |
Для постобработки нам потребуется хранить адрес возврата в клиентский код. С этой целью мы будем использовать TLS и контейнер std::deque (так как в одном потоке вызовы могут быть вложенными, нам нужен именно стек).
ПРИМЕЧАНИЕ Автор говорит о стеке, но по каким-то причинам использует двунаправленную очередь. С точки зрения функциональности это, в общем, безразлично, но несколько сбивает с толку. – прим. ред. |
Внутри перехватчика указатель на нужный std::deque берется из TLS, но так как поток создается не нами, мы не можем получить уведомление о его завершении. Значит, у нас нет точки в программе, где можно было бы безопасно уничтожить объект std::deque, ассоциированный с конкретным потоком. Во избежание потери ресурсов нужно дополнительно хранить список всех созданных объектов std::deque и уничтожать их перед завершением приложения.
Ниже приведена реализация специального класса-обертки, автоматизирующего выполнение всех этих действий. Список созданных std::deque в этом классе хранится в динамическом массиве (std::vector), добавление элементов в который происходит в конкурентном режиме и требует синхронизации. Для синхронизации доступа к нему используется критическая секция.
template<class T> struct TlsStorage { TlsStorage() { m_slot = TlsAlloc(); } ~TlsStorage() { std::vector<std::deque<T>* >::iterator it = m_stacks.begin(); for( ; it != m_stacks.end(); ++it) delete *it; TlsFree(m_slot); } void push(T t) { std::deque<T>* p = reinterpret_cast<std::deque<T>* >(TlsGetValue(m_slot)); if(!p) { p = new std::deque<T>; m_sec.Lock(); m_stacks.push_back(p); m_sec.Unlock(); TlsSetValue(m_slot, p); } p->push_back(t); } T pop() { std::deque<T>* p = reinterpret_cast<std::deque<T>* >(TlsGetValue(m_slot)); T t = p->back(); p->pop_back(); return t; } std::vector<std::deque<T>* > m_stacks; CComAutoCriticalSection m_sec; DWORD m_slot; }; |
Теперь у нас есть все необходимые составляющие. Класс ItfThunk собирает их вместе:
class ItfThunk { public: ItfThunk(void* p) : m_p(p) { vptr = &vtbl; } void __stdcall preprocess(int n) { std::cout << "method " << n << " preprocess" << std::endl; } HRESULT __stdcall postprocess(int n, HRESULT hr) { std::cout << "method " << n << " postrocess, result " << std::hex << hr << std::endl; return hr; } private: #pragma pack(push,1) struct CallInfo { void* p; int n; HRESULT hr; DWORD_PTR ret_addr; }; #pragma pack(pop) private: static void __cdecl store(int n, DWORD_PTR ret_addr, void* p) { CallInfo i = { p, n, 0, ret_addr }; storage.push(i); } static void __cdecl restore(HRESULT hr, CallInfo* pi) { *pi = storage.pop(); pi->hr = hr; } static void thunk(); private: ThunkVtbl* vptr; void* m_p; static TlsStorage<CallInfo> storage; static ThunkVtbl vtbl; }; __declspec(selectany) ThunkVtbl ItfThunk::vtbl(reinterpret_cast<DWORD_PTR>(ItfThunk::thunk)); __declspec(selectany) TlsStorage<ItfThunk::CallInfo> ItfThunk::storage; |
Переменная-член ThunkVtbl* vptr имитирует указатель vptr на таблицу виртуальных функций “обычного” C++-класса, структура CallInfo хранит информацию, необходимую для постобработки вызова. Нам осталось рассмотреть лишь реализацию статического метода void thunk(), выполняющего универсальный перехват. Перед вызовом этого перехватчика в стеке находятся параметры для исходного метода, указатель на this, адрес возврата в клиентский код и n – порядковый номер метода (который положил в стек vthunk):
Рисунок 3. Стек вызова
__declspec(naked) void ItfThunk::thunk() { __asm { push [esp] // кладем в стек n (параметр метода preprocess) push [esp+0Ch] // кладем в стек this для вызова preprocess call preprocess // вызываем ItfThunk::preprocess(n) call store // вызываем ItfThunk::store mov eax, [esp+8] // заменяем this в стеке на исходный mov eax, [eax+4] // из переменной ItfThunk::m_p mov [esp+8], eax lea eax, post_thunk // заменяем адрес возврата на post_thunk mov [esp+4], eax mov eax,[esp+8] // получаем vptr из исходного указателя mov eax, [eax] pop ecx // убираем из стека лишний параметр n mov eax, [eax+4*ecx] // полчаем адрес метода из vtbl jmp eax // переходим в исходный метод post_thunk: sub esp, 10h // выделяем в стеке место для CallInfo push esp push eax // результат вызова исходного метода в eax call restore // восстанавливаем инфрмацию из TLS add esp,8 call postprocess // постобработка ret } } |
Использовать перехватчик очень просто – клиент передает указатель на настоящий интерфейс конструктору ItfThunk и затем использует ItfThunk в качестве указателя:
CComPtr<IFoo> spFoo; HRESULT hr = spFoo.CoCreateInstance(__uuidof(Foo)); thunks::ItfThunk t(spFoo.p); spFoo.p = reinterpret_cast<IFoo*>(&t); spFoo->F(); |
Теперь мы можем выполнять постобработку вызова, но есть еще одна задача, которую этот перехватчик не решает – предположим, что в некоторых случаях в результате предобработки мы принимаем решение, что вызов исходного метода должен быть заблокирован. Типичный пример – ролевая безопасность. Вызов метода не проходит проверку ролевой безопасности и должен быть отклонен. Но мы не можем сделать этого, так как точное количество параметров метода неизвестно, и наш перехватчик делегирует очистку стека после вызова самому методу.
В общем случае для COM-интерфейсов мы не можем узнать сигнатуру их методов, но для интерфейсов, использующих typelib-маршалинг или итерфейсов, proxy/stub которых сгенерирован с ключом MIDL /oicf, эта информация доступна.
ПРИМЕЧАНИЕ Ключ /oicf компилятора midl позволяет генерировать интерпретируемый код для proxy/stub и, как результат, информация о сигнатурах метода доступна программно. Подробнее об этом можно прочитать в статье “Секреты маршалинга”. |
Получив информацию о количестве параметров метода, мы смогли решить несколько задач:
Заблокировать вызов метода.
Выполнять отложенный/асинхронный вызов.
И все это благодаря тому, что перехватчик сможет очищать стек самостоятельно, не делегируя эту работу исходному методу.
Необходимости самостоятельно разрабатывать перехватчик, опирающийся на информацию из библиотеки типов, нет – начиная с W2K документирован API, позволяющий использовать стандартные перехватчики из инфраструктуры COM/COM+ в своих целях.
CoGetInterceptor, CoGetInterceptorFromTypeInfo
В предыдущем разделе статьи мы рассмотрели несколько технологий перехвата вызовов методов интерфейсов (и могли почувствовать сложность создания универсального перехватчика). Но ни одна из этих технологий не позволила решить задачу перехвата полностью. В частности, не решена задача асинхронных/отложенных вызовов.
К нашей радости, теперь документированы API-функции, позволяющие использовать в приложениях перехватчики из инфраструктуры COM/COM+.
ПРИМЕЧАНИЕ Это те самые перехватчики, с помощью которых COM+ обеспечивает свои сервисы прозрачно для компонента и клиента – ролевую безопасность, синхронизацию и т.д. |
Получить перехватчик для произвольного интерфейса можно с помощью функции CoGetInterceptor:
HRESULT CoGetInterceptor( REFIID iidIntercepted, // IID перехватываемого интерфейса IUnknown * punkOuter, // IUnknown для агрегации REFIID iid, // IID интерфейса, запрашиваемого у перехватчика void ** ppv // указатель на интерфейс перехватчика ); |
Перехватчики COM+ используют информацию из библиотеки типов, чтобы определить сигнатуру метода и количество/типы параметров, а также выполнить маршалинг. Поэтому, если быть более точным, в качестве первого параметра (iidInterceptor) годятся не произвольные интерфейсы, а только те из них, которые совместимы с oleautomation и описаны в библиотеке типов.
Основной интерфейс перехватчика – ICallInterceptor, его мы и будем запрашивать в вызове CoGetInterceptor:
#include <callobj.h> CComModule _Module; int _tmain(int argc, _TCHAR* argv[]) { CoInitialize( 0); _Module.Init(0, 0 ); { CComPtr<IFoo> spFoo; HRESULT hr = spFoo.CoCreateInstance(__uuidof(Foo)); CComPtr<ICallInterceptor> spInt; hr = CoGetInterceptor(__uuidof(IFoo), 0, __uuidof(ICallInterceptor), reinterpret_cast<void**>(&spInt)); } _Module.Term(); CoUninitialize(); return 0; } |
Результатом выполнения приведенного выше приложения будет … Access Violation в недрах ntdll.dll. Этот неприятный сюрприз вызван тем, что перехватчики используют распределитель памяти RPC, который по умолчанию не проинициализирован. Исправить эту проблему можно либо с помощью вызова CoInitializeSecurity, либо вызовом любых функций маршалинга, которые проинициализируют RPC heap (есть еще вариант с прямым вызовом функции инициализации из rpcrt4.dll, но она не документирована).
ПРИМЕЧАНИЕ Проблема с инициализацией RPC-кучи была исправлена в Windows 2003 Server. |
Исправленный код клиента:
HRESULT hr = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL); CComPtr<IFoo> spFoo; hr = spFoo.CoCreateInstance(__uuidof(Foo)); CComPtr<ICallInterceptor> spInt; hr = CoGetInterceptor(__uuidof(IFoo), 0, __uuidof(ICallInterceptor), reinterpret_cast<void**>(&spInt)); |
С помощью указателя на интерфейс ICallInterceptor мы можем зарегистрировать свои собственные обработчики вызовов:
Методы ICallInterceptor | Описание |
HRESULT RegisterSink(ICallFrameEvents * psink); | Зарегистрировать обработчик |
HRESULT GetRegisteredSink(ICallFrameEvents ** ppsink); | Получить зарегистрированный обработчик |
ПРИМЕЧАНИЕ Другие методы ICallInterceptor описаны в MSDN |
Обработчик должен реализовать интерфейс ICallFrameEvents.
Методы ICallFraneEvent | Описание |
HRESULT OnCall(ICallFrame * pFrame); | Вызов метода перехватываемого интерфейса |
После регистрации обработчика мы будем получать событие OnCall каждый раз, когда клиент будет осуществлять вызов через перехватываемый интерфейс.
Дополним код клиента (см. выше) – теперь мы будем регистрировать свой обработчик вызовов:
class CallHandler : public CComObjectRoot, public ICallFrameEvents { public: BEGIN_COM_MAP(CallHandler) COM_INTERFACE_ENTRY(ICallFrameEvents) END_COM_MAP() STDMETHOD(OnCall)(ICallFrame* pFrame) { return S_OK; } }; ... CComPtr<ICallInterceptor> spInt; hr = CoGetInterceptor(__uuidof(IFoo), 0, __uuidof(ICallInterceptor), reinterpret_cast<void**>(&spInt)); CComObject<CallHandler>* pHandler = 0; CComObject<CallHandler>::CreateInstance(&pHandler); hr = spInt->RegisterSink(pHandler); CComPtr<IFoo> spFooInt; hr = spInt.QueryInterface(&spFooInt); hr = spFooInt->F(); |
ПРИМЕЧАНИЕ Если обработчик вернет HRESULT с ошибкой, ошибку получит и клиент, но ее код, к сожалению, не передается пользователю. Если клиент не зарегистрирует ни одного обработчика, то вызов метода также завершится с ошибкой. |
Мы запрашиваем указатель на перехватываемый интерфейс у перехватчика, а затем выполняем вызов метода IFoo::F, в результате мы попадем в код обработчика ICallFrameEvent::OnCall.
Задача обработчика – решить, что делать дальше с вызовом:
Отклонить его, вернув ошибку.
Сохранить стек параметров вызова, чтобы выполнить его асинхронно.
Выполнить вызов немедленно.
Прямые/синхронные вызовы
Информацию о вызове обработчик получает с помощью указателя на интерфейс ICallFrame, передаваемый ему в качестве параметра pFrame.
Интерфейс ICallFrame позволяет получить информацию о сигнатуре метода, размере стека параметров, значения отдельных параметров и результат вызова метода. Кроме того, с помощью ICallFrame можно изменить значения отдельных (или всех) параметров и дополнить стек параметров в случае, если клиент передал не все необходимые параметры (например, клиент сделал вызов не через указатель на перехватываемый интерфейс, а с помощью ICallInterceptor::CallIndirect, передавая частичный стек параметров).
ПРИМЕЧАНИЕ Подробнее описание методов интерфейса ICallFrame см. в MSDN |
Расширим код нашего обработчика CallHandler так, чтобы он выдавал отладочные сообщения о вызове и его результатах и выполнял немедленный вызов с помощью ICallFrame::Invoke:
template<class T> class CallHandler : public CComObjectRoot, public ICallFrameEvents { public: BEGIN_COM_MAP(CallHandler) COM_INTERFACE_ENTRY(ICallFrameEvents) END_COM_MAP() void init(CComPtr<T> spItf) { m_spItf = spItf; } STDMETHOD(OnCall)(ICallFrame* pFrame) { LPWSTR itf, method; HRESULT hr = pFrame->GetNames(&itf, &method); hr = pFrame->Invoke(m_spItf.p); ATLTRACE("call %s::%s %8xn", itf, method, hr); CoTaskMemFree(itf); CoTaskMemFree(method); return hr; } private: CComPtr<T> m_spItf; }; |
Вызывая ICallFrame::Invoke, мы не передаем никаких параметров – значения для параметров перехватываемого метода были переданы клиентом, когда он выполнял вызов через перехватчик.
ПРИМЕЧАНИЕ Метод ICallFrame::Invoke имеет переменное количество параметров (что редко встречается у COM-интерфейсов). Если стек параметров вызова заполнен только частично, в Invoke могут передаваться дополнительные параметры вызова (которые будут добавлены в стек перед вызовом). |
Косвенные и асинхронные/отложенные вызовы
Мы научились выполнять прямые вызовы через указатель на перехватываемый интерфейс. Такой перехватчик может выполнять трассировку вызовов и их результатов, облегчать процесс отладки сложных компонентов, отслеживать значения отдельных параметров (и заменять их в целях отладки).
С помощью перехватчиков COM+ можно выполнять косвенные и асинхронные вызовы. Вместо прямого вызова ICallFrame::Invoke мы можем:
сохранить содержимое параметров, находящихся в стеке, в специальный буфер (фактически выполнить маршалинг параметров);
передать их с помощью любого доступного транспорта (RPC, MSMQ, SOAP, файлы и т.п.) компоненту;
выполнить вызов;
получить значения [out] параметров, выполнить обратный маршалинг;
передать значения параметров клиенту с помощью любого доступного транспорта.
Для упаковки стека вызова, т.е. маршалинга предназначен метод ICallFrame::Marshal:
HRESULT Marshal( CALLFRAME_MARSHALCONTEXT * pmshlContext, // контекст (т.e. inproc и т.п.) MSHLFLAGS * mshlflags, // обычный или табличный маршалинг PVOID pBuffer, // буфер ULONG cbBuffer, // размер буфера ULONG * pcBufferUsed, // использованный размер буфера RPCOLEDATAREP * pdataRep, // формат представления данных ULONG * prpcFlags // RPC-флаги ); |
Размер буфера, необходимого для маршалинга, можно определить с помощью ICallFrame::GetMarshalSizeMax:
HRESULT GetMarshalSizeMax( CALLFRAME_MARSHALCONTEXT * pmshlContext, // контекст (т.e. inproc и т.п.) MSHLFLAGS mshlflags, // обычный или табличный маршалинг ULONG * pcbBufferNeeded // необходимый размер буфера ); |
Обратное преобразование буфера в стек вызова выполняется с помощью специального интерфейса ICallUnmarshal и его метода ICallUnmarshal::Unmarshal:
HRESULT Unmarshal( ULONG iMethod, // номер метода PVOID pBuffer, // буфер ULONG cbBuffer, // размер буфера BOOL fForceBufferCopy, // сохранить копию буфера RPCOLEDATAREP dataRep, // формат представления данных CALLFRAME_MARSHALCONTEXT * pcontext, // контекст (т.e. inproc и т.п.) ULONG * pcbUnmarshalled, // размер использованной части буфера ICallFrame ** ppFrame // ICallFrame со стеком вызова ); |
Интерфейс ICallUnmarshal поддерживается перехватчиком, который мы получаем вызовом CoGetInterceptor. Таким образом, чтобы преобразовать буфер в стек вызова, нам необходимо:
создать перехватчик в адресном пространстве сервера (т.е. вызываемого компонента);
запросить у него (через QI) указатель на интерфейс ICallUnmarshal;
вызывать ICallUnmarshal::Unmarshal – мы получим указатель на интерфейс ICallFrame.
После вызова компонента обычно нужно передать выходные (out) параметры обратно клиенту. Сделать это можно парой вызовов:
ICallFrame::Marshal на серверной стороне;
ICallFrame::Unmarshal на стороне клиента.
HRESULT UnMarshal( PVOID pBuffer, // буфер с out-параметрами ULONG cbBuffer, // размер буфера RPCOLEDATAREP pdataRep, // формат представления данных CALLFRAME_MARSHALCONTEXT * pcontext, // контекст (т.e. inproc и т.п.) ULONG * pcbUnmarshaled // размер использованной части буфера ); |
Тип маршалинга параметров – in или out – задается флагом структуры CALLFRAME_MARSHALCONTEXT.
Последовательность вызовов при маршалинге in- и out-параметров проиллюстрирована на рисунке 4.
Рисунок 4. Маршалинг параметров.
В качестве примера, использующего возможности маршалинга параметров, разработаем перехватчик, передающий вызовы серверному компоненту не с помощью традиционного в таких случаях RPC, а через очереди MSMQ (Microsoft Message Queueing).
ПРИМЕЧАНИЕ В COM+ имеется поддержка MSMQ в качестве транспорта. Для COM+-компонентов (такие компоненты называются “queued components”) с помощью MSMQ выполняются асинхронные вызовы, т.е. клиент не ждет завершения вызова и, следовательно, значения out-параметров клиенту не передаются. В нашем примере мы будем выполнять синхронные вызовы с передачей out-параметров клиенту. |
Для общения с сервером нам потребуются 2 очереди MSMQ: для сообщений с in-параметрами и с out-параметрами. Мы будем использовать private-очереди, т.е. очереди, доступ к которым возможен только по полному пути с указанием имени компьютера.
ПРИМЕЧАНИЕ Альтернативный тип очередей MSMQ: public-очереди. Информация о них хранится в Active Directory и доступ к ним возможен по имени (без указания полного пути). |
Для работы с очередями нам понадобится функция CreateQueue, создающая private-очередь (в качестве имени подойдет GUID, сгенерированный функцией CoCreateGuid):
HRESULT CreateQueue(CComBSTR& queue) { CLSID guid = CLSID_NULL; ::CoCreateGuid(&guid); CComBSTR path = L".Private$"; path.Append(guid); const int NumberOfProps = 1; MQPROPVARIANT aQueuePropVar[NumberOfProps]; aQueuePropVar[0].vt = VT_LPWSTR; aQueuePropVar[0].pwszVal = path; QUEUEPROPID aQueuePropId[NumberOfProps] = { PROPID_Q_PATHNAME }; HRESULT aQueueStatus[NumberOfProps] = { S_OK }; MQQUEUEPROPS props; props.cProp = NumberOfProps; props.aPropID = aQueuePropId; props.aPropVar = aQueuePropVar; props.aStatus = aQueueStatus; WCHAR buffer[256]; DWORD dwLen = sizeof(buffer)/sizeof(buffer[0]); HRESULT hr = ::MQCreateQueue(0, &props, buffer, &dwLen); if(SUCCEEDED(hr)) { queue = buffer; } return hr; } |
Еще нам потребуется класс Queue, позволяющий отправлять и получать сообщения методами Send и Receive (в синхронном режиме с ожиданием появления сообщения):
class Queue { public: Queue() : m_hQueue(0) {} HRESULT Init(LPWSTR name, DWORD dwAccess) { Close(); return MQOpenQueue(name, dwAccess, MQ_DENY_NONE, &m_hQueue); } HRESULT Send(BYTE* buffer, DWORD cbSize) { const int NumberOfProps = 2; PROPVARIANT aMsgPropVar[NumberOfProps]; aMsgPropVar[0].vt = VT_VECTOR | VT_UI1; aMsgPropVar[0].caub.pElems = buffer; aMsgPropVar[0].caub.cElems = cbSize; aMsgPropVar[1].vt = VT_UI4; aMsgPropVar[1].lVal = VT_ARRAY | VT_UI1; MSGPROPID aMsgPropId[NumberOfProps]={PROPID_M_BODY, PROPID_M_BODY_TYPE}; HRESULT aMsgStatus[NumberOfProps] = {S_OK, S_OK}; MQMSGPROPS msgprops; msgprops.cProp = NumberOfProps; msgprops.aPropID = aMsgPropId; msgprops.aPropVar = aMsgPropVar; msgprops.aStatus = aMsgStatus; return MQSendMessage(m_hQueue, &msgprops, MQ_NO_TRANSACTION); } HRESULT Receive(BYTE** pBuffer, DWORD* pcbSize, DWORD timeout = INFINITE) { const int NumberOfProps = 2; PROPVARIANT aMsgPropVar[NumberOfProps]; aMsgPropVar[0].vt = VT_VECTOR | VT_UI1; aMsgPropVar[0].caub.pElems = 0; aMsgPropVar[0].caub.cElems = 0; aMsgPropVar[1].vt = VT_NULL; MSGPROPID aMsgPropId[NumberOfProps]={PROPID_M_BODY, PROPID_M_BODY_SIZE}; HRESULT aMsgStatus[NumberOfProps] = {S_OK, S_OK}; MQMSGPROPS msgprops; msgprops.cProp = NumberOfProps; msgprops.aPropID = aMsgPropId; msgprops.aPropVar = aMsgPropVar; msgprops.aStatus = aMsgStatus; HRESULT hr = MQReceiveMessage(m_hQueue, timeout, MQ_ACTION_RECEIVE, &msgprops, 0, 0, 0, MQ_SINGLE_MESSAGE); if(hr == MQ_ERROR_BUFFER_OVERFLOW) { aMsgPropVar[0].caub.pElems = reinterpret_cast<UCHAR*>(malloc(aMsgPropVar[1].lVal)); aMsgPropVar[0].caub.cElems = aMsgPropVar[1].lVal; hr = MQReceiveMessage(m_hQueue, timeout, MQ_ACTION_RECEIVE, &msgprops, 0, 0, 0, MQ_SINGLE_MESSAGE); if(SUCCEEDED(hr)) { *pBuffer = aMsgPropVar[0].caub.pElems; *pcbSize = aMsgPropVar[0].caub.cElems; } else { free(aMsgPropVar[0].caub.pElems); } } return hr; } HRESULT Close() { HRESULT hr = S_OK; if(m_hQueue) { hr = ::MQCloseQueue(m_hQueue); m_hQueue = 0; } return hr; } ~Queue() { Close(); } private: QUEUEHANDLE m_hQueue; }; |
В методе обработчика вызова ICallFrameEvents::OnCall (см. пример выше) вместо прямого вызова исходного компонента с помощью ICallFrame::Invoke мы будем выполнять маршалинг in-параметров, передачу их через очереди MSMQ и обратное преобразование для out-параметров из буфера маршалинга в стек вызова. Помимо преобразованных в буфер маршалинга in-параметров на приемной стороне нам потребуется информация о IID перехватываемого интерфейса и номере вызываемого метода. Эти данные мы будем передавать в заголовке запроса:
struct CallHdr // заголовок запроса { CLSID coclass; // CLSID исходного компонента IID itf; // IID перехватываемого интерфейса ULONG method; // номер перехватываемого метода ULONG rep; // формат представления данных RPC }; ... // устанавливаем контекст маршалинга – in-параметры, MSHCTX_INPROC CALLFRAME_MARSHALCONTEXT ctx = {TRUE, MSHCTX_INPROC}; DWORD cbSize = 0; // определяем размер, необходимый для буфера HRESULT hr = pFrame->GetMarshalSizeMax(&ctx, MSHLFLAGS_NORMAL, &cbSize); if(SUCCEEDED(hr)) { cbSize += sizeof(CallHdr); BYTE* pBuffer = reinterpret_cast<BYTE*>(malloc(cbSize)); CallHdr* pHdr = reinterpret_cast<CallHdr*>(pBuffer); ULONG rep = 0; // маршалинг in-параметров в буфер hr = pFrame->Marshal(&ctx, MSHLFLAGS_NORMAL, pBuffer + sizeof(CallHdr), cbSize, &cbSize, &rep, 0); if(SUCCEEDED(hr)) { pHdr->rep = rep; // получаем у перехватчика номер метода и IID интерфейса для заголовка hr = pFrame->GetIIDAndMethod(&pHdr->itf, &pHdr->method); if(SUCCEEDED(hr)) { pHdr->coclass = m_coclass; // отправляем запрос серверу hr = m_qin.Send(pBuffer, cbSize + sizeof(CallHdr)); if(SUCCEEDED(hr)) { free(pBuffer); pBuffer = 0; cbSize = 0; // получаем отклик сервера с out-параметрами hr = m_qout.Receive(&pBuffer, &cbSize); if(SUCCEEDED(hr)) { // восстанавливаем значения out-параметров и HRESULT // вызова метода на сервере hr = pFrame->Unmarshal(pBuffer, cbSize, rep, &ctx, &cbSize); } } } } if(pBuffer) { free(pBuffer); } } return hr; |
На серверной стороне нам необходимо восстановить стек вызова из полученного от клиента бинарного буфера, сделать вызов исходного компонента и выполнить маршалинг out-параметров и результата вызова метода для клиента:
mq::Queue qin, qout; qin.Init(pInfo->queue_in, MQ_RECEIVE_ACCESS); qout.Init(pInfo->queue_out, MQ_SEND_ACCESS); while(true) { const DWORD timeout = 500; BYTE* pBuffer = NULL; DWORD cbSize = 0; // получаем запрос от клиента HRESULT hr = qin.Receive(&pBuffer, &cbSize, timeout); if(SUCCEEDED(hr)) { CallHdr* pHdr = reinterpret_cast<CallHdr*>(pBuffer); cbSize -= sizeof(CallHdr); CComPtr<ICallUnmarshal> spUnmarshal; // создаем перехватчик на серверной стороне hr = CoGetInterceptor(pHdr->itf, 0, IID_ICallUnmarshal, (void**)&spUnmarshal); if(SUCCEEDED(hr)) { CComPtr<ICallFrame> spFrame; CALLFRAME_MARSHALCONTEXT ctx = { TRUE, MSHCTX_INPROC }; // выполняем преобразование буфера маршалинга в стек вызова hr = spUnmarshal->Unmarshal(pHdr->method, pBuffer + sizeof(CallHdr), cbSize, FALSE, pHdr->rep, &ctx, &cbSize, &spFrame); if(SUCCEEDED(hr)) { CComPtr<IUnknown> spUnk; // создаем экземпляр компонента hr = CoCreateInstance(pHdr->coclass, 0, CLSCTX_ALL, pHdr->itf, (void**)&spUnk); if(SUCCEEDED(hr)) { // вызываем исходный метод hr = spFrame->Invoke(spUnk.p); if(SUCCEEDED(hr)) { ctx.fIn = FALSE; cbSize = 0; free(pBuffer); pBuffer = NULL; // маршалинг out-параметров и результата HRESULT вызова hr = spFrame->GetMarshalSizeMax(&ctx, MSHLFLAGS_NORMAL, &cbSize); if(SUCCEEDED(hr)) { pBuffer = reinterpret_cast<BYTE*>(malloc(cbSize)); hr = spFrame->Marshal(&ctx, MSHLFLAGS_NORMAL, pBuffer, cbSize, &cbSize, 0, 0); } } } } } if(FAILED(hr)) { cbSize = sizeof(HRESULT); *reinterpret_cast<HRESULT*>(pBuffer) = htonl(hr); } // отправляем ответ клиенту hr = qout.Send(pBuffer, cbSize); if(pBuffer) { free(pBuffer); } } if(WaitForSingleObject(pInfo->hShutdown, timeout) == WAIT_OBJECT_0) break; } |
ПРЕДУПРЕЖДЕНИЕ В методе ICallUnmarshal::Unmarshal явно указывается номер метода (первый параметр). Хотя в документации сказано, что возможное значение этого параметра -1 (в этом случае перехватчик сам определит номер метода, прочитав эту информацию из буфера с данными маршалинга), на практике такое значение использовать не удалось – при использовании -1 вызов ICallUnmarshal::Unmarshal завершается ошибкой доступа к памяти Access Violation в модуле ole32.dll |
В рассмотренном выше примере нам не пришлось внести ни одного изменения ни в код клиента, ни в код компонента – благодаря технологии перехвата вызовов вся работа по организации взаимодействия клиента с компонентом происходит для них абсолютно прозрачно.
Использование такого “ручного” маршалинга параметров позволяет нам увидеть, какой информацией обмениваются proxy/stub в стандартной инфраструктуре COM. На иллюстрации приведен пример буфера, содержащего строчку BSTR, массив SAFEARRAY и объектную ссылку (указатель на интерфейс):
Рисунок 5. Буфер с in-параметрами вызова
Работа с параметрами вызова в обработчике
В примере выше мы делегировали работу по преобразованию стека вызова в бинарный буфер перехватчику. Бинарные буферы с параметрами вызова отлично подходят для многих видов транспорта – RPC, MSMQ. Однако если бы мы захотели использовать SOAP для передачи вызова компоненту, такое бинарное представление было бы неприемлемо, так как SOAP-сообщение представляет собой XML-текст, содержащий значения каждого из in-параметров по отдельности. Подробнее о протоколе SOAP и формате SOAP-сообщений можно прочитать в статье: “Использование протокола SOAP в распределенных приложениях Microsoft SOAP Toolkit 3.0”.
В этой статье был рассмотрен способ создания Proxy, работающей через ранее связывание (SOAP Toolkit использует IDispatch и позднее связывание для вызовов). Proxy поддерживала интерфейс:
interface ISoapProxy : IDispatch { [id(1), helpstring("method Initialize")] HRESULT Initialize([in]BSTR wsdl, [in]BSTR wsml, [in]BSTR service, [in]BSTR port); [propget, id(2), helpstring("property ConnectorProperty")] HRESULT ConnectorProperty([in]BSTR prop, [out, retval] VARIANT *pVal); [propput, id(2), helpstring("property ConnectorProperty")] HRESULT ConnectorProperty([in]BSTR prop, [in] VARIANT newVal); [propget, id(3), helpstring("property ProxyProperty")] HRESULT ProxyProperty([in]BSTR prop, [out, retval] VARIANT *pVal); [propput, id(3), helpstring("property ProxyProperty")] HRESULT ProxyProperty([in]BSTR prop, [in] VARIANT newVal); [id(4), helpstring("method GetOperation")] HRESULT GetOperation([in]BSTR name, [out,retval]IWSDLOperation** ppOp); [id(5), helpstring("method Execute")] HRESULT Execute([in]IWSDLOperation* pOp); }; |
Для каждого из интерфейсов/методов был написан код, перенаправляющий вызовы Proxy, которая, в свою очередь, использовала низкоуровневые компоненты из SOAP Toolkit для передачи вызова SOAP-серверу.
Например, реализация одного из методов выглядела так:
STDMETHOD(KillProcess)(LONG processID) { try { // получаем описание операции KillProcess IWSDLOperationPtr spOp = m_spSoapProxy->GetOperation(L"KillProcess"); IEnumSoapMappersPtr spEnum; // заполняем значения входных параметров spOp->GetOperationParts(&spEnum); while(true) { long l = 0; ISoapMapperPtr spMap; spEnum->Next(1, &spMap, &l); if(l == 1) { if(spMap->PartName == _bstr_t(L"processID")) spMap->ComValue = processID; } else break; } // передаем вызов серверу m_spSoapProxy->Execute(spOp); } catch(_com_error & e) { return e.Error(); } return S_OK; } |
ПРИМЕЧАНИЕ В этом коде m_spSoapProxy – экземпляр Proxy (описание интерфейса см. выше) |
Реализации для разных методов различались лишь названиями методов (или операций, в терминах SOAP) и названиями параметров. Вместо того, чтобы писать однотипный код, можно создать перехватчик для нужного интерфейса CoGetInterceptor, а в методе ICallFrameEvents::OnCall, напрямую манипулируя с параметрами вызова, создать SOAP-сообщение.
Получить значение параметра позволяет метод ICallFrame::GetParam:
HRESULT GetParam( ULONG iparam, VARIANT * pvar ); |
Нам нужен номер параметра, который можно получить из описания SOAP-операции ISoapMapper::get_CallIndex:
[propget] HRESULT callIndex([out, retval] long* par_lCallIndex); |
После вызова метода нам потребуется метод для задания нового значения out-параметра в стеке ICallInfo::SetParam и метод для задания результата выполнения метода ICallInfo::SetReturnValue:
HRESULT SetParam( ULONG iparam, VARIANT * pvar ); HRESULT SetReturnValue( HRESULT hr ); |
И, наконец, нужно отличать in- и out-параметры. Сделать это можно вызовом ISoapMapper::get_IsInput.
Полный код реализации обработчика вызова приведен ниже:
STDMETHOD(OnCall)(ICallFrame* pFrame) { LPWSTR lpszItf ,lpszMethod; HRESULT hr = pFrame->GetNames(&lpszItf, &lpszMethod); CoTaskMemFree(lpszItf); // получаем описание SOAP-операции из WSML CComPtr<IWSDLOperation> spOp; hr = m_spProxy->GetOperation(CComBSTR(lpszMethod) , &spOp); CoTaskMemFree(lpszMethod); if(SUCCEEDED(hr)) { CComPtr<IEnumSoapMappers> spEnum; hr = spOp->GetOperationParts(&spEnum); if(SUCCEEDED(hr)) { // перебираем все параметры while(hr == S_OK) { CComPtr<ISoapMapper> spMapper; long lFetched = 0; hr = spEnum->Next(1, &spMapper, &lFetched); if(!lFetched || hr != S_OK) break; long idx = 0; smIsInputEnum paramType; hr = spMapper->get_IsInput(¶mType); // для in-параметров берем значения из стека if(paramType == smInput || paramType == smInOut) { hr = spMapper->get_callIndex(&idx); if(SUCCEEDED(hr) && (idx >= 0)) { CComVariant value; hr = pFrame->GetParam(idx, &value); hr = spMapper->put_ComValue(value); } } } if(SUCCEEDED(hr)) { // выполняем вызов hr = m_spProxy->Execute(spOp); } } if(SUCCEEDED(hr)) { // перебираем все параметры spEnum->Reset(); while(hr == S_OK) { CComPtr<ISoapMapper> spMapper; long lFetched = 0; hr = spEnum->Next(1, &spMapper, &lFetched); if(!lFetched || hr != S_OK) break; smIsInputEnum paramType; hr = spMapper->get_IsInput(¶mType); // для out-параметров устанавливаем новое значение if(paramType == smOutput || paramType == smInOut) { long idx = 0; hr = spMapper->get_callIndex(&idx); if(SUCCEEDED(hr) && idx >= 0) { CComVariant value; hr = spMapper->get_ComValue(&value); hr = pFrame->SetParam(idx, &value); } } } } else { // если вызов завершился с ошибкой – устанавливаем return value pFrame->SetReturnValue(hr); } } return hr; } |
Приведенный выше код работать не будет. :с))
Во-первых, вызов ISoapMapper::get_callIndex всегда возвращает -1, независимо от параметра.
Во-вторых, вызов ICallFrame::SetParam возвращает ошибку E_NOTIMPL, т.е. он попросту не реализован для перехватчика.
Обходной путь для первой проблемы заключается в использовании другого метода – IsoapMapper::get_ParameterOrder, возвращающего порядковый номер параметра в описании WSML. Как правило, порядковый номер в описании WSML соответствует порядковому номеру параметра в сигнатуре метода.
ПРИМЕЧАНИЕ По крайней мере, стандартный генератор WSML из SOAP Toolkit генерирует WSML именно так. Возможно, в будущих версиях SOAP Toolkit эта проблема будет исправлена, и мы сможем использовать более уместный в данном случае метод callIndex. |
Решение второй проблемы не так очевидно. Необходимо каким-либо образом поместить в стек вызова значение out-параметра, но единственный подходящий для этих целей метод ICallFrame::SetParam возвращает E_NOTIMPL.
Разумеется, мы могли бы остановиться на этом. Наш пример корректно работает с in-параметрами, но не умеет передавать out-параметры.
Но все же есть способ добраться до местоположения адреса нужного параметра в стеке. Можно узнать адрес стека вызова с помощью ICallFrame::GetStackLocation:
PVOID GetStackLocation(void); |
А также получить информацию о параметре метода, его местоположение в стеке:
typedef struct { BOOLEAN fIn; BOOLEAN fOut; ULONG stackOffset; ULONG cbParam; } CALLFRAMEPARAMINFO; HRESULT GetParamInfo( ULONG iparam, CALLFRAMEPARAMINFO * pInfo ); |
Теперь, если сложить адрес первого аргумента в стеке вызова и смещение нужного параметра CALLFRAMEPARAMINFO::stackOffset, мы получим адрес параметра в стеке. Код, заполняющий out-параметр выглядел так:
CComVariant value; hr = spMapper->get_ComValue(&value); hr = pFrame->SetParam(idx, &value); |
Мы перепишем его так:
CComVariant value; hr = spMapper->get_ComValue(&value); PVOID pStack = pFrame->GetStackLocation(); CALLFRAMEPARAMINFO info = {0}; hr = pFrame->GetParamInfo(idx, &info); if(SUCCEEDED(hr) && info.cbParam == sizeof(long*)) { long** pParam = reinterpret_cast<long**>( reinterpret_cast<BYTE*>(pStack) + info.stackOffset); if(!IsBadReadPtr(*pParam, sizeof(long))) { VARIANT var = {}; value.Detach(&var); **pParam = var.lVal; } } |
ПРИМЕЧАНИЕ Такой способ не выглядит изящным, к тому же он не будет работать, если размер параметра в стеке отличается от 4. |
В первой части статьи мы познакомились с возможными способами перехвата вызовов методов COM-интерфейсов, их ограничения и слабые стороны, а также могли оценить сложность реализации универсального перехватчика.
Во второй части мы увидели, как наличие информации о сигнатурах методов может упростить реализацию перехватчика, и рассмотрели стандартную реализацию перехватчика из инфраструктуры COM/COM+ ICallInterceptor. Приведенные в статье примеры используют различные виды транспорта для передачи вызова от клиента компоненту – прямые вызовы, MSMQ, SOAP.
Алексей Остапенко. “Перехват методов интерфейса IUnknown”, RSDN Magazine, №3 2003.
Иван Андреев. “Использование протокола SOAP в распределенных приложениях Microsoft SOAP Toolkit 3.0”. RSDN Magazine №1 2003
MSDN, раздел Platform SDK: COM, описание CoGetInterceptor
Universal Delegator:
Building a Lightweight COM Interception Framework, Part 1: The Universal Delegator
Building a Lightweight COM Interception Framework, Part II: The Guts of the UD