Смекни!
smekni.com

Перехват методов COM интерфейсов (стр. 3 из 5)

ПРИМЕЧАНИЕАвтор говорит о стеке, но по каким-то причинам использует двунаправленную очередь. С точки зрения функциональности это, в общем, безразлично, но несколько сбивает с толку. – прим. ред.

Внутри перехватчика указатель на нужный 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 // восстанавливаем инфрмацию из TLSadd 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.