Перехват методов COM интерфейсов – 2

Перехват интерфейсов, не совместимых с automation

Автор: Иван Андреев aka Ivan
The RSDN Group

Источник: RSDN Magazine #2-2005
Опубликовано: 11.07.2005
Версия текста: 1.0
Введение
Компилятор MIDL и генерируемые Proxy/Stub
Как получить экземпляр Proxy/Stub
Информация о vtbl custom-интерфейсов
Форматные строки
IRpcChannelBuffer
Перехват с помощью IRpcChannelBuffer
Реализация перехватчика на основе NDR RPC Format Strings
NDR Engine
Универсальный перехватчик – ItfThunk
ICallInterceptor и ICallFrame
Заключение

Исходные коды: Source.zip

Введение

В первой части статьи о перехвате методов COM-интерфейсов был описан механизм, позволяющий использовать стандартные системные перехватчики из инфраструктуры COM(+). API-функция CoGetInterceptor создает перехватчик для заданного интерфейса, специальные интерфейсы ICallFrame и ICallFrameWalker обеспечивают доступ к переданным в вызов метода in- и out-параметрам, и позволяют “найти” все указатели на интерфейсы среди параметров стека вызова.

Разумеется, в этой инфраструктуре есть свои “подводные камни” и особенности (например, вызов CoGetInterceptor на некоторых платформах приводит к Access Violation, если до этого не была инициализирована подсистема RPC, см. предыдущую статью), но в целом задача перехвата решена достаточно эффективно и универсально.

Одним из самых серьезных ограничений функции CoGetIntercptor является невозможность работы с не oleautomation-совместимыми интерфейсами, т.е. такими интерфейсами, для которых не используется typelib-маршалинг.

Из этого ограничения вытекает невозможность перехвата большинства стандартных интерфейсов – IStream, IEnumVARIANT и т.д. Кроме того, так как automation накладывает на параметры интерфейсов достаточно сильные ограничения в выборе типов параметров (только automation-совместимые типы), в приложениях нередко встречаются custom-интерфейсы, для которых генерируются proxy/stub и используется ручной маршалинг.

Целью этой статьи будет поиск альтернативного механизма перехвата методов COM-интерфейсов, который был бы пригоден для использования с интерфейсами, несовместимыми с automation.

Компилятор MIDL и генерируемые Proxy/Stub

Существует возможность полностью реализовать для custom-интерфейсов собственный маршалинг параметров и предоставить клиентам собственные варианты компонентов Proxy/Stub. Однако на практике это очень трудоемкая задача, решение которой зачастую не дает никаких преимуществ по сравнению с использованием в этих целях стандартной инфраструктуры COM.

ПРИМЕЧАНИЕ

Вспомним, что пара компонентов Proxy/Stub обеспечивает одно из фундаментальных свойств COM – “location transparency”, прозрачность местонахождения компонента. “Представитель” компонента на стороне клиента – Proxy – поддерживает тот же самый интерфейс, что и компонент, и неотличим от него, Stub на серверной стороне преобразует вызовы от Proxy в вызовы реального компонента.

Поэтому чаще всего COM-интерфейсы, их методы и параметры описываются на языке IDL, а компилятор MIDL генерирует по этому описанию несколько файлов:

Ситуация с typelib-маршалингом достаточно проста – библиотека типов содержит в легкодоступном формате полную информацию обо всех методах и параметрах интерфейсов. Эта информация может использоваться во время выполнения для того, чтобы универсальный код мог выполнять маршалинг произвольных интерфейсов. Перехватчики CoGetInterceptor также используют библиотеку типов, чтобы дать клиенту исчерпывающую информацию обо всех параметрах в стеке вызова.

Напротив, Proxy/Stub для custom-интерфейсов представляют собой “черный ящик”. Их код для заданного интерфейса компилируется во время сборки приложения, и во время выполнения “добраться” до информации об интерфейсе либо сложно, либо вовсе невозможно. С другой стороны, возможность анализировать переданные в стеке вызова параметры является ключевой для перехватчика. Представим себе такую ситуацию: клиент вызывает компонент A, для которого был установлен перехватчик вызовов. Компонент A возвращает клиенту в out-параметре указатель на интерфейс компонента B. Если перехватчик не сможет заменить в стеке вызова out-параметр, то клиент получит прямой (т.е. без перехватчика) указатель на интерфейс компонента B.

Хорошая новость заключается в том, что стандартные Proxy/Stub, генерируемые компилятором MIDL, могут использовать интерпретируемый маршалинг. Это значит, что по описанию интерфейса в IDL создаются специальные бинарные структуры, описывающие сигнатуры методов, а универсальный код Proxy/Stub, интерпретируя эти структуры во время выполнения, выполняет маршалинг парметров.

Согласно документации, поведение MIDL задается ключами компиляции. Для генерации полностью интерпретируемых Proxy/Stub служит ключ /Oicf, и так как в новых версиях MIDL этот ключ установлен по умолчанию и используется чаще всего, большинство custom-интерфейсов используют именно интерпретируемый тип Proxy/Stub.

ПРИМЕЧАНИЕ

Другое название ключа Oicf – “generate stubless proxy”, т.е. MIDL будет генерировать proxy, для которых не нужны stub. Stub, разумеется, все равно будет использоваться, но он будет одинаковым вне зависимости от интерфейса, тогда как реализация proxy для каждого интерфейса своя.

Таким образом, для интерфейсов, описание которых было скомпилировано с ключом Oicf, существует доступная во время выполнения информация о сигнатурах методов. Это означает, что может быть реализован универсальный перехватчик, основное отличие которого от CoGetInterceptor заключается в источнике информации – в одном случае это библиотека типов, в другом – сгенерированное MIDL описание, которым пользуются стандартные Proxy/Stub.

Хотя в каком-то степени задача перехвата custom-интерфейсов может быть решена и для Proxy/Stub, скомпилированных с другими ключами MIDL (а, возможно, и для интерфейсов, которые были описаны и вовсе без IDL), в статье будут рассматриваться только полностью интерпретируемые Proxy/Stub, т.е. те, что были построены с ключом Oicf.

Основным источником информации будет раздел MSDN – RPC NDR Format strings, в котором описывается формат интерпретируемых данных и исходные тексты на языке C для Proxy, которые генерирует MIDL по описаниям интерфейсов.

Как получить экземпляр Proxy/Stub

Как мы увидели, стандартные Proxy/Stub из инфраструктуры COM в случае полностью интерпретируемого маршалинга содержат всю необходимую перехватчику информацию. Кроме того, архитектура стандартного маршалинга сделана расширяемой. Это означает, что в роли компонента-перехватчика может быть использован обычный Proxy, для которого соответствующим образом настроен канал для передачи данных к Stub-у IRpcChannelBuffer.

Прежде всего, нам нужно научиться создавать пару proxy/Stub для заданного по IID интерфейсу. Это можно сделать следующим образом:

     
// получаем CLSID для фабрики Proxy/Stub
CLSID clsidPs = GUID_NULL;
CoGetPSClsid(__uuidof(IBar), &clsidPs);

CComPtr<IPSFactoryBuffer> spFactBuffer;

// получаем экземпляр фабрики Proxy/Stub
CoGetClassObject(clsidPs, CLSCTX_INPROC, 0, IID_IPSFactoryBuffer, 
  reinterpret_cast<void**>(&spFactBuffer));

// создаем стандартный Proxy
CComPtr<IRpcProxyBuffer> spProxy;
CComPtr<IBar> spProxyItf;
spFactBuffer->CreateProxy(0, IID_IBar, &spProxy, (void**)&spProxyItf);

// создаем стандартный Stub
CComPtr<IRpcStubBuffer> spStub;
spFactBuffer->CreateStub(IID_IBar, 0, &spStub);

В этом примере IBar – custom-интерфейс, для которого создаются Proxy и Stub. Сначала создается экземпляр фабрики, которая поддерживает интерфейс ISPFactoryBuffer, затем, с помощью этого интерфейса можно создавать экземпляры стандартных Proxy (вызовом CreateProxy) и Stub (вызовом CreateStub).

Как правило, в качестве CLSID для фабрики выбирается IID первого custom-интерфейса в IDL. API-функция CoGetClassObject загружает dll с реализацией Proxy/Stub для custom-интерфейсов и вызывает точку входа в dll DllGetClassObject так же, как и в случае обычного COM-компонента. Типичная реализация функции DllGetClassObject для Proxy/Stub передает вызов другой функции NdrDllGetClassObject вместо того, чтобы создать обычную фабрику компонентов. Таким образом, для различных интерфейсов и их Proxy/Stub используется стандартная реализация фабрики IPSFactoryBuffer, которая находится внутри rpcrt4.dll и создается вызовом NdrDllGetClassObject:

RPCRTAPI HRESULT RPC_ENTRY NdrDllGetClassObject(
  REFCLSID rclsid,
  REFIID riid,
  void** ppv,
  const ProxyFileInfo** pProxyFileList,
  const CLSID* pclsid,
  CStdPSFactoryBuffer* pPSFactoryBuffer
);

typedef struct tagProxyFileInfo 
{
  const PCInterfaceProxyVtblList* pProxyVtblList;
  const PCInterfaceStubVtblList* pstubVtblList;
  const PCInterfaceName* pNamesArray;
  const IID** pDelegatedIIDs;
  const PIIDLookup pIIDLookupRtn;
  unsigned short TableSize;
  unsigned short TableVersion;
  const IID** pAsyncIIDLookup;
  LONG_PTR Filler2;
  LONG_PTR Filler3;
  LONG_PTR Filler4;
} ProxyFileInfo;

В приведенном выше описании функции NdrDllGetClassObject мы видим, что в вызов передается параметр const ProxyFileInfo** pProxyFileList. Именно этот параметр позволяет универсальной реализации фабрики получить информацию о custom-интерфейсах, которая генерируется компилятором MIDL по IDL-описанию интерфейсов, и в бинарном виде находится в модуле dll для Proxy/Stub.

Структура ProxyFileInfo содержит vtbl Proxy и Stub для всех custom-интерфейсов, имена интерфейсов и их количество. TableVersion указывает тип Proxy/Stub. Для полностью интерпретируемых Oicf его значение будет равно 2.

Ниже приведен пример сгенерированной компилятором MIDL структуры для тестового проекта cptest с двумя custom-интерфейсами IBar и IFoo:

const ExtendedProxyFileInfo cptest_ProxyFileInfo =
{
  (PCInterfaceProxyVtblList*) &_cptest_ProxyVtblList,
  (PCInterfaceStubVtblList*) &_cptest_StubVtblList,
  (const PCInterfaceName* ) &_cptest_InterfaceNamesList,
  0, // никакой делегации
  &_cptest_IID_Lookup,
  2,
  2,
  0, /* таблица [async_uuid] интерфейсов */
  0, /* Filler1 */
  0, /* Filler2 */
  0  /* Filler3 */
};

PCInterfaceName const _cptest_InterfaceNamesList[] =
{
  "IFoo",
  "IBar",
  0
};

const CInterfaceProxyVtbl* _cptest_ProxyVtblList[] =
{
  (CInterfaceProxyVtbl*)&_IFooProxyVtbl,
  (CInterfaceProxyVtbl*)&_IBarProxyVtbl,
  0
};

const CInterfaceStubVtbl* _cptest_StubVtblList[] =
{
  (CInterfaceStubVtbl*)&_IFooStubVtbl,
  (CInterfaceStubVtbl*)&_IBarStubVtbl,
  0
};

Помимо этих структур для каждого из custom-интерфейсов создается пара структур с типами CInterfaceProxyVtbl и CInterfaceStubVtbl. Они описывают структуру vtbl интерфейса и либо содержат адреса методов-обработчиков для обычных Proxy/Stub, либо специальную бинарную строку, описывающую сигнатуры методов для интерпретируемых Proxy/Stub.

Благодаря существованию этих структур универсальная реализация фабрики во время выполнения находит информацию для нужного custom-интерфейса и создает экземпляры стандартных Proxy и Stub, инициализуруя их описанием vtbl – CInterfaceProxyVtbl и CInterfaceStubVtbl.

Информация о vtbl custom-интерфейсов

В предыдущем разделе статьи мы остановились на том, что для каждого custom-интерфейса компилятор MIDL генерирует пару структур CInterfaceProxyVtbl и CInterfaceStubVtbl, которые содержат исчерпывающее описание интерфейса для маршалинга стандартными средствами COM.

Описание этих структур приведено ниже:

typedef struct tagCInterfaceProxyHeader
{
  //
  // New fields should be added here, at the beginning of the structure.
  //
#ifdef USE_STUBLESS_PROXY
  const void*  pStublessProxyInfo;
#endif
  const IID*   piid;
} 
CInterfaceProxyHeader;

typedef struct tagCInterfaceProxyVtbl
{
  CInterfaceProxyHeader header;
#if defined(_MSC_VER)
  void* Vtbl[];
#else
  void* Vtbl[1];
#endif
} 
CInterfaceProxyVtbl;

typedef
void
(__RPC_STUB * PRPC_STUB_FUNCTION) (
  IRpcStubBuffer*   This,
  IRpcChannelBuffer _pRpcChannelBuffer,
  PRPC_MESSAGE      _pRpcMessage,
  DWORD*            pdwStubPhase);

typedef struct tagCInterfaceStubHeader
{
  //New fields should be added here, at the beginning of the structure.
  const IID*                piid;
  const MIDL_SERVER_INFO*   pServerInfo;
  unsigned long             DispatchTableCount;
  const PRPC_STUB_FUNCTION* pDispatchTable;
} CInterfaceStubHeader;

typedef struct tagCInterfaceStubVtbl
{
  CInterfaceStubHeader      header;
  struct IRpcStubBufferVtbl Vtbl;
} 
CInterfaceStubVtbl;

Для Proxy у нас есть void* указатель на дополнительную информацию pStublessProxyInfo, IID интерфейса и, непосредственно, vtbl для методов интерфейса. Vtbl содержит либо адреса сгенерированных MIDL-ом функций-перехватчиков с нужной сигнатурой, либо, для интерпретируемого случая (а это наш случай :) ), специальное “неправильное” значение -1 (тогда во время выполнения сюда будут вписаны адреса функции-интерпретатора).

Для Stub доступна примерно такая же информация – IID интерфейса, дополнительная информация для интерпретатора pServerInfo, количество методов в vtbl, адреса обработчиков pDispatchTable и vtbl стандартных методов Stub – IRpcStubBuffer. pDispatchTable, как и у Proxy, либо содержит сгенерированные MIDL адреса функций-обработчиков, либо 0 – для полностью интерпретируемого Stub.

Интерфейс IRpcStubBuffer позволяет вызвать нужный метод у реального компонента с помощью функции Invoke:

HRESULT Invoke (
  RPCOLEMESSAGE*     _prpcmsg,
  IRpcChannelBuffer* _pRpcChannelBuffer
);

Поскольку нас интересуют именно интерпретируемые Proxy/Stub, попробуем разобраться, что содержится в указателях pStublessProxyInfo и pServerInfo.

Судя по коду, который генерирует MIDl, указатель void* pStublessProxyInfo указывает на структуру MIDL_STUBLESS_PROXY_INFO:

typedef struct _MIDL_STUBLESS_PROXY_INFO
{
  PMIDL_STUB_DESC        pStubDesc;
  PFORMAT_STRING         ProcFormatString;
  const unsigned short*  FormatStringOffset;
  PRPC_SYNTAX_IDENTIFIER pTransferSyntax;
  ULONG_PTR              nCount;
  PMIDL_SYNTAX_INFO      pSyntaxInfo;
} 
MIDL_STUBLESS_PROXY_INFO;

typedef struct  _MIDL_SERVER_INFO_
{
  PMIDL_STUB_DESC        pStubDesc;
  const SERVER_ROUTINE*  DispatchTable;
  PFORMAT_STRING         ProcString;
  const unsigned short*  FmtStringOffset;
  const STUB_THUNK*      ThunkTable;
  PRPC_SYNTAX_IDENTIFIER pTransferSyntax;
  ULONG_PTR              nCount;
  PMIDL_SYNTAX_INFO      pSyntaxInfo;
} MIDL_SERVER_INFO, * PMIDL_SERVER_INFO;

Первым членом обеих структур является указатель на MIDL_STUB_DESC. Эта структура содержит достаточно большое количество низкоуровневой RPC-информации, но нас будет интересовать только один ее член:

typedef struct _MIDL_STUB_DESC
{
  ...
  const unsigned char* pFormatTypes;
  ...
} MIDL_STUB_DESC;

pFormatTypes содержит специальную бинарную строку, описывающую сложные параметры методов интерфейса (не примитивные типы). Более подробно содержимое этой строки мы рассмотрим позже.

В структурах MIDL_STUBLESS_PROXY_INFO и MIDL_SERVER_INFO полезную для нас информацию содержат члены ProcFormatString (ProcString) и FormatStringOffset (FmtStringOffset). Эти члены описывают сигнатуры методов интерфейса – количество и типы параметров. Для составных типов здесь будут находиться ссылки на строку описания типов pFormatTypes, которую мы встретили в структуре MIDL_STUB_DESC.

Ниже приведен пример того, как компилятор MIDL заполняет приведенные выше структуры для custom-интерфейса IBar:

CINTERFACE_PROXY_VTABLE(7) _IBarProxyVtbl = 
{
  &IBar_ProxyInfo,
  &IID_IBar,
  IUnknown_QueryInterface_Proxy,
  IUnknown_AddRef_Proxy,
  IUnknown_Release_Proxy ,
  (void*) (INT_PTR)-1 /* IBar::B */ ,
  (void*) (INT_PTR)-1 /* IBar::G */ ,
  (void*) (INT_PTR)-1 /* IBar::H */ ,
  (void*) (INT_PTR)-1 /* IBar::I */
};

const CInterfaceStubVtbl _IBarStubVtbl =
{
  &IID_IBar,
  &IBar_ServerInfo,
  7,
  0, /* pure interpreted */
  CStdStubBuffer_METHODS
};

Так как IDL комплиировался с ключом Oicf, MIDL сгенерировал полностью интерпретируемые Proxy и Stub. Поэтому адреса методов содержат -1 и pDispatchTable = 0.

Информация о Proxy и Stub для интерпретатора выглядит так:

static const MIDL_STUBLESS_PROXY_INFO IBar_ProxyInfo =
{
  &Object_StubDesc,
  __MIDL_ProcFormatString.Format,
  &IBar_FormatStringOffsetTable[-3],
  0,
  0,
  0
};

static const MIDL_SERVER_INFO IBar_ServerInfo = 
{
  &Object_StubDesc,
  0,
  __MIDL_ProcFormatString.Format,
  &IBar_FormatStringOffsetTable[-3],
  0,
  0,
  0,
  0
};

static const MIDL_STUB_DESC Object_StubDesc = 
{
   ...
  __MIDL_TypeFormatString.Format,
  ...
};

Как видим, MIDL добавил ссылки на описание формата – _MIDL_ProcFormatString и _MIDL_TypeFormatString, - и, помимо этого, сгенерировал таблицу смещений в строке сигнатур методов для каждого интерфейса:

static const unsigned short IBar_FormatStringOffsetTable[] =
{
  0,
  30,
  66,
  102
};

Конструкция IBar_FormatStringOffsetTable[-3] выглядит странно. Попробуем разобраться, что означает отрицательное смещение. Первые три метода любого COM-интерфейса унаследованы от IUnknown – QueryInterface, AddRef и Release. Поэтому нумерация собственных методов интерфейса начинается с индекса 3. Чтобы использовать порядковый номер метода интерфейса в качестве индекса в таблице форматных строк в структуре ServerInfo вместо адреса таблицы форматных строк хранится адрес с отрицательным смещением -3. При этом метод с порядковым номером 4 будет первым элементом в таблице.

Форматные строки

Для методов интерфейса MIDL генерирует описание сигнатур – количества и типов параметров в виде бинарной строки - __MIDL_ProcFormatString, для сложных типов параметров генерируется отдельная строка - _MIDL_TypeFormatString. Эти форматные строки содержат описания всех интерфейсов и их методов, которые встретились MIDL-у во время компиляции IDL. Чтобы найти описание нужного метода внутри форматной строки __MIDL_ProcFormatString, для интерфейса создается таблица смещений – если к началу строки прибавить это смещение, результирующий адрес будет указывать на описание сигнатуры метода. Для сложных типов параметров в описании метода могут задаваться смещения в форматной строке типов, по этому смещению может быть получено описание составного типа.

Форматная строка __MIDL_ProcFormatString состоит из последовательности описаний методов интерфейсов, каждое описание метода включает в себя заголовок переменной длины и описание параметров метода.

Заголовок описания метода может варьироваться в зависимости от версии компилятора MIDL и флагов компиляции (Oi, Oicf), а также версии ОС, для которой компилируется IDL. Ниже будет приведено описание заголовка для Oi (Oicf) версии и для версии ОС, начиная с Windows 2000.

typedef struct _NDR_DCOM_OI2_PROC_HEADER
{
  unsigned char         HandleType;          // Заголовок Oi
  INTERPRETER_FLAGS     OldOiFlags;          //
  unsigned short        RpcFlagsLow;         //
  unsigned short        RpcFlagsHi;          //
  unsigned short        ProcNum;             //
  unsigned short        StackSize;           //
  // expl handle descr никогда не генерируется
  unsigned short        ClientBufferSize;    // Заголовок Oi2
  unsigned short        ServerBufferSize;    //
  INTERPRETER_OPT_FLAGS Oi2Flags;            //
  unsigned char         NumberParams;        //
} NDR_DCOM_OI2_PROC_HEADER, * PNDR_DCOM_OI2_PROC_HEADER;      
 

 /* Procedure F */
                0x33,           /* FC_AUTO_HANDLE */
                0x6c,           /* Old Flags:  object, Oi2 */
/*  2 */        NdrFcLong( 0x0 ),       /* 0 */
/*  6 */        NdrFcShort( 0x3 ),      /* 3 */
/*  8 */        NdrFcShort( 0x8 ),      /* x86 Stack size/offset = 8 */
/* 10 */        NdrFcShort( 0x0 ),      /* 0 */
/* 12 */        NdrFcShort( 0x8 ),      /* 8 */
/* 14 */        0x44,           /* Oi2 Flags:  has return, has ext, */
                0x1,            /* 1 */
/* 16 */        0x8,            /* 8 */
                0x1,            /* Ext Flags:  new corr desc, */
/* 18 */        NdrFcShort( 0x0 ),      /* 0 */
/* 20 */        NdrFcShort( 0x0 ),      /* 0 */
/* 22 */        NdrFcShort( 0x0 ),      /* 0 */

В приведенном выше фрагменте кода приведено описание структуры _NDR_DCOM_OI2_HEADER из MSDN и пример сгенерированного компилятором MIDL заголовка для метода F некоторого COM интерфейса.

ПРИМЕЧАНИЕ

Здесь и далее приводятся фрагменты сгенерированного MIDL’ом кода, который помещается в файле *_p.c. Этот файл является результатом компиляции IDL-файла с описаниями интерфейсов и содержит C-код для Proxy/Stub.

HandleType описывает тип используемого RPC-хэндла, как правило, используется значение FC_AUTO_HANDLE. Флаги OldOiFlags содержат комбинацию следующих значений = Oi_OBJECT_PROC | Oi_HAS_RPCFLAGS | Oi_USE_NEW_INIT_ROUTINES | Oi_OBJ_USE_V2_INTERPRETER. Значение этого поля тоже постоянно для любых методов.

Поле ProcNum содержит порядковый номер метода в интерфейсе, минимальное занчение равно 3 (так как первые три метода любого интерфейса унаследованы от IUnknown). StackSize равен размеру стека метода с учетом указателя this и возвращаемого значения. Таким образом, для метода интерфейса минимальное значение равно 8 (или 4 если метод объявлен как void ). Поле Oi2Flags содержит флаги интерпретатора и указание на то, что в заголовке присутствует расширение extension. Поле NumberOfParams содержит количество параметров метода (с учетом возвращаемого значения, но без учета первого неявного параметра this для методов интерфейса). Вслед за этим заголовком следует расширение – extension (если во флагах Oi2Flags было указано, что расширение присутствует). Первое поле расширения (в примере его значение равно 8) содержит длину в байтах. Состав и структуру типичного расширения мы рассматривать не будем.

После заголовка метода следует описание каждого из его параметров в порядке их следования в описании метода.

Для Oicf-случая описание параметра включает в себя следующие поля:

typedef struct
{
  unsigned short  MustSize            : 1;    // 0x0001
  unsigned short  MustFree            : 1;    // 0x0002
  unsigned short  IsPipe              : 1;    // 0x0004
  unsigned short  IsIn                : 1;    // 0x0008
  unsigned short  IsOut               : 1;    // 0x0010
  unsigned short  IsReturn            : 1;    // 0x0020
  unsigned short  IsBasetype          : 1;    // 0x0040
  unsigned short  IsByValue           : 1;    // 0x0080
  unsigned short  IsSimpleRef         : 1;    // 0x0100
  unsigned short  IsDontCallFreeInst  : 1;    // 0x0200
  unsigned short  SaveForAsyncFinish  : 1;    // 0x0400
  unsigned short  Unused              : 2;
  unsigned short  ServerAllocSize     : 3;    // 0xe000
} PARAM_ATTRIBUTES, * PPARAM_ATTRIBUTES;

typedef struct
{
  PARAM_ATTRIBUTES Attributes;
  unsigned short   StackOffset;
  union
  {
    unsigned short TypeOffset;
    unsigned char  TypeFormat;
  };
} PARAM_INFO, * PPARAM_INFO;

/* Возвращаемое значение */

/* 24 */    NdrFcShort( 0x70 ),   /* Flags:  out, return, base type, */
/* 26 */    NdrFcShort( 0x4 ),    /* x86 Stack size/offset = 4 */
/* 28 */    0x8,      /* FC_LONG */
            0x0,      /* 0 */

В примере выше приведено описание возвращаемого значения некоторого COM-метода, сгенерированное компилятором MIDL. Благодаря добавленным в код комментариям мы видим, что здесь описан out-параметр, имеющий базовый тип LONG, занимающий в стеке 4 байта и являющийся возвращаемым значением. Нетрудно догадаться, что таким образом был описан HRESULT – стандартное возвращаемое значение для COM-методов.

Атрибуты параметра (Attributes) описывают его направление – in или out, является ли он простым типом и/или возвращаемым значение, передается ли он по ссылке и т.п. Поле StackOffset указывает смещение в байтах в стеке для параметра. Для простых типов следующее поле, TypeFormat, задает базовый тип, например, FC_LONG и т.п. Для составных типов TypeOffset содержит смещение в форматной строке типов, где находится полное описание типа параметра.

Предположим, что у нас есть метод I со следующей сигнатурой:

[helpstring("method I")] 
  HRESULT I([in] int i, 
            [in] IFoo* piFoo, 
            [in] long k, 
            [out] IFoo** ppiFoo, 
            [out] BSTR* ps);

Форматная строка для него будет выглядеть так (заголовок и описание возвращаемого значения опущены):

ПРИМЕЧАНИЕ

Ниже приведен фрагмент кода, сгенерированного компилятором MIDL по описанию интерфейса в IDL.

  /* Procedure I */

...
  /* Parameter i */

/* 126 */  NdrFcShort( 0x48 ),  /* Flags:  in, base type, */
/* 128 */  NdrFcShort( 0x4 ),  /* x86 Stack size/offset = 4 */
/* 130 */  0x8,    /* FC_LONG */
           0x0,    /* 0 */

  /* Parameter piFoo */

/* 132 */  NdrFcShort( 0xb ),  /* Flags:  must size, must free, in, */
/* 134 */  NdrFcShort( 0x8 ),  /* x86 Stack size/offset = 8 */
/* 136 */  NdrFcShort( 0x2 ),  /* Type Offset=2 */

  /* Parameter k */

/* 138 */  NdrFcShort( 0x48 ),  /* Flags:  in, base type, */
/* 140 */  NdrFcShort( 0xc ),  /* x86 Stack size/offset = 12 */
/* 142 */  0x8,    /* FC_LONG */
           0x0,    /* 0 */

  /* Parameter ppiFoo */

/* 144 */  NdrFcShort( 0x13 ),  /* Flags:  must size, must free, out, */
/* 146 */  NdrFcShort( 0x10 ),  /* x86 Stack size/offset = 16 */
/* 148 */  NdrFcShort( 0x14 ),  /* Type Offset=20 */

  /* Parameter ps */

/* 150 */  NdrFcShort( 0x2113 ),  /* Flags:  must size, must free, out, 
                                     simple ref, srv alloc size=8 */
/* 152 */  NdrFcShort( 0x14 ),  /* x86 Stack size/offset = 20 */
/* 154 */  NdrFcShort( 0x36 ),  /* Type Offset=54 */

...

Параметр I является простым, и для него явно задан тип - FC_LONG. Тип следующего in-параметра piFoo – указатель на интерфейс, поэтому для него задано смещение в форматной строке типов. По этому смещению находится описание интерфейса:

/*  2 */  
          0x2f,    /* FC_IP */
          0x5a,    /* FC_CONSTANT_IID */
/*  4 */  NdrFcLong( 0xb35ee853 ),  /* -1285625773 */
/*  8 */  NdrFcShort( 0xb4a ),  /* 2890 */
/* 10 */  NdrFcShort( 0x4a01 ),  /* 18945 */
/* 12 */  0xa1,    /* 161 */
          0x28,    /* 40 */
/* 14 */  0x33,    /* 51 */
          0x94,    /* 148 */
/* 16 */  0x51,    /* 81 */
          0xc3,    /* 195 */
/* 18 */  0x9,     /* 9 */
          0xb5,    /* 181 */

Парметр piFoo является указателем, для указателей форматная строка типов выглядит так:

pointer_type<1> pointer_attributes<1>
simple_type<1> FC_PAD

или

pointer_type<1> pointer_attributes<1>
offset_to_complex_description<2>

Pointer_type зависит от атрибутов в IDL – ref, full или object pointer. В примере его значение равно FC_IP (interface pointer). Атрибуты указателя описывают, является ли он простым типом (FC_SIMPLE_POINTER), находится ли цель указателя в стеке (FC_ALLOCATED_ON_STACK), нужно ли разыменовывать указатель перед тем, как получить объект (FC_POINTER_DEREF). В нашем случае атрибуты указывают FC_CONSTANT_IID. Это означает, что вслед за заголовком будет указан полный IID интерфейса.

ПРИМЕЧАНИЕ

Если в IDL использовался атрибут iid_is(), описание типа будет выглядеть иначе.

Для out-параметра IFoo** ppIFoo форматная строка типа выглядит очень просто:

/* 20 */
                0x11, 0x10,             /* FC_RP [pointer_deref] */
/* 22 */        NdrFcShort( 0xffec ),   /* Offset= -20 (2) */
ПРИМЕЧАНИЕ

В комментариях к строкам приведено смещение, то есть, когда вам встретится запись вида "Offset= -20 (2)", первое число в ней будет означать относительное смещение от текущей строки, а второе число (в скобках) – абсолютное смещение.

Тип указателя ref перед использованием нужно разыменовать. Полное описание типа находится по отрицательному смещению 20 – что как раз указывает на описание типа IFoo*, который мы рассматривали выше.

Наиболее сложное описание out-параметра BSTR* s:

/* 54 */  0xb4,    /* FC_USER_MARSHAL */
          0x83,    /* 131 */
/* 56 */  NdrFcShort( 0x0 ),  /* 0 */
/* 58 */  NdrFcShort( 0x4 ),  /* 4 */
/* 60 */  NdrFcShort( 0x0 ),  /* 0 */
/* 62 */  NdrFcShort( 0xffde ),  /* Offset= -34 (28) */

Тип маршалинга – FC_USER_MARSHAL. Далее находится описание флагов маршалинга и ссылка на передаваемый тип по отрицательному смещению 34:

/* 28 */  
          0x13, 0x0,  /* FC_OP */
/* 30 */  NdrFcShort( 0xe ),  /* Offset= 14 (44) */

Здесь мы видим описание указателя object pointer и ссылку на вложенное описание:

/* 44 */  
          0x17,    /* FC_CSTRUCT */
          0x3,    /* 3 */
/* 46 */  NdrFcShort( 0x8 ),  /* 8 */
/* 48 */  NdrFcShort( 0xfff0 ),  /* Offset= -16 (32) */
/* 50 */  0x8,    /* FC_LONG */
          0x8,    /* FC_LONG */
/* 52 */  0x5c,    /* FC_PAD */
          0x5b,    /* FC_END */

По этому смещению находится описание структуры FC_CSTRUCT. Ее размер равен 8, описание элементов массива по отрицательному смещению 16, и описание самой структуры – два члена с типом FC_LONG. Описание массива выглядит так:

/* 32 */  
          0x1b,   /* FC_CARRAY */
          0x1,    /* 1 */
/* 34 */  NdrFcShort( 0x2 ),  /* 2 */
/* 36 */  0x9,    /* Corr desc: FC_ULONG */
          0x0,    /*  */
/* 38 */  NdrFcShort( 0xfffc ),  /* -4 */
/* 40 */  NdrFcShort( 0x1 ),  /* Corr flags:  early, */
/* 42 */  0x6,    /* FC_SHORT */
          0x5b,   /* FC_END */

Здесь задан размер элемента массива, 2, и тип элемента FC_SHORT. Таким образом, строка BSTR передается как структура с двумя полями – размер и массив unsigned short с содержимым строки.

Разумеется, приведенное выше описание форматных строк не претендует на полноту – существует огромное количество различных заголовков, флагов и атрибутов, влияющих на маршалинг. По-разному описываются указатели, структуры, массивы, union-ы. На описание типа существенное влияние оказывают атрибуты IDL – size_is, string и т.п.

Задача полного разбора и анализа форматных строк является очень трудоемкой. К счастью, большую часть работы с ними мы сможем передать в NDR Engine, который прекрасно с этим справится – существует большое количество вспомогательных функций. Часть из них документирована в MSDN, а для части придется довольствоваться описаниями в заголовочных файлах. С помощью этих функций мы сможем анализировать содержимое стека вызовов, не связываясь с деталями описания параметров и их типов.

ПРИМЕЧАНИЕ

Более подробное описание форматных строк для методов и сложных типов можно найти в MSDN в разделе “RPC NDR Format Strings”.

IRpcChannelBuffer

В разделе статьи, посвященному “ручному” созданию Proxy/Stub мы встретили несколько полезных интерфейсов:

Очевидно, что между Proxy и Stub должна существовать какая-то связь – канал для передачи информации о вызове. Этот канал описывается интерфейсов IRpcChannelBuffer:

МетодОписание
HRESULT GetBuffer ( RPCOLEMESSAGE* pMessage, REFIID riid);Создать буфер для маршалинга параметров
HRESULT SendReceive ( RPCOLEMESSAGE* pMessage, ULONG* pStatus);Передать вызов Stub-у
HRESULT FreeBuffer ( RPCOLEMESSAGE* pMessage);Освободить буфер, использовавшийся для маршалинга параметров
HRESULT GetDestCtx ( DWORD* pdwDestContext, void** ppvDestContext);Получить информацию о целевом контексте
HRESULT IsConnected ();Подключен ли канал

Интерфейс IRpcProxyBuffer включает в себя метод Connect, который “подключает” Proxy к каналу:

HRESULT Connect(IRpcChannelBuffer*  pRpcChannelBuffer);

Proxy и Stub обмениваются специальными RPC-сообщениями – RPCOLEMESSAGE. Эти сообщения включают в себя служебную информацию и пакет с аргументами вызова. После вызова некоторого метода COM-интерфейса через Proxy, она вычисляет размер буфера для маршалинга параметров, получает буфер нужного размера у канала – IRpcChannelBuffer::GetBuffer, выполняет маршалинг параметров и вызывает IRpcChannelBuffer::SendReceive – передавая подготовленное RPC-сообщение Stub-у. На серверной стороне в метод Invoke интерфейса IRpcStubBuffer передается RPC-сообщение и указатель на интерфейс канала:

HRESULT Invoke(
  RPCOLEMESSAGE*  _prpcmsg,
  IRpcChannelBuffer*  _pRpcChannelBuffer
);

И, наконец, Stub “подключается” к реальному компоненту вызовом Connect:

HRESULT Connect(IUnknown* pUnkServer

Механика передачи вызова между контекстами/апартаментами/процессами/машинами и связи со Stub-ом полностью определяется реализацией канала IRpcChannelBuffer. Типичная последовательность вызовов между участвующими в процессе маршалинга компонентами показана на рисунках 1 и 2.


Рисунок 1. Вызов на стороне клиента.


Рисунок 2. Вызов на стороне сервера.

На диаграммах вызова видно, что “за кадром” остался механизм, с помощью которого на серверной стороне активируется экземпляр реального компонента, а также способ, которым RPC-сообщение было передано от клиента серверу. Эти задачи решает реализация канала. Она отвечает за активацию реального компонента на серверной стороне и передачу ему вызова тем или иным способом. Стандартный и наиболее часто встречаемый в мире COM способ передачи вызова использует Remote Procedure Call для вызовов в другой процесс/на другую машину, передачи обычных Windows-сообщений в пределах процесса между апартаментами, и в некоторых случаях прямых вызовов – между контекстами одного и того же апартамента.

Простой пример иллюстрирует “ручное” создание Proxy, Stub и канала для компонента Bar, а также передачу вызова компоненту:

struct IRpcChannelBufferImpl : 
        public CComObjectRootEx<CComMultiThreadModel>,
        public IRpcChannelBuffer
{
  BEGIN_COM_MAP(IRpcChannelBufferImpl)
    COM_INTERFACE_ENTRY(IRpcChannelBuffer)
  END_COM_MAP()

  STDMETHOD(GetBuffer)(RPCOLEMESSAGE*  pMessage, REFIID  riid )
  {
    pMessage->reserved1 = 0;
    ZeroMemory(pMessage->reserved2, sizeof(pMessage->reserved2));

    pMessage->Buffer = malloc(pMessage->cbBuffer);
    if(pMessage->Buffer == NULL)
    {
      return E_OUTOFMEMORY;
}

    return S_OK;
  }

  STDMETHOD(SendReceive)(RPCOLEMESSAGE*  pMessage, ULONG*  pStatus)
  {
    HRESULT hr = m_Stub->Invoke(pMessage, this);

    if (SUCCEEDED(hr) && pStatus)
      *pStatus = 0;

    return hr;
  }

  STDMETHOD(FreeBuffer)(RPCOLEMESSAGE* pMessage)
  {
    free(pMessage->Buffer);
    return S_OK;
  }

  STDMETHOD(GetDestCtx)(DWORD* pdwDestContext, void** ppvDestContext)
  {
    *pdwDestContext = MSHCTX_CROSSCTX;
    return S_OK;
  }

  STDMETHOD(IsConnected)()
  {
    return m_Stub ? S_OK : S_FALSE;
  }

  CComPtr<IRpcStubBuffer> m_Stub;

};

CComModule _Module;

int _tmain(int argc, _TCHAR* argv[])
{
   ::CoInitializeEx(0, COINIT_MULTITHREADED);
   _Module.Init(0, 0);
  {

    // реальный компонент Bar с интерфейсом IBar
    CComPtr<IBar> spBar;
    spBar.CoCreateInstance(CLSID_Bar);
    
    // получаем CLSID для фабрики Proxy/Stub
    CLSID clsidPs = GUID_NULL;
    CoGetPSClsid(__uuidof(IBar), &clsidPs);

    CComPtr<IPSFactoryBuffer> spFactBuffer;

    // получаем экземпляр фабрики Proxy/Stub
    CoGetClassObject(clsidPs, CLSCTX_INPROC, 0, IID_IPSFactoryBuffer, 
      reinterpret_cast<void**>(&spFactBuffer));

    // создаем стандартный Proxy
    CComPtr<IRpcProxyBuffer> spProxy;
    CComPtr<IBar> spProxyItf;
    spFactBuffer->CreateProxy(0, IID_IBar, &spProxy, (void**)&spProxyItf);

    // создаем стандартный Stub
    CComPtr<IRpcStubBuffer> spStub;
    spFactBuffer->CreateStub(IID_IBar, 0, &spStub);
    
    // "присоединяем" Stub к реальному компоненту
    spStub->Connect(spBar.p);

    CComObject<IRpcChannelBufferImpl>* pChan;
    CComObject<IRpcChannelBufferImpl>::CreateInstance(&pChan);
    spProxy->Connect(pChan);
    pChan->m_Stub = spStub;
    
    HRESULT hr = spProxyItf->B();
  }

  _Module.Term();
  ::CoUninitialize();
  return 0;
}

Перехват с помощью IRpcChannelBuffer

В приведенном выше примере перед вызовом каждого метода интерфейса через Proxy управление получает наша реализация IRpcChannelBuffer::SendReceive. Поместив в этот метод код для обработки передаваемых параметров, мы получим перехватчик, способный работать практически с любыми интерфейсами.

Как правило, среди параметров вызова наибольший интерес представляют указатели на интерфейсы. А среди указателей на интерфейсы, в свою очередь, более важными являтся out (возвращаемые клиенту) или in-out, так как перехватчик, как правило, будет заменять значения таких параметров, чтобы вместо прямого указателя на интерфейс клиент получил указатель на перехватчик.

У нас есть все необходимые составляющие для работы с параметрами – форматная строка сигнатур и типов, и набор функций Ndr*, выполняющих маршалинг отдельных параметров. Пример ниже иллюстрирует обход in- и out-параметров, и “вытаскивание” сложных типов из стека вызова:

void WalkBuffer(RPCOLEMESSAGE* pMessage, 
  CInterfaceStubVtbl* pStubVtbl, bool bIn)
{
  const MIDL_SERVER_INFO* pServInfo = pStubVtbl->header.pServerInfo;
  PMIDL_STUB_DESC pDesc = pServInfo->pStubDesc;
  
  MIDL_STUB_MESSAGE msg = {};
  NdrServerInitialize(reinterpret_cast<PRPC_MESSAGE>(pMessage), &msg, pDesc);

  unsigned short off = pServInfo->FmtStringOffset[pMessage->iMethod];
  unsigned char* procStr = const_cast<unsigned char*>(
     &pServInfo->ProcString[off]);
  
  PNDR_DCOM_OI2_PROC_HEADER pHdr =
    reinterpret_cast<PNDR_DCOM_OI2_PROC_HEADER>(procStr);
  procStr += sizeof(NDR_DCOM_OI2_PROC_HEADER);
  procStr += *procStr;

  PPARAM_INFO pParams = reinterpret_cast<PPARAM_INFO>(procStr);
  for (int i = 0; i < pHdr->NumberParams; ++i)
  {
    if (bIn && pParams[i].Attributes.IsIn
      || !bIn && pParams[i].Attributes.IsOut)
    {
      if (pParams[i].Attributes.IsBasetype)
      {
        msg.Buffer += NdrGetSimpleTypeBufferSize(pParams[i].TypeFormat);
      }
      else
      {
        unsigned char* p = 0;
        const unsigned char* pFmt = pDesc->pFormatTypes +
          pParams[i].TypeOffset;
        NdrTypeUnmarshall(&msg, &p, pFmt, 1);
        NdrTypeFree(&msg, p, pFmt);
      }

    }
  }

}

STDMETHOD(SendReceive)(RPCOLEMESSAGE* pMessage, ULONG* pStatus)
{
  HRESULT hr = S_OK;
  CInterfaceStubVtbl* pStubVtbl = CONTAINING_RECORD(*(PCHAR*)m_Stub.p,
    InterfaceStubVtbl, CInterfaceStubVtbl::Vtbl);
  WalkBuffer(pMessage, pStubVtbl, true);
  hr = m_Stub->Invoke(pMessage, this);
  WalkBuffer(pMessage, pStubVtbl, false);
  
  if (hr == S_OK)
    *pStatus = 0;

  return hr;
}

Для обхода параметров нам пришлось выполнять демаршалинг сложных типов, чтобы узнать занимаемый ими в буфере размер (выделенный фрагмент кода).

ПРИМЕЧАНИЕ

Описание использованных структур можно найти в MSDN, а также заголовочных файлах <rpcproxy.h> и <rpcndr.h>.

Приведенный выше подход вполне работоспособен, и его можно развивать дальше (например, анализируя тип параметра и заменяя указатели на интерфейс перехватчиками). Но все же у него есть один очень существенный недостаток. Даже в самых простых случаях, когда перехватчик не содержит никакой дополнительной логики, передача вызова от клиента к компоненту потребует маршалинга параметров (из-за вызова через стандартный Proxy). Более того, если перехватчику потребуется анализировать переданные параметры, для сложных типов функции маршалинга будут вызваны еще один раз (NdrTypeUnmarshal).

В следующем разделе статьи мы попробуем избавиться от избыточного маршалинга (оставив его только в тех случаях, когда он действительно необходим) и реализовать перехватчик, использующий традиционные интерфейсы ICallInterceptor и ICallFrame.

Реализация перехватчика на основе NDR RPC Format Strings

Основная проблема, которую необходимо решить при реализации универсального перехватчика методов COM-интерфейсов – получение информации о сигнатурах методов, количестве и типах параметров, их местонахождении в стеке вызова, а также замена указателей на интерфейсы в стеке вызова указателями на перехватчики.

В предыдущем разделе статьи мы увидели, что эта информация может быть получена во время выполнения приложения.

В нашей реализации перехватчиков прежде всего понадобится модуль для загрузки этой информации по IID интерфейса. Здесь есть одна тонкость – для automation-интерфейсов вызовы IPsFactoryBuffer::CreateProxy и IPsFactoryBuffer::CreateStub вернут вполне пригодные для вызовов Proxy и Stub, но с их помощью нельзя будет получить информацию об интерфейсе – proc format string и type format string. Поэтому для таких интерфейсов Proxy и Stub будут создаваться с помощью вызовов недокументированных функций CreateProxyFromTypeInfo и CreateStubFromTypeInfo. Ниже приведена реализация класса PsManager, в задачи которого входит загрузка нужных Proxy/Stub по IID интерфейса:

namespace Intercept
{

typedef
  void
  (__RPC_STUB* PRPC_STUB_FUNCTION) (
    IRpcStubBuffer*    This,
    IRpcChannelBuffer* _pRpcChannelBuffer,
    PRPC_MESSAGE       _pRpcMessage,
    DWORD*             pdwStubPhase);

typedef struct tagCInterfaceStubHeader
{
  const IID*                piid;
  const MIDL_SERVER_INFO*   pServerInfo;
  unsigned long             DispatchTableCount;
  const PRPC_STUB_FUNCTION* pDispatchTable;
} CInterfaceStubHeader;

typedef struct tagCInterfaceStubVtbl
{
  CInterfaceStubHeader header;
  void*                Vtbl;
} CInterfaceStubVtbl;


typedef HRESULT (__stdcall * pCreateStubFromTypeInfo)(
  ITypeInfo*       pInfo, 
  REFIID           riid,
  IUnknown*        pUnkServer, 
  IRpcStubBuffer** ppStub);

struct PSManager
{
public:
  HRESULT Initialize(const IID& riid)
  {
    CComPtr<ITypeInfo> spTypeInfo;

    HRESULT hr = LoadTypeInfo(riid, spTypeInfo);

    if (SUCCEEDED(hr))
      hr = CreateTIStub(spTypeInfo, riid, m_spStub);
    
    if (FAILED(hr))
      hr = CreateStub(riid, m_spStub);

    return hr;
  }

  const IID* GetRiid()
  {
    CInterfaceStubVtbl* pStubVtbl = CONTAINING_RECORD(*(PCHAR*)m_spStub.p,
      CInterfaceStubVtbl, CInterfaceStubVtbl::Vtbl);

    return pStubVtbl->header.piid;
  }

  const MIDL_SERVER_INFO* GetStubInfo()
  {
    CInterfaceStubVtbl* pStubVtbl = CONTAINING_RECORD(*(PCHAR*)m_spStub.p,
      CInterfaceStubVtbl, CInterfaceStubVtbl::Vtbl);

    return pStubVtbl->header.pServerInfo;
  }
  unsigned long GetMethodCount()
  {
    CInterfaceStubVtbl* pStubVtbl = CONTAINING_RECORD(*(PCHAR*)m_spStub.p,
      CInterfaceStubVtbl, CInterfaceStubVtbl::Vtbl);

    return pStubVtbl->header.DispatchTableCount;
  }

private:
  HRESULT CreateStub(REFIID riid, CComPtr<IRpcStubBuffer>& spStub)
  {
    CLSID clsid = CLSID_NULL;
    HRESULT hr = CoGetPSClsid(riid, &clsid);

    if (SUCCEEDED(hr))
    {

      CComPtr<IPSFactoryBuffer> spBuffer;
      hr = CoGetClassObject(clsid, CLSCTX_INPROC, 0, IID_IPSFactoryBuffer,
        reinterpret_cast<void**>(&spBuffer));

      if (SUCCEEDED(hr))
      {
        hr = spBuffer->CreateStub(riid, 0, &spStub);
      }
    }
    return hr;
  }

  HRESULT CreateTIStub(CComPtr<ITypeInfo> spTypeInfo, REFIID riid,
    CComPtr<IRpcStubBuffer>& spStub)
  {
    HMODULE hRpcMod = LoadLibrary(_T("rpcrt4.dll"));
    pCreateStubFromTypeInfo pCreateStub = 
      reinterpret_cast<pCreateStubFromTypeInfo>
      (GetProcAddress(hRpcMod, "CreateStubFromTypeInfo"));
    
    if (pCreateStub == 0)
    {
      return E_FAIL;
    }
    HRESULT hr = pCreateStub(spTypeInfo, riid, 0, &spStub);
    
    FreeLibrary(hRpcMod);

    return hr;
  }

  HRESULT LoadTypeInfo(const IID& riid, CComPtr<ITypeInfo>& spTypeInfo)
  {
    HRESULT hr = E_NOINTERFACE;
    CComBSTR bstrPath = L"Interface\\";
    bstrPath.Append(riid);
    bstrPath.Append(L"\\TypeLib");

    CRegKey rk;

    if (rk.Open(HKEY_CLASSES_ROOT, CW2T(bstrPath)) == ERROR_SUCCESS)
    {
      DWORD dwRet = 0;
      ULONG cbSize = 0;

      rk.QueryStringValue(0, 0, &cbSize);

      std::vector<TCHAR> buffer(cbSize);

      dwRet = rk.QueryStringValue(0, &buffer[0], &cbSize);

      if (dwRet != ERROR_SUCCESS)
        return HRESULT_FROM_WIN32(dwRet);

      GUID tlbId = GUID_NULL;
      CLSIDFromString(CT2W(&buffer[0]), &tlbId);

      rk.QueryStringValue(_T("Version"), 0, &cbSize);
      buffer.resize(cbSize);

      dwRet = rk.QueryStringValue(_T("Version"), &buffer[0], &cbSize);
      if (dwRet != ERROR_SUCCESS)
      {
        return HRESULT_FROM_WIN32(dwRet);
      }
      
      DWORD dwMajor = 0;
      DWORD dwMinor = 0;
      _stscanf(&buffer[0], _T("%1d.%1d"), &dwMajor, &dwMinor);

      CComPtr<ITypeLib> spTlb;
      hr = LoadRegTypeLib(tlbId, static_cast<WORD>(dwMajor), static_cast<WORD>(dwMinor), 0, &spTlb);

      if (SUCCEEDED(hr))
      {
        hr = spTlb->GetTypeInfoOfGuid(riid, &spTypeInfo);
      }
    }
    return hr;
  }

  CComPtr<IRpcStubBuffer> m_spStub;
};

Для заданного IID интерфейса сначала выполняется попытка загрузки TypeInfo из библиотеки типов и создания Stub на его основе. Если попытка была неудачной, предпринимается попытка создания Custom Stub способом, который был рассмотрен выше.

ПРИМЕЧАНИЕ

Производительность приведенного выше кода может быть существенно улучшена за счет кэширования экземпляров Stub-ов для интерфейсов так, чтобы исключить необходимость обращения к реестру и загрузки библиотеки типов каждый раз, когда клиент хочет получить информацию по заданному IID.

Клиент класса PsManager вызывает PsManager::Initiliaze и передает нужный IID интерфейса. Если Stub был успешно создан – можно получать информацию об интерфейсе вызовом GetStubInfo() и GetInterfaceMethodCount().

NDR Engine

Следующий элемент инфраструктуры перехватчиков – модуль для работы с форматными строками. В его задачи входит маршалинг/демаршалинг параметров вызова, вычисление размера стека вызова, обход параметров в стеке вызова, разбор информации о методе интерфейса и его параметрах.

Для корректной работы маршалинга реализованы еще два метода – InitOutParameters и FreeParams. Первый предназначен для того, чтобы проинициализировать значения out-параметров. На стороне клиента вызова инициализация заключается в обнулении областей памяти, на которые ссылаются out-параметры, на серверной стороне помимо обнуления необходимо еще и выделить нужные области памяти. Эта задача решается с помощью функции из NDR API – NdrOutInit. По описанию типа эта функция выделяет буфер нужного размера и связывает его с нужным параметром в стеке вызова. Размер памяти, занимаемой out-параметром, можно узнать с помощью другой функции – NdrpMemoryIncrement.

Освобождение памяти, выделенной для out-параметров и непосредственно значений in- и out-параметров на серверной стороне вызовов, выполняет функция FreeParams с помощью вспомогательных функций NrdOleFree и NdrTypeFree. Небольшая тонкость, связанная с освобождением параметров, заключается в том, что для некоторых типов на серверной стороне память не выделяется – вместо этого NDR Engine помещает ссылки на буфер, содержащий данные для маршалинга. Например, если один из параметров вызова – массив простых типов, на серверной стороне во время демаршалинга память для массива выделяться не будет, вместо этого значение соответствующего параметра будет указывать непосредственно в буфер с данными маршалинга.

И, наконец, для маршалинга/демаршалинга и вычисления размера буфера маршалинга используются функции NdrTypeSize, NdrTypeMarshal и NdrTypeUnmarshal, которые в зависимости от типа параметра выполняют нужные действия.

Реализация класса NdrEngine приведена ниже:

#pragma once
#include <rpcndr.h>
#include <callobj.h>
#include "PsManager.h"

namespace Intercept
{

#define FC_IP 0x2F
#define FC_RP 0x11
#define FC_UP 0x12
#define FC_OP 0x13

#define FC_CONSTANT_IID 0x5a

#define FC_SIMPLE_POINTER 0x8
#define FC_POINTER_DEREF 0x10

typedef void
  (__RPC_USER* GENERIC_MARSHAL_ROUTINE)(
    MIDL_STUB_MESSAGE* pMsg,
    void*              pMemory,
    PFORMAT_STRING     pFormat);

typedef void
  (__RPC_USER * GENERIC_UNMARSHAL_ROUTINE)(
    MIDL_STUB_MESSAGE* pMsg,
    void**             ppMemory,
    PFORMAT_STRING     pFormat,
    unsigned char      fSkipRefCheck);

extern "C"
{
void RPC_ENTRY NdrTypeSize(
  MIDL_STUB_MESSAGE* pMsg, void* pMemory, PFORMAT_STRING pFormat);
void RPC_ENTRY NdrTypeMarshall(
  MIDL_STUB_MESSAGE* pMsg, void* pMemory, PFORMAT_STRING pFormat);
void RPC_ENTRY NdrTypeFree(
  MIDL_STUB_MESSAGE* pMsg, void* pMemory, PFORMAT_STRING pFormat);
void RPC_ENTRY NdrTypeUnmarshall(
  MIDL_STUB_MESSAGE* pMsg, void* pMemory, PFORMAT_STRING pFormat,
  unsigned char fSkipRefCheck);  

void RPC_ENTRY NdrOutInit(
  MIDL_STUB_MESSAGE* pMsg, PFORMAT_STRING pFormat, void** pMemory);
void* RPC_ENTRY NdrpMemoryIncrement(
  MIDL_STUB_MESSAGE* pMsg, void* pMemory, PFORMAT_STRING pFormat);
}

typedef unsigned char INTERPRETER_FLAGS;
typedef unsigned char INTERPRETER_OPT_FLAGS;

#define OPT_FLAG_HAS_EXT 0x40

typedef struct _NDR_DCOM_OI2_PROC_HEADER
{
  unsigned char         HandleType;  // Заголовок Oi 
  INTERPRETER_FLAGS     OldOiFlags;  //
  unsigned short        RpcFlagsLow; //
  unsigned short        RpcFlagsHi;  //
  unsigned short        ProcNum;     //
  unsigned short        StackSize;   //
  // expl handle descr никогда не генерируется //
  unsigned short        ClientBufferSize; // Заголовок Oi2
  unsigned short        ServerBufferSize; //
  INTERPRETER_OPT_FLAGS Oi2Flags;         //
  unsigned char         NumberParams;     //
} 
NDR_DCOM_OI2_PROC_HEADER, * PNDR_DCOM_OI2_PROC_HEADER;

typedef struct
{
  unsigned char   HasNewCorrDesc  : 1;  // 0x01
  unsigned char   ClientCorrCheck : 1;  // 0x02
  unsigned char   ServerCorrCheck : 1;  // 0x04
  unsigned char   HasNotify       : 1;  // 0x08
  unsigned char   HasNotify2      : 1;  // 0x10
  unsigned char   Unused          : 3;
} INTERPRETER_OPT_FLAGS2, * PINTERPRETER_OPT_FLAGS2;

typedef struct
{
  unsigned short  MustSize           : 1;  // 0x0001
  unsigned short  MustFree           : 1;  // 0x0002
  unsigned short  IsPipe             : 1;  // 0x0004
  unsigned short  IsIn               : 1;  // 0x0008
  unsigned short  IsOut              : 1;  // 0x0010
  unsigned short  IsReturn           : 1;  // 0x0020
  unsigned short  IsBasetype         : 1;  // 0x0040
  unsigned short  IsByValue          : 1;  // 0x0080
  unsigned short  IsSimpleRef        : 1;  // 0x0100
  unsigned short  IsDontCallFreeInst : 1;  // 0x0200
  unsigned short  SaveForAsyncFinish : 1;  // 0x0400
  unsigned short  Unused             : 2;
  unsigned short  ServerAllocSize    : 3;  // 0xe000
} PARAM_ATTRIBUTES, * PPARAM_ATTRIBUTES;

typedef struct
{
  PARAM_ATTRIBUTES Attributes;
  unsigned short   StackOffset;
  union
  {
    unsigned short TypeOffset;
    unsigned char  TypeFormat;
  };
} PARAM_INFO, * PPARAM_INFO;

class NdrEngine
{
public:

  HRESULT Initialize(const IID& riid)
  {
    return m_psMgr.Initialize(riid);
  }

  unsigned short GetMethodStackSize(int nMethod)
  {
    return GetMethodHdr(nMethod)->StackSize;
  }

  unsigned short GetNumberOfMethods()
  {
    return static_cast<unsigned short>(m_psMgr.GetMethodCount());
  }

  const IID* GetIID()
  {
    return m_psMgr.GetRiid();
  }

  HRESULT GetInterfaceInfo(int nMethod, CALLFRAMEINFO* pInfo)
  {
    HRESULT hr = S_OK;
    
    ZeroMemory(pInfo, sizeof(CALLFRAMEINFO));
    pInfo->iMethod = nMethod;

    const PARAM_INFO* pParams = GetParams(nMethod);
    const NDR_DCOM_OI2_PROC_HEADER* pHdr = GetMethodHdr(nMethod);

    for (ULONG i = 0; i < pHdr->NumberParams; ++i)
    {
      if (pParams[i].Attributes.IsIn)
      {
        if (pParams[i].Attributes.IsOut)
          pInfo->fHasInOutValues = TRUE;
        else
          pInfo->fHasInValues = TRUE;
      }
      else if (pParams[i].Attributes.IsOut)
        pInfo->fHasOutValues = TRUE;

      PFORMAT_STRING pTypeFmt = GetTypeFormat(nMethod, i);

      if (*pTypeFmt == FC_IP)
        ++pInfo->cTopLevelInInterfaces;
    }

    pInfo->cInInterfacesMax = LONG_MAX;
    pInfo->cInOutInterfacesMax = LONG_MAX;
    pInfo->cOutInterfacesMax = LONG_MAX;
    
    pInfo->iid = *GetIID();
    pInfo->cMethod = nMethod;
    pInfo->cParams = GetMethodHdr(nMethod)->NumberParams;

    return S_OK;
  }

  HRESULT WalkInterfacePtrs(
    void* pStack, int nMethod, CALLFRAME_WALK walkWhat, 
    ICallFrameWalker* pWalker)
  {
    HRESULT hr = S_OK;
    const PARAM_INFO* pParams = GetParams(nMethod);
    const NDR_DCOM_OI2_PROC_HEADER* pHdr = GetMethodHdr(nMethod);

    for (ULONG i = 0; i < pHdr->NumberParams; ++i)
    {
      BOOL bWalk = FALSE;

      if (pParams[i].Attributes.IsIn)
      {
        if (pParams[i].Attributes.IsOut)
          bWalk = walkWhat & CALLFRAME_WALK_INOUT;
        else
          bWalk = walkWhat & CALLFRAME_WALK_IN;
      }
      else if (pParams[i].Attributes.IsOut)
        bWalk = walkWhat & CALLFRAME_WALK_OUT;

      if (bWalk)
      {
        PFORMAT_STRING pTypeFmt = GetTypeFormat(nMethod, i);
        void** pArgLocation = 
          reinterpret_cast<void**>((PBYTE)pStack + pParams[i].StackOffset);

        void** ppvItf = NULL;
        IID riid = CLSID_NULL;

        if (*pTypeFmt == FC_IP)
        {
          ppvItf = pArgLocation;

          if (pTypeFmt[1] == FC_CONSTANT_IID)
            riid = *reinterpret_cast<const GUID*>(&pTypeFmt[2]);
        }
        else if (*pTypeFmt == FC_OP || *pTypeFmt == FC_UP
          || *pTypeFmt == FC_RP)
        {
          signed short off = 
            *reinterpret_cast<const signed short*>(&pTypeFmt[2]);
          
          PFORMAT_STRING pPtrType = &pTypeFmt[2] + off;

          if (*pPtrType == FC_IP)
          {
            if (pPtrType[1] == FC_CONSTANT_IID)
              riid = *reinterpret_cast<const GUID*>(&pPtrType[2]);

            ppvItf = reinterpret_cast<void**>(*pArgLocation);
          }
        }
        
        if (ppvItf)
        {
          hr = pWalker->OnWalkInterface(riid, 
            ppvItf, pParams[i].Attributes.IsIn, pParams[i].Attributes.IsOut);

          if (FAILED(hr))
            break;
        }
      }
    }

    return hr;
  }

  HRESULT GetParameterInfo(int nMethod, ULONG iparam, 
    CALLFRAMEPARAMINFO *pInfo)
  {
    const PARAM_INFO& paramInfo = GetParams(nMethod)[iparam];

    pInfo->fIn = (paramInfo.Attributes.IsIn);
    pInfo->fOut = (paramInfo.Attributes.IsOut);
    pInfo->stackOffset = paramInfo.StackOffset;

    const NDR_DCOM_OI2_PROC_HEADER* pHdr = GetMethodHdr(nMethod);

    if (iparam >= pHdr->NumberParams)
      return E_INVALIDARG;

    if (iparam == pHdr->NumberParams - 1)
      pInfo->cbParam = pHdr->StackSize - paramInfo.StackOffset;
    else
      pInfo->cbParam = GetParams(nMethod)[iparam + 1].StackOffset 
        - paramInfo.StackOffset;

    return S_OK;
  }

  HRESULT InitMessage(
    RPC_MESSAGE* pRpcMsg, MIDL_STUB_MESSAGE* pMsg, BOOL fClient, void* pStack,
    int nMethod, void* pBuffer, ULONG cbBuffer, DWORD dwDestCtx)
  {
    
    pRpcMsg->ProcNum = nMethod;
    pRpcMsg->Buffer = pBuffer;
    pRpcMsg->BufferLength = cbBuffer;
    pRpcMsg->ProcNum = nMethod;

    if (fClient == TRUE)
      NdrClientInitialize(pRpcMsg, pMsg, GetServerInfo()->pStubDesc, nMethod);
    else
      NdrServerInitialize(pRpcMsg, pMsg, GetServerInfo()->pStubDesc);

    pMsg->dwDestContext = dwDestCtx;

    pMsg->Buffer = (unsigned char*)pBuffer;
    pMsg->BufferLength = cbBuffer;

    pMsg->BufferStart = pMsg->Buffer;
    pMsg->BufferEnd   = pMsg->Buffer + cbBuffer;

    pMsg->StackTop = reinterpret_cast<unsigned char*>(pStack);

    const PARAM_INFO* pParams = GetParams(nMethod);
    const NDR_DCOM_OI2_PROC_HEADER* pHdr = GetMethodHdr(nMethod);

    if (pHdr->Oi2Flags & OPT_FLAG_HAS_EXT)
    {
      pMsg->fHasExtensions = 1;
      pMsg->fHasNewCorrDesc = GetExtOptFlags(nMethod)->HasNewCorrDesc;
    }

    return S_OK;
  }

  HRESULT InitOutParameters(BOOL fClient, void* pStack, int nMethod)
  {
    HRESULT hr = S_OK;

    RpcTryExcept
    {

      RPC_MESSAGE rpcMsg = {};
      MIDL_STUB_MESSAGE msg = {};

      InitMessage(&rpcMsg, &msg, fClient, pStack, nMethod, 0, 0, 0);

      const PARAM_INFO* pParams = GetParams(nMethod);
      const NDR_DCOM_OI2_PROC_HEADER* pHdr = GetMethodHdr(nMethod);

      for (ULONG i = 0; i < pHdr->NumberParams; ++i)
      {
        void** pArgLocation = 
          reinterpret_cast<void**>((PBYTE)pStack + pParams[i].StackOffset);

        PFORMAT_STRING pTypeFmt = GetTypeFormat(nMethod, i);

        if (pParams[i].Attributes.IsOut && (!pParams[i].Attributes.IsReturn))
        {
          if (fClient)
          {
            if (pParams[i].Attributes.IsSimpleRef)
            {
              void* pNextArg = NdrpMemoryIncrement(&msg, *pArgLocation, 
                pTypeFmt);
              ZeroMemory(*pArgLocation, 
                (LPBYTE)pNextArg - (LPBYTE)*pArgLocation);
            }
            else
              ZeroMemory(*pArgLocation, sizeof(void*));
          }
          else
             NdrOutInit(&msg, pTypeFmt, pArgLocation);
        }
      }
    }
    RpcExcept(EXCEPTION_EXECUTE_HANDLER)
    {
      hr = HRESULT_FROM_WIN32(GetExceptionCode());
    }
    RpcEndExcept

    return hr;
  }

  HRESULT FreeParams(void* pStack, int nMethod, void* pBuffer, ULONG cbBuffer)
  {
    HRESULT hr = S_OK;

    RpcTryExcept
    {
      RPC_MESSAGE rpcMsg = {};
      MIDL_STUB_MESSAGE msg = {};

      InitMessage(&rpcMsg, &msg, FALSE, pStack, nMethod, 0, 0, 0);

      const PARAM_INFO* pParams = GetParams(nMethod);
      const NDR_DCOM_OI2_PROC_HEADER* pHdr = GetMethodHdr(nMethod);

      for (ULONG i = 0; i < pHdr->NumberParams; ++i)
      {
        void** pArgLocation = reinterpret_cast<void**>(
          (PBYTE)pStack + pParams[i].StackOffset);

        PFORMAT_STRING pTypeFmt = GetTypeFormat(nMethod, i);

        if (pParams[i].Attributes.MustFree)
        {
          NdrTypeFree (&msg, *pArgLocation, pTypeFmt);

          if (pParams[i].Attributes.IsSimpleRef)
          {
            if (*pArgLocation < pBuffer 
              || *pArgLocation >= (PBYTE*)pBuffer + cbBuffer)
            {
              NdrOleFree(*pArgLocation);
            }
          }
        }
      }
    }
    RpcExcept(EXCEPTION_EXECUTE_HANDLER)
    {
      hr = HRESULT_FROM_WIN32(GetExceptionCode());
    }
    RpcEndExcept

    return hr;
  }

  HRESULT ReleaseMarshalBuffer(BOOL fClient, void* pBuffer, 
    ULONG cbBuffer, int nMethod)
  {
    return E_NOTIMPL;
  }

  HRESULT GenericMarshall(BOOL fClient, void* pStack, int nMethod, 
    CALLFRAME_MARSHALCONTEXT *pcontext, void* pBuffer, ULONG cbBuffer, 
    void* pRoutine, bool bMarshall, void* pRetVal = 0, 
    ULONG* pcbBufferLen = 0, ULONG* pcbBufferUsed = 0)
  {
    HRESULT hr = S_OK;

    RpcTryExcept
    {
      RPC_MESSAGE rpcMsg = {};
      MIDL_STUB_MESSAGE msg = {};

      InitMessage(&rpcMsg, &msg, fClient, pStack, nMethod, pBuffer, cbBuffer,
        pcontext->dwDestContext);

      const PARAM_INFO* pParams = GetParams(nMethod);
      const NDR_DCOM_OI2_PROC_HEADER* pHdr = GetMethodHdr(nMethod);

      for (ULONG i = 0; i < pHdr->NumberParams; ++i)
      {
        void** pArgLocation = 
          reinterpret_cast<void**>((PBYTE)pStack + pParams[i].StackOffset);
        
        PFORMAT_STRING pTypeFmt = GetTypeFormat(nMethod, i);

        if (pcontext->fIn && pParams[i].Attributes.IsIn
          || !pcontext->fIn && pParams[i].Attributes.IsOut)
        {
            if (bMarshall)
            {
              if (pParams[i].Attributes.IsReturn && pRetVal)
                pArgLocation = reinterpret_cast<void**>(pRetVal);

              reinterpret_cast<GENERIC_MARSHAL_ROUTINE>(pRoutine)(&msg, 
                pParams[i].Attributes.IsBasetype 
                ? pArgLocation : *pArgLocation, pTypeFmt);
            }
            else
            {
              if (pParams[i].Attributes.IsReturn && pRetVal)
                pArgLocation = reinterpret_cast<void**>(pRetVal);

              reinterpret_cast<GENERIC_UNMARSHAL_ROUTINE>(pRoutine)(&msg, 
                pParams[i].Attributes.IsBasetype 
                ? (void**)&pArgLocation : pArgLocation, pTypeFmt, 0);
            }
        }
      }

      if (pcbBufferLen)
        *pcbBufferLen = msg.BufferLength;

      if (pcbBufferUsed)
        *pcbBufferUsed = static_cast<ULONG>(msg.Buffer - msg.BufferStart);
    }
    RpcExcept(EXCEPTION_EXECUTE_HANDLER)
    {
      hr = HRESULT_FROM_WIN32(GetExceptionCode());
    }
    RpcEndExcept

    return hr;
  }


private:
  const MIDL_SERVER_INFO* GetServerInfo()
  {
    return m_psMgr.GetStubInfo();
  }

  const NDR_DCOM_OI2_PROC_HEADER* GetMethodHdr(int nMethod)
  {
    unsigned short off = GetServerInfo()->FmtStringOffset[nMethod];
    PFORMAT_STRING pMethodStr = &GetServerInfo()->ProcString[off];

    const NDR_DCOM_OI2_PROC_HEADER* pHdr = 
      reinterpret_cast<const NDR_DCOM_OI2_PROC_HEADER*>(pMethodStr);

    return pHdr;
  }

  const INTERPRETER_OPT_FLAGS2* GetExtOptFlags(int nMethod)
  {
    unsigned short off = GetServerInfo()->FmtStringOffset[nMethod];
    PFORMAT_STRING pMethodStr = &GetServerInfo()->ProcString[off];

    if (reinterpret_cast<const NDR_DCOM_OI2_PROC_HEADER*>(
      pMethodStr)->Oi2Flags & OPT_FLAG_HAS_EXT)
    {
      pMethodStr += sizeof(NDR_DCOM_OI2_PROC_HEADER);
      pMethodStr += sizeof(byte);
      return reinterpret_cast<const INTERPRETER_OPT_FLAGS2*>(pMethodStr);
    }

    return NULL;
  }

  const PARAM_INFO* GetParams(int nMethod)
  {
    unsigned short off = GetServerInfo()->FmtStringOffset[nMethod];
    PFORMAT_STRING pMethodStr = &GetServerInfo()->ProcString[off];
    int iHasExt = reinterpret_cast<const NDR_DCOM_OI2_PROC_HEADER*>(
      pMethodStr)->Oi2Flags & OPT_FLAG_HAS_EXT;

    pMethodStr += sizeof(NDR_DCOM_OI2_PROC_HEADER);
    
    if (iHasExt)
      pMethodStr += *pMethodStr;

    return reinterpret_cast<const PARAM_INFO*>(pMethodStr);
  }

  PFORMAT_STRING GetTypeFormat(int nMethod, ULONG iparam)
  {
    const PARAM_INFO& paramInfo = GetParams(nMethod)[iparam];
    if (paramInfo.Attributes.IsBasetype)
      return &paramInfo.TypeFormat;

    return &GetServerInfo()->pStubDesc->pFormatTypes[paramInfo.TypeOffset];
  }

private:
  PSManager m_psMgr;
};
}

Универсальный перехватчик – ItfThunk

Еще одна важная составляющая часть инфраструктуры перехватчиков – универсальный перехватчик. В его задачи входит создание vtbl нужной структуры и общая реализация всех перехватываемых методов, делегирующая вызов нужному обработчику. Принципы реализации подобного перехватчика рассматривались в первой части статьи, “Перехват методов COM интерфейсов”, здесь же мы кратко рассмотрим только основные элементы:

namespace Intercept
{
#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)

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];
};

template<class T>
class ItfThunk
{
public:
  ItfThunk() : m_pOuter(0)
  {
    vptr = &vtbl;
  }

  HRESULT Initialize(T* pOuter, const IID& riid)
  {
    HRESULT hr = m_engine.Initialize(riid);

    if (SUCCEEDED(hr))
      m_pOuter = pOuter;

    return hr;
  }

  HRESULT CreateCallFrame(int nMethod, IUnknown* pExtRef, void* pBuffer,
    ULONG cbBuffer, BOOL bCopyBuffer,CALLFRAME_MARSHALCONTEXT* pCtx,
    ULONG* pcbUnmarshalled, ICallFrame** ppFrame)
  {
    if (nMethod < 3)
      return E_INVALIDARG;

    DWORD dwStackSz = m_engine.GetMethodStackSize(nMethod);
    void* pStack = malloc(dwStackSz);

    ZeroMemory(pStack, dwStackSz);

    CComObject<ICallFrameImpl>* pFrame = 0;
    HRESULT hr = CComObject<ICallFrameImpl>::CreateInstance(&pFrame);
CComPtr<ICallFrame> spFrame(pFrame);

    if (SUCCEEDED(hr))
    {
      hr = pFrame->InitializeServer(pStack, dwStackSz, nMethod, GetEngine(),
        pExtRef, pBuffer, cbBuffer, bCopyBuffer, pCtx, pcbUnmarshalled);
      
      if (SUCCEEDED(hr))
        *ppFrame = spFrame.Detach(); 
    }

    if (FAILED(hr))
      free(pStack);

    return hr;
  }

  NdrEngine* GetEngine()
  {
    return &m_engine;
  }

  struct QIArgs
  {
    REFIID riid;
    void** ppv;
  };

  HRESULT __stdcall Invoke(void* pStack, DWORD* dwStackSz, int nMethod)
  {
    HRESULT hr = S_OK;

    if (nMethod >= 3)
    {
      *dwStackSz = m_engine.GetMethodStackSize(nMethod) - sizeof(HRESULT);
      
      CComObjectStack<ICallFrameImpl> callFrame;

      callFrame.InitializeClient(pStack, *dwStackSz, nMethod, &m_engine);
      hr = m_pOuter->OnCall(&callFrame);
    }
    else
    {
      QIArgs* pArgs = reinterpret_cast<QIArgs*>((PBYTE)pStack + sizeof(this));

      switch (nMethod)
      {
      case 0:
        *dwStackSz = sizeof(const IID&) + sizeof(void*);
        hr = m_pOuter->GetUnknown()->QueryInterface(pArgs->riid, pArgs->ppv);
        break;
      case 1:
        *dwStackSz = sizeof(this);
        hr = m_pOuter->GetUnknown()->AddRef();
        break;
      case 2:
        *dwStackSz = sizeof(this);
        hr = m_pOuter->GetUnknown()->Release();
        break;
      }
    }

    return hr;
  }

private:
  static void thunk();
  ThunkVtbl* vptr;
  T* m_pOuter;
  NdrEngine m_engine;
  static ThunkVtbl vtbl;
};

template<class T> __declspec(selectany) ThunkVtbl
ItfThunk<T>::vtbl(reinterpret_cast<DWORD_PTR>(ItfThunk::thunk));

template<class T>
__declspec(naked) void ItfThunk<T>::thunk()
{
  __asm
  {
    push [esp]
    lea eax, [esp + 4]
    push eax
    lea eax, [esp + 10h]
    push eax
    mov eax, [esp + 14h]
    push eax
    call ItfThunk::Invoke
    mov edx, [esp + 4]
    add esp, [esp]
    add esp, 8
    jmp edx
  }
}
}

В реализации метода ItfThunk::Invoke (куда переходит выполнение после вызова любого перехватываемого метода) особым образом обрабатываются вызовы первых трех методов: QueryInterface, AddRef и Release. Они делегируются реализации IUnknown внешнего компонента. Для всех остальных методов вызывается метод OnCall, куда передается сконструированный экземпляр ICallFrame, содержащий всю необходимую информацию о стеке вызова.

ICallInterceptor и ICallFrame

И, наконец, рассмотрим два главных элемента инфраструктуры перехватчиков, с которыми непосредственно взаимодействует клиент – реализации ICallInterceptor и ICallFrame.

Реализация ICallInterceptor тривиальна. Это обычный реализованный на ATL COM-класс, который делегирует все вызовы вспомогательным классам ItfThunk и NdrEngine. Единственная особенность этого класса заключается в том, что он не объявляет обычную карту интерфейсов ATL, так как поддерживаемый им IID интерфейса неизвестен на этапе компиляции. Вместо этого он сам реализует метод InternalQueryInterface:

class ICallInterceptorImpl : public CComObjectRootEx<CComMultiThreadModel>,
  public ICallInterceptor,
  public ICallUnmarshal
{

public:
  _ATL_DECLARE_GET_UNKNOWN(ICallInterceptorImpl)
  DECLARE_GET_CONTROLLING_UNKNOWN()

  DECLARE_AGGREGATABLE(ICallInterceptorImpl)

  IUnknown* _GetRawUnknown()
  {
    return static_cast<ICallInterceptor*>(this);
  }

  HRESULT _InternalQueryInterface(REFIID iid, void** ppvObject)
  {
    HRESULT hr = E_NOINTERFACE;

    if (iid == IID_IUnknown || iid == IID_ICallInterceptor)
      hr = CComPtr<ICallInterceptor>(this).CopyTo(
        (ICallInterceptor**)ppvObject);
    else if (iid == IID_ICallUnmarshal)
      hr = CComPtr<ICallUnmarshal>(this).CopyTo((ICallUnmarshal**)ppvObject);
    else if (iid == m_iid)
      hr = CComPtr<IUnknown>((IUnknown*)&m_thunk).CopyTo(
        (IUnknown**)ppvObject);

    return hr;
  }

  HRESULT Initialize(const IID& riid)
  {
    HRESULT hr = m_thunk.Initialize(this, riid);

    if (SUCCEEDED(hr))
      m_iid = riid;

    return hr;
  }

  HRESULT OnCall(ICallFrame* pFrame)
  {
    return m_spSink != 0 ? m_spSink->OnCall(pFrame) : E_FAIL;
  }

  STDMETHOD(CallIndirect)(
    /* [out] */ HRESULT* phrReturn,
    /* [in]  */ ULONG    iMethod,
    /* [in]  */ void*    pvArgs, 
    /* [out] */ ULONG*   cbArgs)
  {
    if (phrReturn == NULL || pvArgs == NULL || cbArgs == 0)
      return E_POINTER;

    if (iMethod >= m_thunk.GetEngine()->GetNumberOfMethods() || iMethod < 3)
      return E_INVALIDARG;

    *phrReturn = m_thunk.Invoke(pvArgs, cbArgs, iMethod);

    return S_OK;
  }
  
  STDMETHOD(GetMethodInfo)(
    /* [in]  */ ULONG          iMethod, 
    /* [out] */ CALLFRAMEINFO* pInfo,
    /* [out] */ LPWSTR*        pwszMethod)
  {
    if (pInfo == NULL || pwszMethod == NULL)
      return E_POINTER;

    if (iMethod >= m_thunk.GetEngine()->GetNumberOfMethods() || (iMethod < 3)
      return E_INVALIDARG;

    HRESULT hr = m_thunk.GetEngine()->GetInterfaceInfo(iMethod, pInfo);

    if (SUCCEEDED(hr))
    {
      *pwszMethod = (LPWSTR)CoTaskMemAlloc(10 * sizeof(WCHAR));
      _ltow(iMethod, *pwszMethod, 10);
    }

    return S_OK;
  }

  STDMETHOD(GetStackSize)(
    /* [in] */ ULONG  iMethod, 
   /* [out] */ ULONG* cbArgs)
  {
    if (cbArgs == 0)
      return E_POINTER;

    *cbArgs = m_thunk.GetEngine()->GetMethodStackSize(iMethod);

    return S_OK;
  }

  STDMETHOD(GetIID)(
    /* [out] */ IID*    piid,
    /* [out] */ BOOL*   pfDerivesFromIDispatch,
    /* [out] */ ULONG*  pcMethod, 
    /* [out] */ LPWSTR* pwszInterface)
  {
    if (piid == NULL || pfDerivesFromIDispatch == NULL || pcMethod == NULL
      || pwszInterface == 0)
      return E_POINTER;

    HRESULT hr = StringFromCLSID(m_iid, pwszInterface);

    if (SUCCEEDED(hr))
    {
      *piid = m_iid;
      *pcMethod = m_thunk.GetEngine()->GetNumberOfMethods();
    }

    return S_OK;
  }

  STDMETHOD(RegisterSink)(/* [in] */ ICallFrameEvents* psink)
  {
    m_spSink = psink;
    return S_OK;
  }

  STDMETHOD(GetRegisteredSink)(/* [out] */ ICallFrameEvents** ppsink)
  {
    if (ppsink == NULL)
      return E_POINTER;

    return m_spSink.CopyTo(ppsink);
  }

  STDMETHOD(Unmarshal)(ULONG  iMethod, PVOID pBuffer, ULONG cbBuffer,
    BOOL fForceBufferCopy, RPCOLEDATAREP dataRep, 
    CALLFRAME_MARSHALCONTEXT * pcontext, ULONG * pcbUnmarshalled, 
    ICallFrame ** ppFrame)
  {
    if (pcontext == NULL || pcbUnmarshalled == NULL || ppFrame == NULL) 
      return E_POINTER;
    
    CComPtr<ICallFrame> spFrame;
    
    HRESULT hr = m_thunk.CreateCallFrame(iMethod, GetControllingUnknown(), 
      pBuffer, cbBuffer, fForceBufferCopy, pcontext, pcbUnmarshalled, 
      &spFrame);

    if (SUCCEEDED(hr))
      hr = spFrame.CopyTo(ppFrame);

    return hr;
  }

  STDMETHOD(ReleaseMarshalData)(ULONG  iMethod, PVOID pBuffer, 
    ULONG cbBuffer, ULONG ibFirstRelease, RPCOLEDATAREP dataRep, 
    CALLFRAME_MARSHALCONTEXT * pcontext)
  {
    if (pcontext == NULL)
      return E_POINTER;

    return E_NOTIMPL;
  }

private:
  CComPtr<ICallFrameEvents> m_spSink;
  ItfThunk<ICallInterceptorImpl> m_thunk;
  IID m_iid;
};

inline HRESULT CreateInterceptor(REFIID iid, IUnknown* pUnkOuter, 
  REFIID riid, void** ppvInterface)
{
  HRESULT hr = S_OK;

  if (ppvInterface == NULL)
    return E_POINTER;

  *ppvInterface = NULL;

  if (pUnkOuter && !InlineIsEqualUnknown(riid))
    return E_NOINTERFACE;

  CComPtr<IUnknown> spUnk;

  if (pUnkOuter == NULL)
  {
    CComObject<ICallInterceptorImpl>* pInt = NULL;
    CComObject<ICallInterceptorImpl>::CreateInstance(&pInt);

    hr = pInt->Initialize(iid);
    spUnk = pInt->GetUnknown();
  }
  else
  {
    CComAggObject<ICallInterceptorImpl>* pInt = NULL;
    CComAggObject<ICallInterceptorImpl>::CreateInstance(pUnkOuter, &pInt);

    hr = pInt->m_contained.Initialize(iid);
    spUnk = pInt;
  }

  if (SUCCEEDED(hr))
    hr = spUnk->QueryInterface(riid, ppvInterface);

  return hr;
}

Для получения перехватчика по нужному IID интерфейса (вспомним аналогичную API-функцию CoGetInterceptor) реализована функция CreateInterceptor.

Реализация интерфейса ICallFrame по-разному использует переданный стек вызова и буфер для маршалинга в зависимости от того, на клиентской или серверной стороне был создан ее экземпляр. На серверной стороне инициализация out-параметров возможна только после того, как был выполнен демаршалинг in-параметров. Это связано с тем, что для некоторых out-параметров в IDL мог быть задан атрибут size_is с указанием одного из in-параметров.

class ICallFrameImpl : public CComObjectRootEx<CComMultiThreadModel>,
  public ICallFrame
{
public:
  ICallFrameImpl() : m_hrRet(0), m_pStack(0), m_nMethod(0), m_dwStackSz(0),
    m_pEngine(0), m_fClient(false)
  {
  }

  ~ICallFrameImpl()
  {
    if (!m_fClient)
    {
      m_pEngine->FreeParams(m_pStack, m_nMethod, m_pInBuffer, m_cbInBuffer);
      free(m_pStack);

      if (m_bFreeBuffer)
        free(m_pInBuffer);
    }
  }

  HRESULT InitializeClient(void* pStack, DWORD dwStackSz, int nMethod,
    NdrEngine* pEngine)
  {
    HRESULT hr = pEngine->InitOutParameters(TRUE, pStack, nMethod);

    if (SUCCEEDED(hr))
      InternalInitialize(pStack, dwStackSz, nMethod, pEngine, TRUE);

    return hr;
  }

  HRESULT InitializeServer(void* pStack, DWORD dwStackSz, int nMethod,
    NdrEngine* pEngine, IUnknown* pExtRef, void* pBuffer, ULONG cbBuffer, 
    BOOL bCopyBuffer, CALLFRAME_MARSHALCONTEXT* pCtx, ULONG* pcbUnmarshalled)
  {
    if (bCopyBuffer)
    {
      void* pTempBuffer = malloc(cbBuffer);
      memcpy(pTempBuffer, pBuffer, cbBuffer);
      pBuffer = pTempBuffer;
    }

    HRESULT hr = pEngine->GenericMarshall(FALSE, pStack, nMethod, pCtx,
      pBuffer, cbBuffer, NdrTypeUnmarshall, false, 0, 0, pcbUnmarshalled);

    if (SUCCEEDED(hr))
    {
      hr = pEngine->InitOutParameters(FALSE, pStack, nMethod);

      if (SUCCEEDED(hr))
      {
        InternalInitialize(pStack, dwStackSz, nMethod, pEngine, FALSE, 
          pExtRef);

        m_pInBuffer = pBuffer;
        m_cbInBuffer = cbBuffer;
        m_bFreeBuffer = bCopyBuffer;
      }
    }

    if (FAILED(hr) && bCopyBuffer)
      free(pBuffer);

    return hr;
  }

  void InternalInitialize(void* pStack, DWORD dwStackSz, int nMethod, 
    NdrEngine* pEngine, BOOL fClient, IUnknown* pExtRef = 0)
  {
    m_pStack = pStack;
    m_dwStackSz = dwStackSz;
    m_nMethod = nMethod;
    m_pEngine = pEngine;
    m_spExtRef = pExtRef;
    m_fClient = fClient;
  }
  
  BEGIN_COM_MAP(ICallFrameImpl)
    COM_INTERFACE_ENTRY(ICallFrame)
  END_COM_MAP()

public:
    STDMETHOD(GetInfo)(/* [out] */ CALLFRAMEINFO *pInfo)
    {
      if (pInfo == NULL)
        return E_POINTER;

      return m_pEngine->GetInterfaceInfo(m_nMethod, pInfo);
    }

    STDMETHOD(GetIIDAndMethod)(/* [out] */ IID *pIID,
      /* [out] */ ULONG *piMethod)
    {
      if (pIID == NULL || piMethod == NULL)
        return E_POINTER;

      *pIID = *m_pEngine->GetIID();
      *piMethod = m_nMethod;

      return S_OK;
    }

    STDMETHOD(GetNames)(
      /* [out] */ LPWSTR* pwszInterface,
      /* [out] */ LPWSTR* pwszMethod)
    {
      if (pwszInterface == NULL || pwszMethod == NULL)
        return E_POINTER;

      HRESULT hr = StringFromCLSID(*m_pEngine->GetIID(), pwszInterface);

      if (SUCCEEDED(hr))
      {
        *pwszMethod = (LPWSTR)CoTaskMemAlloc(10*sizeof(WCHAR));
        _ltow(m_nMethod, *pwszMethod, 10);
      }

      return hr;
    }

    STDMETHOD_(PVOID, GetStackLocation)(void)
    {
      return m_pStack;
    }

    STDMETHOD_(void, SetStackLocation)(/* [in] */ PVOID pvStack)
    {
      m_pStack = pvStack;
    }

    STDMETHOD_(void, SetReturnValue)(/* [in] */ HRESULT hr)
    {
      m_hrRet = hr;
    }

    STDMETHOD(GetReturnValue)(void)
    {
      return m_hrRet;
    }

    STDMETHOD(GetParamInfo)(/* [in] */ ULONG iparam, 
      /* [out] */ CALLFRAMEPARAMINFO *pInfo)
    {
      if (pInfo == 0)
        return E_POINTER;

      return m_pEngine->GetParameterInfo(m_nMethod, iparam, pInfo);
    }

    STDMETHOD(SetParam)(/* [in] */ ULONG iparam, /* [in] */ VARIANT *pvar)
    {
      if (pvar == NULL)
        return E_POINTER;

      if (pvar->vt != VT_BYREF)
        return E_INVALIDARG;

      CALLFRAMEPARAMINFO paramInfo = {};

      HRESULT hr = m_pEngine->GetParameterInfo(m_nMethod, iparam, &paramInfo);

      if (SUCCEEDED(hr))
        *reinterpret_cast<void**>((PBYTE)m_pStack + paramInfo.stackOffset) =
          pvar->byref;

      return hr;
    }

    STDMETHOD(GetParam)(/* [in] */ ULONG iparam,/* [out] */ VARIANT *pvar)
    {
      if (pvar == NULL)
        return E_POINTER;

      VariantInit(pvar);

      CALLFRAMEPARAMINFO paramInfo = {};
      HRESULT hr = m_pEngine->GetParameterInfo(m_nMethod, iparam, &paramInfo);

      if (SUCCEEDED(hr))
      {
        pvar->vt = VT_BYREF;
        pvar->byref = 
          *reinterpret_cast<void**>((PBYTE)m_pStack + paramInfo.stackOffset);
      }

      return hr;
    }

    STDMETHOD(Copy)(
      /* [in]  */ CALLFRAME_COPY    copyControl, 
      /* [in]  */ ICallFrameWalker* pWalker,
      /* [out] */ ICallFrame**      ppFrame)
    {
      return E_NOTIMPL;
    }

    STDMETHOD(Free)(
      /* [in] */ ICallFrame*       pframeArgsDest,
      /* [in] */ ICallFrameWalker* pWalkerDestFree,
      /* [in] */ ICallFrameWalker* pWalkerCopy,
      /* [in] */ DWORD             freeFlags,
      /* [in] */ ICallFrameWalker* pWalkerFree, 
      /* [in] */ DWORD             nullFlags)
    {
      return E_NOTIMPL;
    }

    STDMETHOD(FreeParam)(/* [in] */ ULONG iparam, /* [in] */ DWORD freeFlags,
      /* [in] */ ICallFrameWalker *pWalkerFree, /* [in] */ DWORD nullFlags)
    {
      return E_NOTIMPL;
    }

    STDMETHOD(WalkFrame)(
      /* [in] */ DWORD             walkWhat,
      /* [in] */ ICallFrameWalker* pWalker)
    {
      return m_pEngine->WalkInterfacePtrs(m_pStack, m_nMethod, 
        static_cast<CALLFRAME_WALK>(walkWhat), pWalker);
    }

    STDMETHOD(GetMarshalSizeMax)(
      /* [in] */ CALLFRAME_MARSHALCONTEXT* pmshlContext,
      /* [in] */ MSHLFLAGS                 mshlflags,
      /* [out] */ ULONG*                   pcbBufferNeeded)
    {
      if (pmshlContext == NULL || pcbBufferNeeded == NULL)
        return E_POINTER;

      HRESULT hr = m_pEngine->GenericMarshall(m_fClient, m_pStack, m_nMethod,
        pmshlContext, 0, 0, NdrTypeSize, true, 0, pcbBufferNeeded);

      return hr;
    }

    STDMETHOD(Marshal)(
      /* [in]  */ CALLFRAME_MARSHALCONTEXT* pmshlContext,
      /* [in]  */ MSHLFLAGS                 mshlflags,
      /* [size_is][in] */ PVOID             pBuffer,
      /* [in]  */ ULONG                     cbBuffer,
      /* [out] */ ULONG*                    pcbBufferUsed,
      /* [out] */ RPCOLEDATAREP*            pdataRep,
      /* [out] */ ULONG*                    prpcFlags)
    {
      if (pmshlContext == NULL || pBuffer == NULL)
        return E_POINTER;

      HRESULT hr = m_pEngine->GenericMarshall(m_fClient, m_pStack, m_nMethod,
        pmshlContext, pBuffer, cbBuffer, NdrTypeMarshall, true, 
        m_fClient ? 0 : &m_hrRet, 0, pcbBufferUsed);

      return hr;
    }

    STDMETHOD(Unmarshal)(
      /* [size_is][in] */ PVOID             pBuffer,
      /* [in]  */ ULONG                     cbBuffer,
      /* [in]  */ RPCOLEDATAREP             dataRep,
      /* [in]  */ CALLFRAME_MARSHALCONTEXT* pcontext, 
      /* [out] */ ULONG*                    pcbUnmarshalled)
    {
      if (pcontext == NULL)
        return E_POINTER;

      HRESULT hr = m_pEngine->GenericMarshall(m_fClient, m_pStack, m_nMethod,
        pcontext, pBuffer, cbBuffer, NdrTypeUnmarshall, false,
        m_fClient ? &m_hrRet : 0, 0, pcbUnmarshalled);
      
      return hr;
    }

    STDMETHOD(ReleaseMarshalData)(
      /* [size_is][in] */ PVOID            pBuffer, 
      /* [in] */ ULONG                     cbBuffer,
      /* [in] */ ULONG                     ibFirstRelease,
      /* [in] */ RPCOLEDATAREP             dataRep,
      /* [in] */ CALLFRAME_MARSHALCONTEXT* pcontext)
    {
      if (pcontext == NULL || pBuffer == NULL)
        return E_POINTER;

      HRESULT hr = m_pEngine->ReleaseMarshalBuffer(
        m_fClient, pBuffer, cbBuffer, m_nMethod);

      return hr;
    }

    STDMETHOD(Invoke)(/* [in] */ void *pvReceiver, ...)
    {
      HRESULT hr = S_OK;

      if (pvReceiver == NULL)
        return E_POINTER;

      void* pStack = _alloca(m_dwStackSz);

      memcpy(pStack , m_pStack, m_dwStackSz);
      
      *reinterpret_cast<void**>(pStack) = pvReceiver;

      __asm
      {
        mov eax, pvReceiver
        mov eax, [eax]
        mov edx, [this]
        mov edx, [edx + m_nMethod]
        lea eax, [eax + edx*4]
        call [eax]
        mov edx, [this]
        mov [edx + m_hrRet], eax
      }

      return hr;
    }

private:
  void* m_pInBuffer;
  ULONG m_cbInBuffer;
  BOOL m_bFreeBuffer;

  void* m_pStack;
  DWORD m_dwStackSz;
  int m_nMethod;
  HRESULT m_hrRet;
  BOOL m_fClient;
  CComPtr<IUnknown> m_spExtRef;
  NdrEngine* m_pEngine;
};

Метод Invoke вызывает нужный метод реального компонента, используя стек вызова, хранящийся внутри ICallFrameImpl, и запоминает возвращаемое значение, которое позднее может быть получено с помощью вызова ICallFrame::GetReturnValue.

Заключение

В результате знакомства с механизмом MDR RPC FormatString нам удалось создать альтернативную реализацию стандартных интерфейсов перехватчика ICallInterceptor/ICallUnmarshal/ICallFrame, которая способна работать не только с automation-совместимыми интерфейсами, но и с более широким классом интерфейсов, proxy/stub которых были скомпилированы в Oicf-интерпретируемом режиме. Несомненное преимущество этой реализации заключается в том, что она доступна в исходных текстах, и ее поведение может изменяться в соответствии с требованиями конкретного приложения.


Эта статья опубликована в журнале RSDN Magazine #2-2005. Информацию о журнале можно найти здесь