Перехват методов COM интерфейсов
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-параметров проиллюстрирована на р