Задача обработчика – решить, что делать дальше с вызовом:
Отклонить его, вернув ошибку.
Сохранить стек параметров вызова, чтобы выполнить его асинхронно.
Выполнить вызов немедленно.
Информацию о вызове обработчик получает с помощью указателя на интерфейс 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 %8x\n", 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_INPROCCALLFRAME_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-параметров и результата вызова метода для клиента: