Продолжается подписка на наши издания! Вы не забыли подписаться?

Секреты маршалинга

Код к статье: ftp.k-press.ru/pub/cs/2000/3/marshalling/MarshalingCode.zip (112 kb)
Скомпилированные примеры: ftp.k-press.ru/pub/cs/2000/3/marshalling/Bin.zip (145 kb)

В прошлом номере нашего журнала были опубликованы два обзора – по CORBA и COM. Их целью было дать представление о конкурирующей технологии программистам, уже владеющим одной из них. Но эти обзоры можно использовать и как систематизирующий материал, позволяющий ориентироваться в соответствующих технологиях программистам разного уровня подготовки. Многие, прочитав его, открыли для себя новые стороны своей любимой технологии. Увы, большинство современных технологий (а уж COM и CORBA стопроцентно) являются очень сложными. Их описа-ние, помещающееся в журнальной статье, может только поверхностно затрагивать суть этих технологий. Понимая это, мы приняли решение опубликовать ряд статей, подробнее раскрывающих самые интересные аспекты COM и CORBA. Конкретно эта статья посвящена маршалингу в COM. Если вы использовали COM в своих приложениях и при этом делали межпроцессные и/или межмашинные вызовы, то, наверное, вас должен был интересовать один вопрос – «А как, собственно, это работает?». Нет, то, как примерно это делается, знают многие, но как конкретно? Именно на этот вопрос и призвана ответить эта статья. Она затрагивает не только теоретические аспекты маршалинга. Здесь будет приведен код, позволяющий на практике использовать преимущества нестандартного (ручного) маршалинга. Мы обсудим также вопросы, связанные с «бесшовной» передачей COM-объектов по значению (Marshaling By Value – MBV) и оптимизацией сетевых вызовов. Маршалинг (Marshaling)

Marshaling – это процесс запаковки и посылки вызовов (методов COM-интерфейса) через границу процесса (или потоков одного и того же приложения). COM поддерживает два вида маршалинга: «стандартный маршалинг» и «ручной маршалинг». Первый подразумевает практически автоматическую поддержку со стороны средств разработки. Второй же дает максимальную гибкость и позволяет заменить реализацию процесса маршалинга на собственную. Попробуем подробнее разобраться с каждым из них.

В процессе маршалинга в другой контекст (возможно, удаленный) передаются данные, которые там распаковываются и преобразуются в вид, пригодный для использования. Указатели на эти данные помещаются в стек, и производится передача управления вызываемому/вызывающему коду. При этом в качестве данных могут быть переданы и указатели на другие интерфейсы (возможно, других объектов). Именно передача (маршалинг) интерфейсов нас и будет интересовать, больше всего, так как именно при этом встает вопрос – «Какой тип и подтип маршалинга будет использовать переданный указатель на интерфейс?». Постарайтесь не запутаться в терминах. «Маршалинг указателя на интерфейс» и «Маршалинг для интерфейса» (или просто маршалинг). Первый подразумевает установление связи между удаленным объектом и вызывающим апартаментом, а второй – комплекс мер по запаковке и посылке вызовов методов COM-интерфейса. Это очень важно! Зачем нужен маршалинг между процессами, понятно. Процессы в Win32 имеют разные адресные пространства, поэтому указатели на данные в другом процессе не имеют смысла даже в пределах одного компьютера, не говоря уже о разных. А зачем нужен маршалинг между потоками одного и того же процесса? Коротко на этот вопрос можно ответить так: для облегчения работы программистов не умеющих, не имеющих возможности (например, VB-программистов) или попросту не желающих писать потокобезопасный код. Если говорить точнее, то это связано с апартаментной моделью, принятой в COM. Объяснение принципов апартаментной модели является непростой задачей, требующей отдельного разговора. В следующих номерах нашего журнала мы обязательно выделим место для этого «разговора», а пока для простоты будем считать, что маршалинг происходит между процессами, находящимися на разных компьютерах. Так что если вы не очень хорошо понимаете, что такое «апартамент» и «контекст», то, встретив фразу типа «...передача блока данных в другой апартамент...» или «...происходит в другом контексте...», просто мысленно заменяйте слова «апартамент» и «контекст» на «компьютер» и, скорее всего, смысл останется верным. В случаях, когда речь будет идти об апартаментах и контекстах в одном и том же процессе, об этом будет отдельно сказано.

Стандартный маршалинг

Стандартный маршалинг может быть подразделен на два подкласса – proxy/stub-маршалинг и typelib-маршалинг. Первый из них может быть расширен за счет применения техники, называемой wire-маршалингом, и оба из них можно заменить комбинацией стандартного маршалинга с «ручным». Но обо всем по порядку...

Явно COM поддерживает стандартный маршалинг только для одного COM-интерфейса, IUnknown. Proxy- и stub-менеджеры умеют заставить работать между контекстами только этот интерфейс. К счастью, proxy- и stub-менеджеры расширяемы, что позволяет другим интерфейсам использовать стандартный маршалинг. Как показано на Рис. 1, proxy-менеджер может загрузить proxy для преобразования вызовов методов интерфейса в ORPC-запросы. Stub-менеджер может загрузить stub, который будет преобразовать эти запросы в вызовы метода реального объекта, как показано на Рис. 2.

Рис. 1 Расширение Proxy-менеджера

Рис. 2 Расширение Stub-менеджера

Все интерфейсы, кроме IUnknown, для работы со стандартным маршалингом должны иметь зарегистрированные в системе proxy/stub. Это справедливо для интерфейсов, определенных как Microsoft, так и сторонними разработчиками. Например, если интерфейс IStorage использует стандартный маршалинг, то в HKEY_CLASSES_ROOT\Interface должен быть добавлен ключ, имеющий в качестве имени IID этого интерфейса. В этом ключе должен иметься подключ ProxyStubClsid32, указывающий на inprocess сервер, содержащий proxy/stub для этого интерфейса. К счастью, IID_IStorage содержит такое вхождение. Если вы посмотрите его содержимое, то, скорее всего, обнаружите там GUID – {00000320-0000-0000-C000-000000000046}. Перейдя к одноименному подключу в HKEY_CLASSES_ROOT\CLSID, вы увидите, что он указывает на OLE32.DLL, а дружественное имя класса – PSFactoryBuffer (под Windows 2000) или oleprx32_PSFactory (под Windows NT 4.0). Этот же GUID можно найти у большинства интерфейсов, составляющих ядро COM и рассчитанных на межпроцессное взаимодействие. Так как определяемые вами интерфейсы не считаются частью ядра COM, фабрика proxy/stub, встроенная в OLE32.DLL, не может создать proxy/stub для вашего интерфейса. К счастью, компилятор MIDL создает исходный код на C, который, будучи откомпилированным и зарегистрированным, позволит вашему интерфейсу использовать стандартный маршалинг. Когда вы зарегистрируете полученную proxy/stub-DLL, в реестр будут добавлены вхождения, необходимые, чтобы COM мог ее найти. В общем, получается неплохая картина: никакого ручного кодирования... Просто компилируете и инсталлируете proxy/stub-DLL, и ваши интерфейсы волшебным образом начинают работать, хотя до этого не работали (кросс-контекстные клиенты получали от QueryInterface E_NOINTERFACE). Единственное, что смущает в этой идиллии – все время приходится следить за proxy/stub-DLL. Она должна быть, она должна быть правильной версии (не то можно потратить много часов в поисках никогда не существовавшей ошибки в своем коде), она должна быть зарегистрирована после регистрации библиотеки, в которой описан сам интерфейс. От всех этих проблем может избавить typelibrary-маршалинг, но чтобы разобраться с ним, нужно сперва разобраться с proxy/stub-маршалингом, предоставляемым MIDL. Итак, все proxy/stub DLL должны содержать фабрику классов, реализующую интерфейс IPSFactoryBuffer вместо IClassFactory. Вот IDL-описание интерфейса IPSFactory-Buffer:

[
    local, object,
    uuid(D5F569D0-593B-101A-B569-08002B2DBF7A)
]
interface IPSFactoryBuffer : IUnknown 
{
// Вызывается proxy-менеджером для создания или
// агрегации proxy
  HRESULT CreateProxy(
    // Указатель на proxy-менеджер
    [in] IUnknown *pUnkOuter,
    // IID запрашиваемого интерфейса
    [in] REFIID riid, 
    // Указатель на RpcProxy
    [out] IRpcProxyBuffer **ppProxy,
    // Указатель на запрашиваемый интерфейс
    [out] void **ppv 
    );
// Вызывается stub-менеджером для создания stub
  HRESULT CreateStub(
    // IID запрашиваемого интерфейса
         [in] REFIID riid,
    // Указатель на реальный объект
          [in] IUnknown *pUnkServer,
    // Указатель на RpcStub
          [out] IRpcStubBuffer **ppStub
    );
}

Для создания proxy для вашего интерфейса proxy-менеджер выполняет следующие действия:

// Поиск ProxyStubClsid32 интерфейса ISomeInterface
CLSID psclsid;
HRESULT hr = CoGetPSClsid(IID_ISomeInterface, &psclsid);
if (SUCCEEDED(hr)) 
{
    // Загрузка proxy/stub-объекта
    IPSFactoryBuffer *psfb = 0;
    hr = CoGetClassObject(psclsid,CLSCTX_INPROC, 0,
                          IID_IPSFactoryBuffer, (void**)&psfb);
    if (SUCCEEDED(hr))
    {
        // Создание proxy для ISomeInterface
        hr = psfb->CreateProxy(this, IID_ISomeInterface,
                               &m_pProxy, (void**)&pClientPtr);
        pfsb->Release();
    }
}

Заметьте, что CoGetPSClsid – это реально существующая API-функция, используемая proxy- и stub-менеджерами для поиска маршалеров интерфейсов. Сначала эта функция просматривает хранящуюся в памяти таблицу, чтобы выяснить, не был ли уже найден ключ реестра, или не зарегистрировал ли кто (внутри процесса) с помощью функции CoRegisterPSClsid отображение IID на PSCLSID. Если IID не находится в этой таблице, CoGetPSClsid ищет вхождение ProxyStubClsid32 в реестре и кэширует его в таблице для ускорения последующих поисков. После определения CLSID маршалера создается proxy/stub-объект. Это делается обычным для COM способом, через функцию CoGetClassObject. У этого объекта вызывается метод CreateProxy, который и создает proxy для необходимого интерфейса. Stub-менеджер использует сходную процедуру для создания stub'а, но вместо CreateProxy вызывает CreateStub. Реализации функций CreateProxy и CreateStub генерируются MIDL'ом. В зависимости от ключей командной строки, передаваемой MIDL'у, можно получить совершенно разный результат. Так, если оставить настройки MIDL по умолчанию, без всяких ключей командной строки, например:

midl.exe Bob.idl

, MIDL выдаст исходный C-код для proxу и stub. Этот код помещается в файл *_p.c (в вышеприведенном примере это будет bob_p.c) в виде функций IBob_MethodX_Proxy и IBob_MethodY_Stub. Если войти при помощи отладчика в proxy, то можно увидать, что реально функции _Proxy находятся в vtable proxy. Если на серверной стороне поставить точки остановки внутри одного из методов этого объекта и посмотреть стек вызовов, то будет хорошо видно, что вызов осуществляется напрямую соответствующим методом _Stub. Правда, чтобы получить возможность отлаживать proxy/stub-код, надо немного поколдовать. Во-первых, вам будет нужно удалить директивы #pragma code_seg(".orpc") из файла _p.c, а во-вторых, скомпилировать proxy/stub DLL с отладочной информацией. Удаление директивы «#pragma code_seg(".orpc")» необходимо, так как знакомые с COM отладчики не хотят входить в сегмент с именем ".orpc", видимо, наивно полагая, что код, генерируемый MIDL, никогда не содержит ошибок (наивные...). На рисунках 3 и 4 показаны структуры proxy и stub, сгенерированных MIDL для интерфейса ISomeInterface.

Рис. 3 Откомпилированный proxy интерфейса

Рис. 4 Откомпилированный Stub интерфейса

Заметьте, что vtable заполнена сгенерированными MIDL функциями *_Proxy. Заметьте также, что stub содержит вектор указателей на *_Stub-функции. Этот вектор используется стандартной реализацией IRpcStubBuffer::Invoke (CStdStubBuffer_Invoke из RPCRT4.DLL) для доставки по назначению входящего вызова. В наши дни описанный подход выглядит старомодно, в большой степени потому, что содержит компилированный C-код, выполняющий маршалинг. Теперь более выгодно использовать интерпретирующие маршалеры. MIDL- компилятор с ключом /Oicf может генерировать байтовый код вместо C-кода:

midl.exe /Oicf bob.idl

Таблица 1 
Сравнение объемов памяти, занимаемой Oicf- и Оs (не Oicf-) маршалерами

  

Минимальный
IDL 

IDL с 5-ю
интерфейсами 

IDL с 10-ю
интерфейсами 

ADO
2.0 

/Oicf 

24KB

29KB 

29KB 

36KB 

/Os
(Не-Oicf) 

24KB 

78KB 

135KB 

208KB 

Основанный на /Oicf маршалер предпочтительнее из-за сравнительно малого объема используемой памяти. Таблица 1 показывает затраты памяти для различных IDL-файлов. При создании основанного на /Oicf маршалера файл _p.c содержит две строки байтов вместо исполняемых C-функций. Наиболее важной является строка MIDL_PROC_FORMAT_STRING. Эта строка содержит байт-код, описывающий сигнатуры всех методов, содержащихся в IDL-файле. Листинг 1 содержит строку формата для следующего IDL:

[ uuid(12341234-2134-2134-5235-123563234431) ]
 interface ISomeInterface : IUnknown 
{
     import "unknwn.idl";
     struct BOB { long a; long b; };
     HRESULT Eat([out, retval] long *pn);
     HRESULT Sleep([in] struct BOB *pBob, 
                   [out, retval] long *pn);
     HRESULT Drink([in] struct BOB *pBob, 
                   [out, retval] long *pn);
}

Листинг 1

MIDL_PROC_FORMAT_STRING 
static const MIDL_PROC_FORMAT_STRING __MIDL_ProcFormatString =    
    {        
        0,        
        {    
            /* Procedure Eat */           
            0x33,                 /* FC_AUTO_HANDLE */  
            0x6c,                 /* Old Flags:  object, Oi2 */
/*  2 */    NdrFcLong( 0x0 ),     /* 0 */
/*  6 */    NdrFcShort( 0x3 ),    /* 3 */
/*  8 */    NdrFcShort( 0xc ),    /* x86, MIPS, PPC Stack size/offset = 12 */
/* 10 */    NdrFcShort( 0x0 ),    /* 0 */
/* 12 */    NdrFcShort( 0x10 ),   /* 16 */
/* 14 */    0x4,                  /* Oi2 Flags:  has return, */
            0x2,                  /* 2 */

    /* Parameter pn */

/* 16 */    NdrFcShort( 0x2150 ), /* Flags:  out, base type, 
                                    simple ref, srv alloc size=8 */
/* 18 */    NdrFcShort( 0x4 ),    /* x86, MIPS, PPC Stack size/offset = 4 */
/* 20 */    0x8,                  /* FC_LONG */
            0x0,                  /* 0 */

    /* Return value */

/* 22 */    NdrFcShort( 0x70 ),   /* Flags:  out, return, base type, */
/* 24 */    NdrFcShort( 0x8 ),    /* x86, MIPS, PPC Stack size/offset = 8 */
/* 26 */    0x8,                  /* FC_LONG */
            0x0,                  /* 0 */

    /* Procedure Sleep */

/* 28 */    0x33,                 /* FC_AUTO_HANDLE */
            0x6c,                 /* Old Flags:  object, Oi2 */
/* 30 */    NdrFcLong( 0x0 ),     /* 0 */
/* 34 */    NdrFcShort( 0x4 ),    /* 4 */
/* 36 */    NdrFcShort( 0x10 ),   /* x86, MIPS, PPC Stack size/offset = 16 */
/* 38 */    NdrFcShort( 0x10 ),   /* 16 */
/* 40 */    NdrFcShort( 0x10 ),   /* 16 */
/* 42 */    0x4,                  /* Oi2 Flags:  has return, */
            0x3,                  /* 3 */

    /* Parameter pBob */

/* 44 */    NdrFcShort( 0x10a ),  /* Flags:  must free, in, simple ref, */
/* 46 */    NdrFcShort( 0x4 ),    /* x86, MIPS, PPC Stack size/offset = 4 */
/* 48 */    NdrFcShort( 0xa ),    /* Type Offset=10 */

    /* Parameter pn */

/* 50 */    NdrFcShort( 0x2150 ), /* Flags:  out, base type, 
                                     simple ref, srv alloc size=8 */
/* 52 */    NdrFcShort( 0x8 ),    /* x86, MIPS, PPC Stack size/offset = 8 */
/* 54 */    0x8,                  /* FC_LONG */
            0x0,                  /* 0 */

    /* Return value */

/* 56 */    NdrFcShort( 0x70 ),   /* Flags:  out, return, base type, */
/* 58 */    NdrFcShort( 0xc ),    /* x86, MIPS, PPC Stack size/offset = 12 */
/* 60 */    0x8,                  /* FC_LONG */
            0x0,                  /* 0 */

    /* Procedure Drink */

/* 62 */    0x33,                 /* FC_AUTO_HANDLE */
            0x6c,                 /* Old Flags:  object, Oi2 */
/* 64 */    NdrFcLong( 0x0 ),     /* 0 */
/* 68 */    NdrFcShort( 0x5 ),    /* 5 */
/* 70 */    NdrFcShort( 0x10 ),   /* x86, MIPS, PPC Stack size/offset = 16 */
/* 72 */    NdrFcShort( 0x10 ),   /* 16 */
/* 74 */    NdrFcShort( 0x10 ),   /* 16 */
/* 76 */    0x4,                  /* Oi2 Flags:  has return, */
            0x3,                  /* 3 */

    /* Parameter pBob */

/* 78 */    NdrFcShort( 0x10a ),  /* Flags:  must free, in, simple ref, */
/* 80 */    NdrFcShort( 0x4 ),    /* x86, MIPS, PPC Stack size/offset = 4 */
/* 82 */    NdrFcShort( 0xa ),    /* Type Offset=10 */

    /* Parameter pn */

/* 84 */    NdrFcShort( 0x2150 ), /* Flags:  out, base type, 
                                     simple ref, srv alloc size=8 */
/* 86 */    NdrFcShort( 0x8 ),    /* x86, MIPS, PPC Stack size/offset = 8 */
/* 88 */    0x8,                  /* FC_LONG */
            0x0,                  /* 0 */

    /* Return value */

/* 90 */    NdrFcShort( 0x70 ),   /* Flags:  out, return, base type, */
/* 92 */    NdrFcSi2rt( 0xc ),    /* x86, MIPS, PPC Stack size/offset = 12 */
/* 94 */    0x8,                  /* FC_LONG */
            0x0,                  /* 0 */
            0x0
        }
    };

Нетрудно заметить, что строка состоит из последовательности блоков, каждый из которых описывает метод интерфейса. Если попытаться создать IDL-описание такого блока, то получится примерно следующее: Структура, описывающая метод:

struct METHOD_INFO 
{
     byte    handle_type;    
     byte    old_flags;   
     long    reserved;
     short   method_index;
     short   stack_size;
     short   in_param_hint;
     short   out_param_hint;
     byte    oi2_flags;
     byte    cParams;
     [size_is(cParams)] PARAM_INFO params[];
 };

Структура, описывающая параметр метода:

struct PARAM_INFO 
{
     short flags;
     short stack_offset;
     union 
     {
         struct 
         {
           byte fc_basetype_code;
           byte reserved;
         };
         short type_offset
     };
};

Заметьте, что точный формат /Oicf-строк не документирован, вообще не поддерживается и может измениться в любой момент. Но посмотреть на /Oicf-строки интересно хотя бы для того, чтобы понять, как работает COM.

Заметьте также, что в случае простого метода Eat, все его описание содержится внутри MIDL_PROC_FORMAT_STRING (смещения 0-27). Два других метода, Sleep и Drink (смещения 28-61 и 62-95 соответственно), имеют в качестве параметра указатель на структуру BOB. Информация о нестандартных (не базовых) типах данных помещается MIDL'ом в MIDL_TYPE_FORMAT_STRING, а описания параметров, использующих эти типы, содержит смещение внутри MIDL_PROC_FORMAT_STRING (см. PARAM_INFO.type_offset). Вы можете видеть эти смещения в строках 48 и 82 листинга 1.

Строка формата типа (MIDL_TYPE_FORMAT_STRING) для IDL-описания интерфейса ISomeInterface приводится в Листинге 2. Вместо повторения потенциально сложного описания типа данных в строке формата процедуры (MIDL_PROC_FORMAT_STRING), MIDL один раз помещает определение типа в строку формата типа (MIDL_TYPE_FORMAT_STRING). Этот же принцип применяется и в том случае, когда сложный тип, описанный в MIDL_TYPE_FORMAT_STRING, содержит поле (прямо или по ссылке) другого сложного типа. То есть строка формата типа содержит ссылки на саму себя, что резко уменьшает ее размер.

Листинг 2. MIDL_TYPE_FORMAT_STRING

static const MIDL_TYPE_FORMAT_STRING __MIDL_TypeFormatString =
    {
        0,
        {
            NdrFcShort( 0x0 ),  /* 0 */
/*  2 */    
            0x11, 0xc,          /* FC_RP [alloced_on_stack] [simple_pointer] */
/*  4 */    0x8,                /* FC_LONG */
            0x5c,               /* FC_PAD */
/*  6 */    
            0x11, 0x0,          /* FC_RP */
/*  8 */    NdrFcShort( 0x2 ),  /* Offset= 2 (10) */
/* 10 */    
            0x15,               /* FC_STRUCT */
            0x3,                /* 3 */
/* 12 */    NdrFcShort( 0x8 ),  /* 8 */
/* 14 */    0x8,                /* FC_LONG */
            0x8,                /* FC_LONG */
/* 16 */    0x5c,               /* FC_PAD */
            0x5b,               /* FC_END */

            0x0
        }
    };

Как же эти две строки связаны с proxу и stub? Как их найти, если есть только указатель на proxy интерфейса? Рисунки Рис. 5 и Рис. 6 показывают раскладку основанных на /Oicf proxу и stub. Заметьте, что строки формата можно найти, обратившись назад относительно COM vtable. Это довольно просто для Stub, поскольку реализация IRpcStubBuffer::Invoke требует вернуться назад от адреса, на который указывает vptr, на 12 байтов и разадресовать найденный там указатель. 12-ю байтами ниже вы найдете указатель на строку MIDL_PROC_FORMAT_STRING. Естественно, это будет уже не ее текстовое MIDL-описание, а бинарная структура.

Рис. 5 Структура proxy для интерфейса ISomeInterface в случае использования /Oicf-маршалинга

Сходным образом можно найти строку MIDL_TYPE_FORMAT_STRING. Внутри реализации IRpcStubBuffer::Invoke нужно только вычислить смещение в строке формата, основываясь на вызванном методе. Это просто, поскольку MIDL создает таблицу вхождений методов по типу индекс-смещение. По строке формата реализация IRpcStubBuffer::Invoke переходит в процедуру NdrStubCall2. Эта процедура находится в RPCRT4.DLL. Она представляет собой функцию, берущую строку формата и выполняющую интерпретирующий демаршалинг. После демаршалинга NdrStubCall2 вызывает реализацию метода настоящего объекта. Когда метод объекта возвращает управление обратно функции NdrStubCall2, она использует MIDL_TYPE_FORMAT_STRING для маршалинга [out]-параметров и возвращает управление в IRpcStubBuffer::Invoke.


Рис. 6 Структура stub для интерфейса ISomeInterface в случае использования /Oicf-маршалинга

Proxy работает примерно так же. Но, поскольку клиенту должна быть возвращена vtable неизвестной заранее длины, MIDL создает vtable, заполненный не реальными указателями на функции, а значениями –1 (0xffffffff). Перед тем, как возвратить клиенту vtable, runtime заполняет эти вхождения маленькими процедурами-переходниками ObjectStublessClientXXX, где XXX – индекс метода. Такой переходник помещает индекс метода в регистр ecx и осуществляет переход на универсальную функцию ObjectStubless, также находящуюся в RPCRT4.DLL. Вот код ObjectStublessClient9:

mov ecx, 0x00000009 ; индекс в виртуальной таблице
jmp ObjectStubless  ; переход на универсальную функцию-обработчик

ObjectStubless просто вызывает функцию ObjectStublessClient, передавая ей индекс метода (из ecx) в качестве параметра. Наконец, ObjectStublessClient считывает строки формата и переходит на NdrClientCall2. NdrClientCall2 выполняет интерпретирующий маршалинг и демаршалинг.

Хотя в обсуждении было пропущено множество деталей (практически все они недокументированны и могут измениться в любой момент), суть в том, что виртуальные таблицы в /Oicf-маршалере снабжены комментариями в виде строк формата, полностью описывающими интерфейс. Знать все эти подробности совершенно необязательно. Просто используйте /Oicf, и вы сами увидите, как уменьшится размер proxy/stub DLL.

Маршалинг, основанный на библиотеках типов (TypeLib-маршалинг)

Интерфейсы, помеченные атрибутами [oleautomation] или [dual], получают особую обработку. При регистрации библиотеки типов, в которой они описаны, COM добавляет ключ ProxyStubClsid32 и заносит {00020424-0000-0000-C0000-000000000046} в (Default)-значение этого ключа. Это CLSID маршалера с именем PSOAInterface. Этот компонент проживает в OLEAUT32.DLL (OLE automation DLL). Из-за места проживания этот маршалер иногда называют [oleautomation]-маршалером, typelibrary-маршалером или универсальным маршалером. Я буду называть его typelib-маршалером.

Typelib-маршалер не может загрузить proxy/stub-DLL, ведь ее попросту не существует. Вместо этого он в своих процедурах CreateProxy и CreateStub, черпая описание интерфейса из библиотеки типов, на лету создает /Oicf-proxy/stub. Поскольку эффективного пути отыскать ITypeInfo для произвольного интерфейса не существует, то, чтобы узнать описание интерфейса, ему надо знать три составляющие: LIBID библиотеки типов, версию библиотеки типов и IID-интерфейса. IID typelib-маршалер получает в качестве параметров своих методов CreateProxy и CreateStub, а LIBID и версию библиотеки типов должны храниться в ключе реестра HKCR\Interface\{XXX}\TypeLib.

После загрузки ITypeInfo, описывающего интерфейс, typelib-маршалер вызывает одну из двух недокументированных функций: CreateProxyFromTypeInfo для создания proxy или CreateStubFromTypeInfo для создания Stub'а. Обе эти функции находятся в RPCRT4.DLL. Вот их описание:

HRESULT CreateProxyFromTypeInfo(
     [in] ITypeInfo *pTypeInfo,
     [in] IUnknown *pUnkOuter,
     [in] REFIID riid,
     [out] IRpcProxyBuffer **ppProxy,
     [out] void **ppv
 );
 HRESULT CreateStubFromTypeInfo(
     [in] ITypeInfo *pTypeInfo,
     [in] REFIID riid,
     [in, unique] IUnknown *pUnkServer,
     [out] IRpcStubBuffer **ppStub
 );

Заметьте, что эти функции имеют описание, практически идентичное методам IPSFactoryBuffer. Очевидное исключение представляет наличие параметра ITypeInfo, используемого RPCRT4 для создания строк формата в стиле /Oicf.

Ниже приведен псевдокод реализаций typelib-маршалера:

struct PSTypeLibFactoryBuffer : 
   CComObjectRootEx<CComMultiThreadModel>,
   IPSFactoryBuffer
{
BEGIN_COM_MAP(PSTypeLibFactoryBuffer)
    COM_INTERFACE_ENTRY(IPSFactoryBuffer)
END_COM_MAP()
        
    STDMETHODIMP CreateProxy(IUnknown *pUnkOuter, REFIID riid, 
                             IRpcProxyBuffer **ppProxy, void **ppv)
    {
        *ppv = *ppProxy = 0;
        // Находим через реестр и открываем библиотеку типов
        ITypeLib *ptl = 0;
        HRESULT hr = LoadCachedTypeLibFromIID(riid, &ptl);
        if (SUCCEEDED(hr))
        {
            ITypeInfo *pti = 0;
            // Получаем TypeInfo для интерфейса
            hr = ptl->GetTypeInfoOfGuid(riid, &pti);
            if (SUCCEEDED(hr))
            {
                // Просим RPCRT4.DLL создать по ITypeInfo /Oicf-proxy 
                hr = CreateProxyFromTypeInfo(pti, pUnkOuter, riid, 
                                             ppProxy, ppv);
                pti->Release();
            }
            ptl->Release();
        }
        return hr;
    }
    
    STDMETHODIMP CreateStub( 
        REFIID riid,
        IUnknown *pUnkServer,
        IRpcStubBuffer **ppStub)
    {
        *ppStub = 0;
        // Находим через реестр и открываем библиотеку типов
        ITypeLib *ptl = 0;
        HRESULT hr = LoadCachedTypeLibFromIID(riid, &ptl);
        if (SUCCEEDED(hr))
        {
            ITypeInfo *pti = 0;
            // Получаем TypeInfo для интерфейса
            hr = ptl->GetTypeInfoOfGuid(riid, &pti);
            if (SUCCEEDED(hr))
            {
                // Просим RPCRT4.DLL создать по ITypeInfo /Oicf- stub 
                hr = CreateStubFromTypeInfo(pti, riid, pUnk-Server, ppStub);
                pti->Release();
            }
            ptl->Release();
        }
        return hr;
    }
};

Обратите внимание – методы просто грузят библиотеку типов и передают ITypeInfo нужного интерфейса соответствующей процедуре RPCRT4. Процедуры CreateXXXFrom-TypeInfo выполняют изрядное количество кэширования для смягчения затрат на создание vtable и строк формата. Typelib-marshaler также выполняет некое кэширование для уменьшения затрат времени при поиске и открытии информации о типах.

Создаваемые typelib-маршалером proxy и stub ничем не отличаются от созданных компилятором MIDL с /Oicf-опцией. Главное отличие в том, что основанные на /Oicf proxy/stub-DLL теоретически должны быстрей создавать первые proxy и/или stub, так как строки формата /Oicf прекомпилированы и не требуют дополнительной обработки. Однако разница может оказаться совсем несущественной, так как библиотеки типов, по сути, хранят прекомпилированную информацию о маршалинге (ITypeInfo::GetMops).

Ну и?

Так какой метод выбрать при использовании стандартного маршалинга? И вообще если /Oicf- и typelib-маршалеры так круты, то зачем нужен обычный маршалинг?

За всю свою жизнь я еще не видел ничего абсолютно «крутого». Если у некоторой бочки содержимое круто как вареные куриные яйца и аппетитно как мед, почему-то обязательно находится ложка (чего подберите сами, по вкусу) которая все испортит, ну или, по крайней мере, немного испортит настроение. Так и тут. Скажите, много вы знаете интерпретаторов, генерирующих P-код (или в современном звучании Byte-код) который был бы быстрее нормального машинного кода. Правильно, это так же невозможного, как и создание вечного двигателя. Это справедливо и для /Oicf-маршалинга. За малые размеры proxy/stub-DLL мы расплачиваемся пониженной производительностью. У /Oicf-маршалинга есть и некоторое ограничение – если метод содержит более 16 параметров, то для него автоматически генерируется нормальная (не интерпретируемая) версия маршалинга, даже если MIDL'у был задан флаг /Oicf (по крайней мере, в документации сказано именно это). Кстати, независимо от флагов MIDL'а, тем, какой способ применять для маршалинга конкретного метода, можно управлять, задавая специальный атрибут – optimize(...) в .acf-файле. Это атрибут можно задавать как для всего интерфейса, так и для отдельно взятого метода интерфейса. optimize("s") говорит MIDL, что надо создавать компилируемый (/Os) маршалинг, а optimize ("i") – что надо создать строки формата для интерпретируемого (/Oicf) маршалинга.

В общем, выбор между обычным (/Os) и интерпретируемым (/Oicf) маршалингом – это выбор между оптимизацией по скорости и оптимизацией по размеру кода. Выбор же typelib-маршалинга может быть продиктован соображениями простоты его использования, высокой совместимостью, а иногда (например, в случае VB) и отсутствием альтернатив :).

Таблица 2 

   /Os Proxy/Stub
(Не /Oicf) 
/Oicf Proxy/Stub DLL  TypeLib
Маршалинг 
Поддержка 16-битных задач Нет

Нет

Да
Поддержка NT 3.51 или Windows 95 (без DCOM) Да Нет Да

Расходование памяти

Большое

Маленькое

Маленькое
Поддержка структур

Полная Полная Ограничено OLE automation совместимостью. Появилось в  NT4 SP4 и в DCOM95 v. 1.2
Поддержка объединений Полная

Полная

Отсутствует
Поддержка атр. [iid_is], [size_is], [call_as], [transmit_as], [wire_marshal] Полная Полная Отсутствует

Ручной маршалинг (Custom marshaling)

Когда указатель на интерфейс передается как параметр некоторого метода удаленного объекта, всегда вызывается функция CoMarshalInterface. Она спрашивает объект, которому принадлежит интерфейс, хочет ли он использовать стандартный маршалинг, или желает сам заняться маршалингом. Этот вопрос ставится в форме запроса (через QueryInterface) интерфейса IMarshal. Если объект не поддерживает этого интерфейса (по умолчанию объекты его не поддерживают), то COM предполагает, что связи через стандартные proxy и stub будет более чем достаточно. Однако если вы сказали "да" при вызове QueryInterface, ваш объект получит возможность собственноручно заняться установкой связи с получателем. Вот описание интерфейса IMarshal:

interface IMarshal : IUnknown 
{
    HRESULT GetUnmarshalClass(
        [in] REFIID riid,
        [in, unique] void *pv,
        [in] DWORD dwDestContext,
        [in, unique] void *pvDestContext,
        [in] DWORD mshlflags, 
        [out] CLSID *pCid);

    HRESULT GetMarshalSizeMax(
        [in] REFIID riid,
        [in, unique] void *pv,
        [in] DWORD dwDestContext,
        [in, unique] void *pvDestContext,
        [in] DWORD mshlflags, 
        [out] DWORD *pSize);

    HRESULT MarshalInterface(
        [in, unique] IStream *pStm,
        [in] REFIID riid,
        [in, unique] void *pv,
        [in] DWORD dwDestContext,
        [in, unique] void *pvDestContext,
        [in] DWORD mshlflags);

    HRESULT UnmarshalInterface(
         [in, unique] IStream *pStm, 
         [in] REFIID riid, 
         [out] void **ppv);

    HRESULT ReleaseMarshalData([in, unique] IStream *pStm);

    HRESULT DisconnectObject ([in] DWORD dwReserved);
}

Итак, объекты, реализующие интерфейс IMarshal, считаются поддерживающими ручной маршалинг. Для такого объекта stub создаваться не будет. Вместо этого вам дается возможность указать объект, который будет создан в удаленном апартаменте, и послать ему блок байтов произвольной длины. Этот блок передается в одном пакете с сообщением об установке соединения. При получении этого сообщения апартаментом получателя COM создаст inprocess-объект указанного класса. Этот объект носит гордое имя unmarshaler. Он должен реализовать интерфейс IMarshal. Задача этого объекта – прочитать блок предназначенных для него данных и, основываясь на запакованной информации, создать и проинициализировать inprocess-объект, который будет возвращен получателю. Этот объект называется proxy, но это не стандартный proxy, а созданный вручную. Вместо создания нового объекта unmarshaler может просто возвратить получателю указатель на один из своих интерфейсов. При этом функции unmarshaler'а и proxy объединяются «в одном флаконе». В любом случае, получателю должен возвращаться указатель на интерфейс, семантически эквивалентный интерфейсу исходного объекта. Proxy, созданный таким образом, волен делать, что угодно, например, он может использовать очередь MSMQ для организации асинхронного взаимодействия, или создать proxy (по данным, запакованным в присланном блоке) для другого интерфейса того же удаленного объекта с целью организации кэширования данных с блочной подчиткой, или для создания локальной копии объекта (в этом случае в блоке данных должно быть запаковано состояние удаленного объекта).

  1. Процесс Х создает объект В как внутрипроцессный сервер для передачи его объекту А через [in]-параметр.
  2. Proxy объекта А производит маршалинг указателя на В для передачи его между процессами, при этом запрашивается интерфейс IMarshal.
  3. Proxy объекта А узнает у В, каков будет размер сообщения об установке соединения (через IMarshal::GetMarshalSizeMax).
  4. Proxy объекта А запрашивает у В CLSID unmarshaler'а (через IMarshal::GetUnmarshalClass) и записывает полученный CLSID в заголовок сообщения.
  5. Proxy объекта А просит у В записать сообщение об установке соединения (IMarshal::MarshalInterface)
  6. Proxy объекта А отсылает сообщения об установке соединения stub'у объекта A. Сообщение посылается внутри буфера содержащего запакованные параметры вызываемого у объекта A метода.
  7. Stub объекта А получает буфер с параметрами вызываемого метода. Распаковывает параметры, извлекая из буфера запрос на установку соединения. Читает CLSID из заголовка и создает unmarshaler для интерфейса объекта В.
  8. Stub объекта А говорит unmarshaler'у инициализировать/подключить proxy (через вызов IMarshal::UnmarshalInterface unmarshaler'а).
  9. Stub объекта А передает указатель на созданный proxy объекта В как in-параметр объекта А.

Рис. 7. Принципиальная схема ручного маршалинга

Чтобы разобраться в этом клубке, посмотрите на Рис. 7. Сперва CoMarshalInterface спросит ваш объект, сколько байт нужно объекту для размещения своей информации в сообщения об установке соединения. Затем COM размещает буфер передачи, достаточный для размещения сообщения (некоторого заголовка и данных объекта). После этого CoMarshalInterface спросит CLSID unmarshaler'а, который будет создан для чтения этого сообщения. CLSID будет послан в заголовке сообщения. Наконец, объекту дадут возможность заполнить необходимый блок данных, который будет помещен в сообщение. Это заполнение происходит через метод MarshalInterface. С помощью его параметров можно узнать: насколько удален получатель (та же машина? тот же процесс?), и маршалинг какого интерфейса производится. Что же помещается в «необходимый блок данных»? А собственно, что угодно. Главное, чтобы unmarshaler смог возвратить получателю нечто осмысленное.

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

При получении буфера в апартаменте получателя код-приемник (возможно, proxy или stub) передаст буфер COM функции CoUnmarshalInterface. Она декодирует пакет и извлечет из него указатель на интерфейс. При этом буфер будет содержать флаг, говорящий, что пакет содержит интерфейс, для которого реализован пользовательский маршалинг. Этот флаг заставит COM считать CLSID (тот самый CLSID, который объект предоставил в своем методе GetUnmarshalClass) и создать unmarshaler, знающий, как читать присланное сообщение. Unmarshaler создается с помощью CoCreateInstance, то есть это обычный COM-объект. У этого объекта (Unmarshaler'а) будет вызван метод IMarshal::UnmarshalInterface, через параметры которого он получит необходимые данные. Unmarshaler должен считать и проанализировать присланные данные, и возвратить необходимый интерфейс (возможно, попутно создав proxy-объект).

Ручной маршалинг и COM+/MTS

В документации по COM+ и MTS сказано, что эти технологии поддерживают только стандартный маршалинг. Приплыли?! Такая информация может испугать кого угодно. Может сложиться впечатление, что все разговоры о ручном маршалинге DCOM-программистам ни к чему, так как дело идет к тому, что COM+ станет доминирующей платформой для создания серверных приложений. Но не стоит пугаться. Хотя сами интерфейсы, опубликованные в COM+/MTS-каталоге, действительно не поддерживают ручной маршалинг (при их маршалинге попросту не запрашивается IMarshal), маршалинг указателей на интерфейсы других объектов, передаваемых как параметры методов COM+/MTS-объектов, производится обычным образом, а значит, ручной маршалинг прекрасно поддерживается.

Маршалинг COM-объектов по значению (MBV)

Иногда возникает потребность передать объект на другую машину (в другой процесс). Ни COM, ни языки программирования, поддерживающие COM (за исключением Java), не предоставляют такой возможности. Лобовым решением этой проблемы является пересылка состояния объекта через параметры метода какого-то COM-объекта, создание на удаленной машине нового экземпляра объекта и инициализация этого объекта присланными данными. О том, как передать по сети набор неструктурированных данных, коими является состояние объекта, будет рассказано в разделе «Оптимизация сетевых вызовов». Хотя ручная передача состояния и последующее воссоздание объекта и решает проблему, назвать такой метод эстетичным нельзя. Да и назвать такой подход «передачей объекта» можно только с некоторой натяжкой.

К счастью, COM предоставляет способ скрыть всю работу, связанную с передачей состояния объектов между компьютерами, создание независимой копии таких объектов и их инициализацию. Этот способ – ручной (custom) маршалинг.

Проблемы стандартного маршалинга

Предположим у вас есть COM-объект, интерфейс которого имеет большое количество свойств и/или методов. Например:

[
  uuid(FACE0B0B-0000-0000-0000-C00000000046), 
  object
]  
interface IComputer : IUnknown
{
  HRESULT GetMake([out, string] OLECHAR **ppwsz);
  HRESULT GetModel([out, string] OLECHAR **ppwsz);
  HRESULT GetClockSpeed([out] long *pn);
  HRESULT GetRamSize([out] long *pn);
  // ...
};

Если использовать этот интерфейс в сценарии стандартного маршалинга, он потребует n клиент-серверных обменов для получения текущего состояния объекта (где n - число процедур Get). Поскольку сетевые вызовы – враг №1 каждого COM-программиста, заботящегося о производительности, было бы целесообразно как-то телепортировать объект в адресное пространство получателя. Тогда все обращения к методам GetXXX стали бы внутрипроцессными вызовами. Это повысило бы общую производительность распределенного приложения за счет уменьшения количества сетевых вызовов.

Конечно, вместо передачи объекта можно передавать структуру с запакованной в нее информацией, но объект может обеспечить дополнительную обработку при обращении к его методам или свойствам.

В COM передаче объекта как параметра ближе всего соответствует передача указателя на один из его интерфейсов. Так как интерфейсы полиморфны, нельзя сделать никаких допущений об их реализации в конкретном объекте. Для передачи не указателя на интерфейс, а копии объекта можно воспользоваться ручным маршалингом (см. Ручной маршалинг (Custom marshaling)).

Помните, при описании ручного маршалинга упоминался блок данных, который COM предлагает заполнить объекту, и который впоследствии посылается unmarshaler'у в буфере сообщения об установке соединения? Это просто произвольный поток байтов (представляемый с помощью интерфейса IStream), который записывается в методе MarshalInterface и может содержать что угодно, нужное для создания семантически корректного указателя в апартаменте получателя.

А теперь давайте задумаемся. Нужно ли нам соединение с удаленным объектом? Правильно, мы пытаемся передать объект по значению, а это подразумевает копирование состояния объекта, а не связь с ним. И еще разок напряжем свои извилины... А если нам не нужна связь с удаленным объектом, то за каким чертом нам нужен proxy? Вместо него можно создать копию настоящего объекта. Собственно, нам не нужен и unmarshaler... Но, похоже, мы переборщили с умозаключениями - ведь нужен нам unmarshaler или нет, нас не спрашивают. Он просто должен быть потому, что этого требует COM.

Еще раз обратимся к разделу, описывающему ручной маршалинг. Там говорится, что unmarshaler должен считать буфер и возвратить получателю указатель на требуемый интерфейс. Интерфейс можно заполучить, создав новый объект, или попросту возвратив указатель на интерфейс, реализуемый самим unmarshaler'ом. Улавливаете? Unmarshaler может быть того же типа, что и объект, подлежащий маршалингу. Другими словами можно заставить COM создать в апартаменте получателя вместо unmarshaler'а копию нашего объекта. COM же поможет нам и проинициализировать новую копию объекта. Этой копии больше не нужна связь с исходным объектом (ведь она же копия), и все её методы будут исполняться в адресном пространстве клиента. Так как это все-таки объект, его методы могут выполнять некоторую обработку, что было бы невозможно при использовании структур и т.п. Как и во многих языках программирования, изменение переданного по значению COM-объекта никак не отражается на самочувствии исходного объекта. Он продолжает жить своей жизнью – возможно, на другом конце света. Разрывается при этом и зависимость между временем жизни оригинального объекта и указателем на интерфейс, ведь, по нашей логике, теперь мы держим указатель не на оригинальный объект, а на его копию.

Остается одна проблема. Легко написать: «сохранить копию объекта..., проинициализировать новую копию...». Если объект прост, то это может быть действительно просто, но что делать, если объект большой и пушистый? Чтобы решить эту проблему, вспомним, где чаще всего возникает потребность в сохранении и последующей загрузке состояния COM-объектов. Правильно, при создании элементов управления ActiveX и OLE-серверов. Для этого в этих технологиях используются стандартные интерфейсы под общим названием IPersistXXX. Обычно применяются или IPersistPropertyBag, или IPersistStream[Init]. IPersistPropertyBag применяется там, где есть необходимость записать состояние объекта в текстовом виде, например, его обычно выбирает VB при сохранении объектов в файле формы (.frm). Но зачем нам текст, не-е, нам кузнец не нужен... А вот IPersistStreamInit нам очень даже подойдет. Нам нужно сохранять состояние в IStream, а это как раз делают интерфейсы IPersistStream и IPersistStreamInit. IPersistStreamInit поддерживают большинство библиотек и средств разработки, от VB до ATL. Причем обычно это делается довольно просто. Например, чтобы заставить ATL-объект реализовать IPersistStreamInit надо:

Давайте сравним IMarshal и IPersistStream! Параллель между IMarshal и IPersistStream поражает. Оба требуют сериализации состояния объекта в IStream. Оба требуют от объекта CLSID. Оба требуют "десериалайзера" для чтения данных из байтового хранилища и восстановления объекта. Посмотрите на Таблицу 3. IPersistStream несколько проще, но реализует все необходимые для реализации IMarshal возможности.

Таблица 3 Сравнение IMarshal и IPersistStream 

IMarshal IPersistStream
HRESULT GetUnmarshalClass(
   REFIID riid, void *pv,
   DWORD dwDestCtx,
   void *pvDestCtx,
   DWORD mshlflags,
   CLSID *pclsid);
HRESULT GetClassID(
   CLSID *pclsid);
HRESULT GetMarshalSizeMax(
   REFIID riid, void *pv,
   DWORD dwDestCtx,
   void *pvDestCtx,
   DWORD mshlflags,
   DWORD *pSize);
HRESULT GetSizeMax(
   ULARGE_INTERGER *pSize);
HRESULT MarshalInterface(
   IStream *pStm,
   REFIID riid, void *pv,
   DWORD dwDestCtx,
   void *pvDestCtx,
   DWORD mshlflags);
HRESULT Save(
   IStream *pStm,
   BOOL bClearDirty);
HRESULT UnmarshalInterface(
   IStream *pStm,
   REFIID riid, void **ppv);
HRESULT Load(IStream *pStm);
HRESULT ReleaseMarshalData(
   IStream * pStm);
Нет сравнимых методов
HRESULT DisconnectObject(DWORD); Нет сравнимых методов
No comparable method HRESULT IsDirty(void);

Helper-объект, реализующий MBV-функциональность

Попробуем создать объект, который будет передаваться по значению. На C++ это можно сделать двумя путями. Первый – написать класс-хелпер, который будет специальным образом подключаться к реализации объекта, который надо «маршалить» по значению. Второй - создать хелпер-объект же, но не в виде класса C++, а в виде агрегируемого COM-объекта. Второй способ предпочтительнее потому, что позволяет сэкономить память и использовать такой хелпер-объект в других языках программирования (например, в Borland Delphi). На рисунке 8 показана схема работы объекта MBVHelper, инкапсулирующего MBV-функциональность. Инкапсуляция MBV-функциональности заключается в том, что он предоставляет реализацию интерфейса IMarshal, используя для сериализации реализацию IPersistStream внешнего объекта. Листинг 3 показывает агрегируемый helper-объект, используемый для реализации маршалинга по значению.

Рис. 8. MBVHelper

В пояснениях здесь нуждаются только два момента. Первый – то, что запись состояния объекта производится не только в методе MarshalInterface, но и в методе GetMarshalSizeMax. Если бы программисты, реализующие интерфейсы, всегда полностью реализовали их методы, то трюки с опережающей записью и кэшированием стрима не понадобились бы... Но большинство программистов пытается сэкономить свое время, а не наше. Так, большая часть реализаций IPersistStream[Init] не реализует метод GetSizeMax, попросту возвращая E_NOTIMPL. Наша реализация пробует воспользоваться этим методом, и кэширует состояние в случае неудачи. Второй момент – использование FreeThreaded Marshaler (FTM). MBV-техника нужна при передаче объекта между процессами. Для маршалинга между потоками одного и того же процесса она не нужна. Но маршалинг указателя на интерфейс все же нужен. Для этого можно воспользоваться средствами стандартного маршалера, который можно получить с помощью API-функции CoGetStandardMarshal (именно ей пользуется COM, когда в пришедшем буфере не обнаруживает CLSID proxy). Но заниматься маршалингом указателя на интерфейс объекта, хранящего, по сути, статические данные, не резонно. Лучше воспользоваться FTM'ом. Наша реализация IMarshal определяет, что маршалинг производится для потока, находящегося в том же процессе и, если это так, передает управление FTM'у.

Использование Helper-объекта

Для объекта, уже реализующего IPersistStream, потребуется слегка изменить функциональность QueryInterface. QueryInterface должен возвращать указатель на интерфейс IMarshal объекта MBVHelper. Необходимо также создать сам MBVHelper, передав ему указатель на IUnknown основного объекта, чтобы он агрегировал наш объект. Его создание можно произвести или в момент создания основного (MBV-) объекта, или при первом запросе интерфейса IMarshal. При реализации на ATL второй способ несколько более сложен, так как ATL реализует QueryInterface с помощью статического массива, но позволяет сэкономить память (16 байт) и уменьшить время создания основного объекта. Такая экономия особенно актуальна, если объект может и не передаваться в другой процесс.

В любом случае в основной объект надо добавить переменную, которая будет хранить указатель на MBVHelper:

 CComPtr<IUnknown> m_spUnkMBVHelper;

Перед уничтожением основного объекта в методе FinalRelease, для этого указателя надо вызвать Release:

void FinalRelease()
{
   m_spUnkMBVHelper.Release();
}

Чтобы объект не уничтожился преждевременно, надо добавить:

DECLARE_PROTECT_FINAL_CONSTRUCT()

а чтобы воспользоваться функцией GetControllingUnknown, надо добавить:

DECLARE_GET_CONTROLLING_UNKNOWN()

В случае создания MBVHelper'а в момент создания основного объекта надо добавить COM_INTERFACE_ENTRY_AGGREGATE в COM_MAP основного объекта:

BEGIN_COM_MAP(CMBVObj)
   ...
   COM_INTERFACE_ENTRY_AGGREGATE(IID_IMarshal, m_spUnkMBVHelper.p)
END_COM_MAP()

а в метод FinalConstruct код по созданию MBVHelper'a:

HRESULT FinalConstruct()
{
   HRESULT hr = S_OK;
   hr = CoCreateInstance(
      CLSID_MBVHelper, 
      GetControllingUnknown(), 
      CLSCTX_ALL, 
      IID_IUnknown, 
      (void**)&m_spUnkMBVHelper.p);
      return hr;
}

В случае создания MBVHelper'а в момент первого запроса QueryInterface, надо добавить COM_INTERFACE_ENTRY_FUNC в COM_MAP основного объекта:

BEGIN_COM_MAP(CMBVObj)
   ...
   COM_INTERFACE_ENTRY_FUNC(IID_IMarshal, 0, GetIMarshal)
END_COM_MAP()

и реализовать функцию GetIMarshal следующим образом:

static HRESULT WINAPI GetIMarshal(void* pv, REFIID riid, LPVOID* ppv, DWORD dw)
{
   HRESULT hr = E_NOINTERFACE;
   CMBVObj & MBVObj = *(CMBVObj*)pv;
   if(!MBVObj.m_spUnkMBVHelper)
   {
      hr = CoCreateInstance(
         CLSID_MBVHelper, 
         MBVObj.GetControllingUnknown(), 
         CLSCTX_ALL, 
         IID_IUnknown, 
         (void**)&MBVObj.m_spUnkMBVHelper.p);
      if(FAILED(hr))
         return hr;
   }
   return MBVObj.m_spUnkMBVHelper->QueryInterface(riid, ppv);
}

В коде тестового объекта, который можно взять здесь: ftp.k-press.ru/pub/cs/2000/3/marshalling/MarshalingCode.zip я реализовал оба подхода. Так как на одном объекте они одновременно работать не могут, я воспользовался операторами условной компиляции. Если закомментировать строку:

#define DYNAMIC_IMARSHAL

, то MBVHelper будет создаваться при создании основного объекта, иначе MBVHelper будет создаваться динамически при первом запросе интерфейса IMarshal, то есть в момент, когда COM попытается произвести маршалинг основного объекта в другой контекст.

Листинг 3
Реализация Helper-объекта, предоставляющего реализацию IMarshal для превращения простого объекта, реализующего IPersistStream[Init] в MBV-объект.
// MBVHelper.h : Declaration of the CMBVHelper
#ifndef __MBVHELPER_H_
#define __MBVHELPER_H_

#include "resource.h"       // main symbols

////////////////////////////////////////////////////////////
// CMBVHelper
class ATL_NO_VTABLE CMBVHelper : 
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<CMBVHelper, &CLSID_MBVHelper>,
// Наш класс поддерживает IMarshal для реализации MBV
public IMarshal
{
public:
   CMBVHelper() : m_cbMax(0) {}
   HRESULT FinalConstruct()
   {
      // Создаем FreeThreadedMarshaler (FTM).
      // Ему мы передадим управление в случае, если требуется 
      // маршалинг между потоками одного и того же процесса.
      // Учтите, что в реальных условиях все данные этого 
      // объекта надо защищать от параллельного доступа,
      // так как объекты, реализующие FTM, игнорируют 
      // апартаментную модель...
      HRESULT hr = CoCreateFreeThreadedMarshaler(
         GetControllingUnknown(), &m_spUnkFTM.p);
      return S_OK;
   }
   void FinalRelease() 
   // FTM должен быть уничтожен перед уничтожением объекта.
   {m_spUnkFTM.Release();}
   
DECLARE_REGISTRY_RESOURCEID(IDR_MBVHELPER)
// Следующие два макроса очень важны!
DECLARE_GET_CONTROLLING_UNKNOWN()
DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CMBVHelper)
   COM_INTERFACE_ENTRY(IMarshal)
END_COM_MAP()
      
public:
   HRESULT GetIPersistStream(IPersistStream **ppIPersistStream)
   {// Эта рабочая функция получает указатель на IPersistStream
    // от основного объекта.
      IUnknown * pIUnknown = GetControllingUnknown();
      HRESULT hr = pIUnknown->QueryInterface(
         IID_IPersistStream, (void**)ppIPersistStream);
      // Если объект не реализует IPersistStream, то пытаемся
      // получить IPersistStreamInit (для нас они идентичны).
      if(FAILED(hr))
         hr = pIUnknown->QueryInterface(IID_IPersistStreamInit, 
         (void**)ppIPersistStream);
      return hr;
   }
   
   ////////////////////////////////////
   // IMarshal
   
   // Этот метод должен возвратить CLSID unmarshaler'а. Для нас
   // он по совместительству является и proxy, и самим MBV
   // Так что мы просто переадресуем этот вызов методу
   // IPersistStream[Init]::GetClassID(...) MBV-объекта.
   STDMETHODIMP GetUnmarshalClass(REFIID riid, void *pv, 
      DWORD dwDestContext, void *pvDestContext, 
      DWORD mshlflags, CLSID *pCid)
   {
      HRESULT hr = S_OK;
      if(dwDestContext == MSHCTX_INPROC)
      {// Если маршалинг в пределах одного процесса, то 
       // передаем управление (FTM).
         CComPtr<IMarshal> pMsh;
         HRESULT hr = m_spUnkFTM->QueryInterface(&pMsh);
         if(SUCCEEDED(hr))
            return pMsh->GetUnmarshalClass(riid, pv, 
                            dwDestContext, pvDestContext, mshlflags, pCid);
      }
      CComPtr<IPersistStream> spIPersistStm;
      hr = GetIPersistStream(&spIPersistStm);
      if(FAILED(hr))
         return hr;
      return spIPersistStm->GetClassID(pCid);
   }
   
   // Это самый неудобный метод. В нем мы должны указать, 
   // сколько памяти нужно для размещения необходимых нам
   // данных. Но на этом этапе нам еще это неизвестно,
   // так как большинство реализаций 
   // IPersistStream[Init]::GetSizeMax возвращают E_NOTOMPL.
   // По этому мы подстраховываемся и для "недоделанных"
   // реализаций заранее сохраняем состояние объекта и узнаем
   // необходимый размер. Естественно, нам приходится кэшировать
   // записанный буфер.
   STDMETHODIMP GetMarshalSizeMax(REFIID riid, void *pv, 
      DWORD dwDestContext, void *pvDestContext, 
      DWORD mshlflags, DWORD *pSize)
   {
      HRESULT hr = S_OK;
      if(dwDestContext == MSHCTX_INPROC)
      {// Если маршалинг в пределах одного процесса, то 
       // передаем управление (FTM).
         CComPtr<IMarshal> pMsh;
         HRESULT hr = m_spUnkFTM->QueryInterface(&pMsh);
         if (SUCCEEDED(hr))
            return pMsh->GetMarshalSizeMax(riid, pv, 
                            dwDestContext, pvDestContext, mshlflags, pSize);
      }
      // Реализация маршалера ОС (по крайней мере в W2k) очень
      // забывчива и имеет склонность несколько раз 
      // переспрашивать размер буфера. В таком случае просто 
      // возвращаем кэшированный размер.
      if(m_cbMax && m_spIStream)
      {
         *pSize = m_cbMax;
         return S_OK;
      }
      m_spIStream.Release();
      CComPtr<IPersistStream> spIPersistStm;
      hr = GetIPersistStream(&spIPersistStm);
      if(FAILED(hr))
         return hr;
      
      // Пытаемся получить размер у реализации 
      // IPersistStream[Init] основного объекта...
      __int64 cbMax;
      hr = spIPersistStm->GetSizeMax((ULARGE_INTEGER*)&cbMax);
      if (SUCCEEDED(hr))
         *pSize = (DWORD)cbMax;
      else
      {
         // ...и если не получается, то производим запись 
         // состояния во временный IStream.
         hr = CreateStreamOnHGlobal(NULL, TRUE, &m_spIStream);
         if(FAILED(hr))
            return hr;
         hr = spIPersistStm->Save(m_spIStream, TRUE);
         LARGE_INTEGER iLNull = {0, 0};
         ULARGE_INTEGER uLLen = {0, 0};
         // Узнаем размер записанных данных...
         hr = m_spIStream->Seek(iLNull, 
            STREAM_SEEK_CUR, &uLLen);
         if(FAILED(hr))
            return hr;
         // Перематываем IStream в начало...
         hr = m_spIStream->Seek(iLNull, STREAM_SEEK_SET, NULL);
         if(FAILED(hr))
            return hr;
         // Запоминаем размер буфера...
         *pSize = m_cbMax = (DWORD)uLLen.QuadPart;
      }
      return hr;    
   }
   
   // А вот здесь-то и надо было бы по-хорошему производить
   // запись, но жизнь бывает непредсказуема...(см. коментарий
   // к предыдущему методу).
   STDMETHODIMP MarshalInterface(IStream *pStm, REFIID riid, 
      void *pv, DWORD dwDestContext, 
      void *pvDestContext, DWORD mshlflags)
   {    
      HRESULT hr = S_OK;
      if(dwDestContext == MSHCTX_INPROC)
      {// Если маршалинг в пределах одного процесса, то 
       // передаем управление (FTM).
         CComPtr<IMarshal> pMsh;
         HRESULT hr = m_spUnkFTM->QueryInterface(&pMsh);
         if (SUCCEEDED(hr))
            return pMsh->MarshalInterface(pStm, riid, pv, 
                                dwDestContext, pvDestContext, mshlflags);
      }
      if(!pStm)
         return E_INVALIDARG;
      // Если промежуточного кэширования не производилось, 
      // попалась нормальная реализация IPersistStream[Init]
      // (что встречается очень редко), то сохраняем состояние
      // в IStream, переданный как параметр метода...
      if(!m_spIStream)
      {
         CComPtr<IPersistStream> spIPersistStm;
         hr = GetIPersistStream(&spIPersistStm);
         if(FAILED(hr))
            return hr;
         hr = spIPersistStm->Save(pStm, TRUE);
      }
      else
      {  // Иначе копируем данные из промежуточного stream'а...
         ULARGE_INTEGER cbMax;
         cbMax.QuadPart = m_cbMax;
         hr = m_spIStream->CopyTo(pStm, cbMax, NULL, NULL);
         if(FAILED(hr))
            return hr;
         m_spIStream.Release();
         m_cbMax = 0;
      }
      return hr;
   }
   
   // Этот метод будет вызван у объекта, когда он будет создан
   // в контексте получателя. В этом методе надо 
   // проинициализировать новую копию присланным состоянием.
   STDMETHODIMP UnmarshalInterface(IStream *pStm, REFIID riid, 
      void **ppv)
   {
      *ppv = NULL;
      if(!pStm || !ppv)
         return E_POINTER;
      CComPtr<IPersistStream> spIPersistStm;
      HRESULT hr = GetIPersistStream(&spIPersistStm);
      if(FAILED(hr))
         return hr;
      hr = spIPersistStm->Load(pStm);
      if(FAILED(hr))
         return hr;
      return GetControllingUnknown()->QueryInterface(riid,ppv);
   }
   
   STDMETHODIMP ReleaseMarshalData(IStream *pStm)
   {return E_NOTIMPL;}
   STDMETHODIMP DisconnectObject(DWORD dwReserved)
   {return S_OK;}
   
public:
   // Указатель на FreeThreadedMarshaler.
   CComPtr<IUnknown> m_spUnkFTM;
   // m_spIStream используется для временного хранения 
   // состояния объекта, если IPersistStream[Init]::GetSizeMax 
   // основного объекта возвращают E_NOTOMPL.
   CComPtr<IStream> m_spIStream;
   // А эта переменная хранит размер данных, записанный 
   // в m_spIStream
   DWORD m_cbMax;
};
#endif //__MBVHELPER_H_

Чтобы продемонстрировать функциональность MBV, я создал тестовый объект MBVObj. Он предоставляет информацию об идентификаторах (ID) процессов, в которых он обрабатывался. Этот объект имеет два метода и одно свойство. Метод GetCurrentProcessId возвращает ID процесса, в котором в настоящее время исполняется объект. GetEarliestProcessId возвращает ID процесса, в котором объект был создан. Свойство AssignedProcessId позволяющее задать произвольный ID процесса в любой момент времени.

Нетрудно догадаться, что значения метода GetEarliestProcessId и свойства AssignedProcessId будут «ездить» между процессами, а GetCurrentProcessId получает информацию о процессе динамически.

Но, чтобы продемонстрировать функциональность MBVObj на практике, надо создать еще один объект, который будет передавать наш MBV-объект между процессами. Назовем его MBVProxy. У него будет два метода. Первый:

HRESULT GetMBVObj([out, retval]IMBVObj ** ppMBV);

создающий MBV-объект и возвращающий его через [out, retval]-параметр и второй:

HRESULT InOutMBVObj([in, out]IMBVObj ** ppMBV);

Он принимает MBV-объект, устанавливает значение свойство AssignedProcessId, присваивая ему ID текущего процесса, и возвращает MBV-объект обратно клиенту. Это объект должен быть зарегистрирован в COM+ или MTS, иначе он будет загружаться в том же процессе.

Все три объекта помещены в трех различных проектах MS VC++ 6.0, объединенных одним Workspace'ом. Полные исходные тексты этих проектов можно найти на нашем сайте www.k-press.ru в электронной версии этой статьи.

Но это еще не все. Нам нужен еще один проект, который создал бы удаленный объект и вызвал у него методы GetMBVObj и InOutMBVObj. Это удобнее сделать на VB. Вот текст этого приложения:

Dim prxy As MBVTESTLib.MBVProxy
Private Sub ShowInfo(tb As TextBox, mbv As MBVLib.MBVObj)
    tb = "ID процесса в котором объект был создан = " & mbv.GetEarliestProcessId & vbCrLf & _
    "ID текущего процесса = " & mbv.GetCurrentProcessId & vbCrLf & _
    "AssignedProcessId = " & mbv.AssignedProcessId
End Sub
Private Sub Command1_Click()
    Dim mbv As MBVLib.MBVObj
    Set mbv = prxy.GetMBVObj
    ShowInfo Text1, mbv
End Sub

Private Sub Command2_Click()
    Dim mbv As New MBVLib.MBVObj
    mbv.AssignedProcessId = GetCurrentProcessId()
    prxy.InOutMBVObj mbv
    ShowInfo Text2, mbv
End Sub

Private Sub Form_Load()
    Set prxy = New MBVTESTLib.MBVProxy
    Command1_Click
    Command2_Click
End Sub

А вот его внешний вид после запуска на выполнение:

Как видите, объект создаваемый на сервере (возвращаемый методом GetMBVObj), имеет ID процесса, отличный от клиентского. Объект, передаваемый через [In, out] параметр (метод InOutMBVObj) проносит свое состояние сквозь границы процессов от клиентского к серверному и обратно. На сервере в методе InOutMBVObj свойству AssignedProcessId присваивается значение ID серверного процесса. Отличие AssignedProcessId от ID процессов, возвращаемых методами GetCurrentProcessId и GetEarliestProcessId, подтверждает, что объект совершил виртуальное путешествие между процессами.

Последнее замечание касается тех пытливых умов, что захотят лицезреть происходящее под отладчиком. Не забудьте, что MBV-объект работает в адресных пространствах сразу двух процессов! Чтобы увидеть, что творится в клиентском процессе, надо в качестве отлаживаемого процесса поставить VB.EXE или EXE-модуль скомпилированного тестового VB-приложения. С отладкой серверной стороны немного сложней. В качестве отлаживаемого процесса надо поставить dllhost.exe для COM+, или mtx.exe для MTS (не забудьте добавить полный путь!). В качестве параметра для COM+ надо задать «/ProcessID:{<Application GUID>}», где < Application GUID> - это GUID приложения, в который был помещен объект MBVProxy. Для MTS вместо «/ProcessID» надо задать «/p» и соответствующий GUID или имя package’а. Перед запуском на отладку серверного процесса надо убедиться, что он уже не запущен в нормальном режиме (без отладчика). Для этого надо сделать «Shut down» COM+/MTS приложению. После этих нехитрых действий просто ставьте точку прерывания, нажимайте F5, запускайте тестовое VB-приложение и любуйтесь.

Создание универсального MBV-helper-объекта

Все здорово в созданном нами helper-объекте, но есть одна проблема. Дело в том, что VB не поддерживает агрегацию. Создается впечатление, что писавшие его люди или вовсе не знали о существовании такой замечательной функции, или пытались защитить «неопытных» VB-программистов от этого «вселенского зла». В любом случае, легальных путей обойти это ограничение не существует. Конечно, можно было бы плюнуть (лично мне ближе C++), но давайте посмотрим правде в глаза, этот мир - мир Visual Basic-программистов, а мы в нем - только случайные посетители...

До этого мы предполагали подключать MBV-функциональность с помощью агрегации, но для расширения функциональности готового объекта COM поддерживает не только агрегацию, но и делегацию. Надеюсь, все понимают принципы делегации. Да-да, нудное, скучное обертывание одного объекта другим. Для реализации MBV-функциональности с помощью делегации вам надо будет повторить все интерфейсы исходного объекта, добавив реализацию IMarshal. Лично меня перспектива проделывать это для каждого объекта не вдохновляет. К счастью, есть на свете люди с пытливым умом. Так, в MSJ (теперь MSDN magazine) за январь 1999 года была опубликована статья Кейза Брауна (Keith Brown) о создании простого COM-перехватчика. Его работа базируется на идее, что можно построить перехватывающий слой поверх существующего COM-объекта. Этот слой может делегировать все вызовы нижележащему объекту — со знанием (или без знания) сигнатур нижележащего метода. Это становится возможным благодаря тому, что практически все COM-объекты используют в качестве соглашения вызова метода __stdcall. В соответствии со спецификацией __stdcall, указатель на this всегда доступен по определенному смещению в стеке. Неважно, сколько параметров может иметь данный метод.

Рис. 9 Структура COM-вызова в стеке

Рис. 9 показывает типичную структуру COM-вызова в стеке. Заметьте, указатель на интерфейс всегда хранится в четырех байтах от вершины стека (прямо под адресом возврата). Дон Бокс (Don Box) – другой автор вышеупомянутого журнала – взяв за основу идею Кейза Брауна, предложил создать так называемый слепой делегатор, объект, перехватывающий все вызовы к методам интерфейса некоторого объекта. Большинство из вызовов слепо делегируются методам реального объекта, но некоторые, например QueryInterface, обрабатываются по-своему. Короче говоря, клиенту выставляются не сам объект, а объект «слепой делегатор», который производит автоматическую делегацию. Заметьте, что при этом не надо каждый раз вручную создавать код делегации (эту задачу берет на себя универсальный код). К тому же слепая делегация имеет очень высокую производительность, так как слепой делегатов делает всего несколько обращений к памяти, не трогая параметры, находящиеся в стеке.

Слепая делегация основана на замещении в стеке указателя делегирующего объекта указателем на интерфейс реального объекта с последующей передачей управления в его метод. Указатель на интерфейс исходного объекта хранится по заранее известному смещению относительно указателя на интерфейс слепого делегатора. Слепо делегируются не все вызовы. Вызовы методов IUnknown::XXX перехватываются и обрабатываются особым образом.

Рис. 10 Схема функционирования слепого делегатора

На Рис. 10 показана схема функционирования слепого делегатора. Обратите внимание, что каждое вхождение vtable для слепого делегатора указывает на соответствующий метод-заглушку:

#define METHODTHUNK(n) \
   void __declspec(naked) methodthunk##n() \
   { \
      _asm { mov ecx, (n * SZPFN) } \
      _asm { jmp Thunk } \
   }

состоящий, по сути, из двух ассемблерных команд. Первая из них заносит в регистр ECX смещение метода, а вторая передает управление универсальному обработчику, перенаправляющему вызов методу реального объекта (см Рис. 10). Этот универсальный обработчик представляет собой следующую ассемблерную заглушку:

void __declspec(naked) Thunk()
{
   __asm
   {
      // BD - Blind Delegator (слепой делегатор)
      // По смещению (esp + 4) хранится BD::this
      // eax = BD::this 
      mov eax, DWORD PTR [esp + 4]
      // eax = BD::this->m_pUnkRealItf
      mov eax, DWORD PTR [eax + 4]
      // заменяем this (указатель на BD) в стеке
      // на this реального объекта (находящийся в eax)
      mov [esp + 4], eax
      // помещаем указатель на vtable (базу) в eax
      mov eax, DWORD PTR [eax]
      // добавляем смещение для вызываемого метода
      // к базе находящейся в eax (ecx содержит 
      // смещение текущего метода)
      add eax, ecx
      // переходим к методу реального объекта

      jmp DWORD PTR [eax]
   }
}

В сущности, это тот же ассемблерный код, что и в /Oicf proxy. __declspec(naked) говорит компилятору, что для функции не надо генерировать стандартный пролог/эпилог-код. Это гарантирует сохранность значений регистров в промежутке между вызовом функции и началом работы первой ассемблерной команды.

Для соответствия законам тождественности COM, методы IUnknown слепых делегаторов должны переадресовываться общим реализациям QueryInterface, AddRef и Release. Листинг 4 содержит исходный код BlindDelegator'а предложенного Don Box-ом.

Листинг 4
Реализация слепого делегатора
BlindDel.h
#ifndef _BLIND_DELEGATOR_H
#define _BLIND_DELEGATOR_H

enum { MAX_VTABLE_ENTRIES  = 1024 };
typedef void (_stdcall *VTBL_ENTRY)();
typedef VTBL_ENTRY VTABLE[MAX_VTABLE_ENTRIES];

extern VTABLE g_bdvtbl;

struct BlindDelegator
{
   VTABLE                 *m_pVTable;
   IUnknown               *m_pUnkRealItf;
   IUnknown               *m_pCtlUnk;
   LONG                    m_cRef;
   
   static inline BlindDelegator *This(void *pvThis) 
   { 
      return (BlindDelegator*)
        (LPBYTE(pvThis) - offsetof(BlindDelegator, m_pVTable));
   }
   
   static HRESULT STDMETHODCALLTYPE QueryInterface(
                    IUnknown *pvThis, REFIID riid, void **ppv)
   {
     return This(pvThis)->m_pCtlUnk->QueryInterface(riid, ppv);
   }
   
   static ULONG STDMETHODCALLTYPE AddRef(IUnknown *pvThis)
   {
      return InterlockedIncrement(&This(pvThis)->m_cRef);
   }
   
   static ULONG STDMETHODCALLTYPE Release(IUnknown *pvThis)
   {
      BlindDelegator *pThis = This(pvThis);
      LONG res = InterlockedDecrement(&pThis->m_cRef);
      if(res == 0)
      {
         IUnknown *pCtlUnk = pThis->m_pCtlUnk;
         pThis->m_pUnkRealItf->Release();
         delete pThis;
         res = pCtlUnk->Release();
      }
      return res;
   }
   
   static HRESULT CreateDelegator(IUnknown *pCtlUnk, 
             IUnknown *pUnkDelegatee, REFIID riid, void **ppv)
   {
      IUnknown *pItf = 0;
      *ppv = 0;
      HRESULT hr = pUnkDelegatee->QueryInterface(riid, 
                                               (void**)&pItf);
      if(SUCCEEDED(hr))
      {
         BlindDelegator *pDel = new BlindDelegator;
         if(pDel)
         {
            pDel->m_cRef = 1;
            (pDel->m_pCtlUnk = pCtlUnk)->AddRef();
            pDel->m_pUnkRealItf = pItf;
            pDel->m_pVTable = &g_bdvtbl;
            *ppv = LPBYTE(pDel) + 
                          offsetof(BlindDelegator, m_pVTable);
         }
         else
         {
            pItf->Release();
            hr = E_OUTOFMEMORY;
         }
         
      }
      return hr;
   }
};
#endif
BlindDel.cpp
#include <stdafx.h>
#include "blinddel.h"

void __declspec(naked) Thunk()
{
   __asm
   {
      // BD - Blind Delegator (слепой делегатор)
      // По смещению (esp + 4) хранится BD::this
      // eax = BD::this 
      mov eax, DWORD PTR [esp + 4]
      // eax = BD::this->m_pUnkRealItf
      mov eax, DWORD PTR [eax + 4]
      // заменяем this (указатель на BD) в стеке
      // на this реального объекта (находящийся в eax)
      mov [esp + 4], eax
      // помещаем указатель на vtable (базу) в eax
      mov eax, DWORD PTR [eax]
      // добавляем смещение для вызываемого метода
      // к базе находящейся в eax (ecx содержит 
      // смещение текущего метода)
      add eax, ecx
      // переходим к методу реального объекта
      jmp DWORD PTR [eax]
   }
}

enum { SZPFN = sizeof(void (*)()) };

#define METHODTHUNK(n) \
   void __declspec(naked) methodthunk##n() \
   { \
      _asm { mov ecx, (n * SZPFN) } \
      _asm { jmp Thunk } \
   }

#define METHODTHUNK10(n10) \
    METHODTHUNK(n10##0) \
    METHODTHUNK(n10##1) \
    METHODTHUNK(n10##2) \
    METHODTHUNK(n10##3) \
    METHODTHUNK(n10##4) \
    METHODTHUNK(n10##5) \
    METHODTHUNK(n10##6) \
    METHODTHUNK(n10##7) \
    METHODTHUNK(n10##8) \
    METHODTHUNK(n10##9) \

#define METHODTHUNK100(n100) \
    METHODTHUNK10(n100##0) \
    METHODTHUNK10(n100##1) \
    METHODTHUNK10(n100##2) \
    METHODTHUNK10(n100##3) \
    METHODTHUNK10(n100##4) \
    METHODTHUNK10(n100##5) \
    METHODTHUNK10(n100##6) \
    METHODTHUNK10(n100##7) \
    METHODTHUNK10(n100##8) \
    METHODTHUNK10(n100##9) \

METHODTHUNK(3)
METHODTHUNK(4)
METHODTHUNK(5)
METHODTHUNK(6)
METHODTHUNK(7)
METHODTHUNK(8)
METHODTHUNK(9)
METHODTHUNK10(1)
METHODTHUNK10(2)
METHODTHUNK10(3)
METHODTHUNK10(4)
METHODTHUNK10(5)
METHODTHUNK10(6)
METHODTHUNK10(7)
METHODTHUNK10(8)
METHODTHUNK10(9)
METHODTHUNK100(1)
METHODTHUNK100(2)
METHODTHUNK100(3)
METHODTHUNK100(4)
METHODTHUNK100(5)
METHODTHUNK100(6)
METHODTHUNK100(7)
METHODTHUNK100(8)
METHODTHUNK100(9)
METHODTHUNK10(100)
METHODTHUNK10(101)
METHODTHUNK(1020)
METHODTHUNK(1021)
METHODTHUNK(1022)
METHODTHUNK(1023)


#define UMTHUNK(n) reinterpret_cast<VTBL_ENTRY>(methodthunk##n), 

#define UMTHUNK10(n) \
    UMTHUNK(n##0) \
    UMTHUNK(n##1) \
    UMTHUNK(n##2) \
    UMTHUNK(n##3) \
    UMTHUNK(n##4) \
    UMTHUNK(n##5) \
    UMTHUNK(n##6) \
    UMTHUNK(n##7) \
    UMTHUNK(n##8) \
    UMTHUNK(n##9) \

#define UMTHUNK100(n) \
    UMTHUNK10(n##0) \
    UMTHUNK10(n##1) \
    UMTHUNK10(n##2) \
    UMTHUNK10(n##3) \
    UMTHUNK10(n##4) \
    UMTHUNK10(n##5) \
    UMTHUNK10(n##6) \
    UMTHUNK10(n##7) \
    UMTHUNK10(n##8) \
    UMTHUNK10(n##9) \

VTABLE g_bdvtbl = 
   {
      reinterpret_cast<VTBL_ENTRY>(BlindDelegator::QueryInterface),
      reinterpret_cast<VTBL_ENTRY>(BlindDelegator::AddRef),
      reinterpret_cast<VTBL_ENTRY>(BlindDelegator::Release),
      UMTHUNK(3)
      UMTHUNK(4)
      UMTHUNK(5)
      UMTHUNK(6)
      UMTHUNK(7)
      UMTHUNK(8)
      UMTHUNK(9)
      UMTHUNK10(1)
      UMTHUNK10(2)
      UMTHUNK10(3)
      UMTHUNK10(4)
      UMTHUNK10(5)
      UMTHUNK10(6)
      UMTHUNK10(7)
      UMTHUNK10(8)
      UMTHUNK10(9)
      UMTHUNK100(1)
      UMTHUNK100(2)
      UMTHUNK100(3)
      UMTHUNK100(4)
      UMTHUNK100(5)
      UMTHUNK100(6)
      UMTHUNK100(7)
      UMTHUNK100(8)
      UMTHUNK100(9)
      UMTHUNK10(100)
      UMTHUNK10(101)
      UMTHUNK(1020)
      UMTHUNK(1021)
      UMTHUNK(1022)
      UMTHUNK(1023)
   };

Теперь, когда код слепой делегации есть, нужен только некий объединяющий код – чтобы приклеить делегатор поверх реального (персистентного) объекта. Для этого нам нужен новый объект MBV-делегатор. Рис. 11 показывает схему работы MBV-делегатора. Код MBV будет создавать слепые делегаторы для всех интерфейсов кроме IUnknown и IMarshal. IUnknown всяко должен быть реализован вручную для поддержки законов идентичности, а IMarshal нужно реализовать потому, что вся идея как раз в этом и состоит. Реализация IMarshal – просто переходник к нижележащей реализации IPersistStream реального объекта. Это идентично работе нашего предыдущего варианта. Единственная разница в том, что теперь персистентный объект «псевдо-агрегирован» с MBV-реализацией.


Рис. 11 Схема работы MBV-делегатора

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

Как видно из Рис. 11, MBVDelegator реализует только один интерфейс – IMarshal. Стало быть, для его подключения к персистентному объекту надо создавать MBVDelegator как C++-класс и вручную подключать его к персистентному объекту. Это неудобно в C++ и невозможно в других языках. Поэтому надо создать еще один объект, который будет проделывать эту операцию. Назовем его MBVInit. Этот объект будет реализовывать один интерфейс с двумя методами. Вот описание этого интерфейса:

   interface IMBVInit : IDispatch
   {
      [id(1), helpstring("Создает объект и псевдо-агрегирует его с MBVDelegator'ом")] 
      HRESULT CreateInstance([in] BSTR bstrProgId, 
                   [out, retval] IUnknown **ppUnkNewObject);
      [id(2), helpstring("Псевдо-агрегирует pUnkObj c MBVDelegator'ом")] 
      HRESULT MBVRef([in] IUnknown *pUnkObj, 
                   [out, retval] IUnknown **pvarResult);
   };

Как всегда это бывает, при решении одной проблемы обязательно вылезают другие. Проблемой является то, что псевдо-агрегируемый объект может использовать FTM, а может и стандартный внутрипроцессный маршалинг. Чтобы определить, какой тип маршалинга использует объект, можно запросить у него указатель на IMarshal, и, посредством вызова GetUnmarshalClass, узнать CLSID unmarshaler'а. Но с чем сравнить этот CLSID? Можно, конечно, подсмотреть его в отладчике и создать свою константу, но CLSID может измениться (с новым сервис-паком или новой версией Windows). Поэтому лучше получить его динамически тем же самым способом, но у объекта, наверняка поддерживающего FTM. Для этого был создан псевдо-объект и некоторый дополнительный код:

//////////////////////////////////////////////////
// Следующая кучка кода нужна для выяснения 
// CLSID FreeThreaded Marshaler'а

CLSID CLSID_FreeThreadedMarshaler = CLSID_NULL;
// Класс, эмулирующий COM-объект, агрегирующий 
// FreeThreaded Marshaler
struct Dummy : public IUnknown 
{
   IUnknown *m_pUnkFTM;
   Dummy(void) 
   {
      CoCreateFreeThreadedMarshaler(this,&m_pUnkFTM);
   }
   ~Dummy(void)
   { 
      m_pUnkFTM->Release(); 
   }
   STDMETHODIMP QueryInterface(REFIID r,void **p)
   {
      if(r == IID_IUnknown)
         *p = static_cast<IUnknown*>(this);
      else
         return m_pUnkFTM->QueryInterface(r, p);
      return S_OK;
   }
   STDMETHODIMP_(ULONG) AddRef() { return 4; }
   STDMETHODIMP_(ULONG) Release() { return 16; }
};
// InitFTMCLSID инициализирует переменную 
// CLSID_FreeThreadedMarshaler идентификатором класса 
// FreeThreaded Marshaler'а
void InitFTMCLSID() 
{
   if(CLSID_FreeThreadedMarshaler != CLSID_NULL)
      return;
   IMarshal *pMsh = 0;
   Dummy d;
   HRESULT hr = d.QueryInterface(IID_IMarshal, 
      (void**)&pMsh);
   if(SUCCEEDED(hr)) 
   {
      hr = pMsh->GetUnmarshalClass(IID_IUnknown, &d,
         MSHCTX_INPROC, 0, 
         MSHLFLAGS_NORMAL, 
         &CLSID_FreeThreadedMarshaler);
      pMsh->Release();
   }
}
// IsApartmentNeutral определяет, агрегирует ли объект 
// FreeThreaded Marshaler
BOOL IsApartmentNeutral(IUnknown *pUnk)
{
   BOOL bResult = FALSE;
   IMarshal *pMsh = 0;
   HRESULT hr = pUnk->QueryInterface(IID_IMarshal, 
      (void**)&pMsh);
   if(SUCCEEDED(hr))
   {
      CLSID clsid;
      hr = pMsh->GetUnmarshalClass(IID_IUnknown,
         pUnk,
         MSHCTX_INPROC, 0, 
         MSHLFLAGS_NORMAL,
         &clsid);
      InitFTMCLSID();
      bResult = clsid == CLSID_FreeThreadedMarshaler;
      pMsh->Release();
   }
   return bResult;
}

Главной в этом коде является функция IsApartmentNeutral, она позволяет определить, агрегирует ли объект FreeThreaded Marshaler. Эта функция вызывается из UnmarshalInterface. В случае, если объект поддерживает FTM, то и MBVDelegator использует FTM.

В остальном код MBVDelegator, по существу, напоминает код MBVHelper'а, за исключением следующего фрагмента:

BEGIN_COM_MAP(CMBVDelegator)
    COM_INTERFACE_ENTRY(IMarshal)
    COM_INTERFACE_ENTRY_FUNC_BLIND(0, _BlindDelegate)
END_COM_MAP()

   // Вызывается из internalQI для реализации слепой делегации
   static HRESULT WINAPI _BlindDelegate(void* pvThis, 
                            REFIID riid, void** ppv, DWORD dw)
   {
      CMBVDelegator *pThis = 
                     reinterpret_cast<CMBVDelegator*>(pvThis);
       return BlindDelegator::CreateDelegator(
                             static_cast<IMarshal*>(pThis),
                             pThis->m_ppsDelegatee, riid, ppv);
   }

MBVDelegator использует BlindDelegator для псевдо-агрегации реального объекта.

Еще одной проблемой стало то, что при попытке передать по значению объект, реализованный с помощью VB, из кода, написанного на VBScript, объект выдавал ошибку. Это связано с тем, что у объекта после его создания и перед вызовом IPersistStreamInit::Save не был вызван IPersistStreamInit::InitNew. Некоторые реализации (например, MS JVM) склонны к всепрощению и позволяют вызвать IPersistStreamInit::Save, не вызывая сперва InitNew или Load. Увы, реализация IPersistStream::Save в VB приходит в жуткое расстройство, если объект сначала не получит вызова метода IPersistStreamInit::InitNew. Как ни странно, сам VB вызывает InitNew при создании объекта с помощью оператора New или встроенной функции CreateObject. Однако VBScript этого не делает, что означает – хотите маршалить VB-объект по значению из VBScript, вызовите сначала InitNew, а потом уж передавайте объект как параметр. Естественно, что сделать это напрямую из VBScript невозможно, поэтому объект MBVInit имеет метод CreateInstance, который создает новый экземпляр объекта и, если объект поддерживает IPersistStreamInit, вызывает InitNew. Так что, если вам надо создавать объект в VBScript, то воспользуйтесь методом CreateInstance объекта MBVInit. В самом VB можно пользоваться методом MBVRef, который просто производит псевдо-агрегацию и не производит никакой инициализации.

К сожалению, VB-проекты нельзя объединять в группу, так как VB при этом начинает запускать все отлаживаемые компоненты, расположенные внутри DLL, в адресном пространстве исполняемого файла, игнорируя регистрацию в COM+.

Я тестировал все объекты с помощью COM+. Если вы не хотите мучиться с ручной компиляцией, регистрацией, и используете COM+, то можно просто зарегистрировать уже скомпилированные объекты и проинсталлировать готовое COM+-приложение. DLL и инсталлятор COM+-приложения находятся в подкаталоге Bin. Для упрощения процедуры установки можно воспользоваться batch-файлом – SetUp.bat. Если вы еще не работаете с главной проблемой двухтысячного года (W2k), то вам придется произвести установку вручную. Я не тестировал работоспособность компонентов под MTS, но надеюсь, что все будет работать. Если вы проведете эксперимент с MTS, то я буду признателен вам за сообщение о его результатах. Сообщение можно прислать по электронной почте на адрес mag@rsdn.ru с пометкой Marshaling. Сюда же можно присылать свои предложения, пожелания, и даже рецензии.

Оптимизация сетевых вызовов

При передаче объекта по значению (см. «Маршалинга COM-объектов по значению (MBV)») переданный объект теряет всякую связь с исходным. При передаче указателя на интерфейс средствами стандартного маршалинга создается связь с удаленным объектом. А если нужно создать объект-заглушку на клиентской стороне, которая бы имела связь с удаленным объектом? Такая ситуация нередко возникает, когда необходимо промежуточное кэширование данных на клиентской стороне, или когда хочется упростить программный интерфейс серверного объекта.

Представьте, что перед вами стоит задача передачи по сети большого массива данных (списка файлов большой директории, выборки, сделанной на таблице, содержащей большое количество строк и т.п.). Как вы помните, главными врагами программиста распределенных приложений являются объем передаваемых данных и частота сетевых вызовов. С точки зрения производительности выгоднее передавать данные за меньшее количество сетевых вызовов. Но передача данных может быть очень долгой, что при выполнении этого вызова в исходном потоке негативно скажется на поведении пользовательского интерфейса. Зачастую все данные сразу не нужны. Например, при выводе длинного списка пользователю можно отображать первые десять строк, а остальные докачивать при нажатии пользователем PgDown или во время простоя системы. При этом не лишним было бы отображать прогресс поступления данных.

Реализация этой функциональности приводит к тому, что интерфейс с серверным объектом становится неудобным в силу своей низкоуровневости. На клиентской стороне удобней пользоваться объектами, реализующими простой навигационный стиль работы или представляющими данные в виде единого, плоского массива. Как вы сами понимаете, в данном случае красота входит в противоречие с производительностью и функциональностью. Для упрощения интерфейса можно было бы воспользоваться MBV, передать все данные в одном буфере, а на клиентской стороне с помощью proxy-объекта эмулировать доступ в простом, навигационном стиле, но, как говорилось ранее, при передаче больших объемов данных пользователь может попросту подумать, что программа «зависла» и снять ее.

Более грамотным решением было бы создать объект, который, как и MBV-объект, позволял бы передавать данные в момент передачи указателя на интерфейс, но в отличие от MBV-объекта не разрывал бы связь с удаленным объектом. Такой объект мог бы посылать данные пакетами, передавая первый пакет в момент установления связи, а остальные – по мере необходимости.

Как говорилось при обсуждении ручного маршалинга, proxy-объект может быть любым COM-объектом. Proxy-объект можно использовать в качестве посредника между клиентом и серверным объектом. При этом proxy скроет все тонкости взаимодействия с серверным объектом и позволит упростить программную модель. Но как серверный объект с небольшим набором низкоуровневых методов превратится в белый и пушистый клиентский объект, содержащий много простых методов и свойств?

Осуществить чудесное превращение из гадкого работящего утенка в белого и пушистого лебедя-трутня поможет великий и могучий волшебник QueryInterface. Метод серверного объекта, возвращающий указатель на объект, должен работать не с низкоуровневым интерфейсом серверного объекта, а с интерфейсом, реализуемым клиентским объектом. Основная хитрость в том, что серверный объект вовсе не обязан полностью реализовывать этот интерфейс. Объекту надо реализовать ручной маршалинг для этого интерфейса, и обеспечить, чтобы при запросе у него (через QueryInterface) клиентского интерфейса возвращался бы некоторый интерфейс. Добиться этого можно или реализовав интерфейс, возвращающий из всех методов E_NOTIMPL, или попросту «перемапив» его на другой интерфейс этого же объекта. Главное, чтобы интерфейс содержал корректную реализацию IUnknown. Эмуляция интерфейса путем «перемапливания» – не лучший выбор, но и этот путь сойдет, если вам невмоготу написать лишний десяток строк, и вы уверены, что никому не придет в голову вызвать какой-нибудь метод клиентского интерфейса у серверного объекта. Но если таких гарантий нет, лучше перестраховаться и сделать заглушечную реализацию. Тем более, что с применением средств, входящих в VC, это довольно просто – компилируете IDL и выбираете из контекстного меню класса пункт «Implement Interface...».

В остальном техника очень проста. Вы создаете копию такого «умного» объекта, запрашиваете у него указатель на интерфейс клиентского объекта и возвращаете этот указатель через [out, retval]-параметр метода серверного объекта. Естественно, этот параметр должен быть объявлен как указатель на интерфейс клиентского объекта. Далее COM запрашивает у вашего объекта указатель на IMarshal (пытаясь таким образом выяснить, поддерживает ли объект ручной маршалинг). Получив этот указатель, COM пытается инициировать процедуру ручного маршалинга. При этом он начинает поочередно вызывать методы интерфейса IMarshal (подробности можно обновить в своей памяти, перечитав раздел «Ручной маршалинг (Custom marshaling)»). Вот тут то и начинается самое интересное...

Реализация IMarshal нашего объекта должна вместо маршалинга клиентского интерфейса передать первый блок данных и указатель на низкоуровневый (серверный) интерфейс. Передавать буфер мы уже умеем, но как же быть с указателем на интерфейс – мы не можем передать его средствами ручного маршалинга? Не можем, и ладно. Воспользуемся услугами стандартного маршалинга. Реализация ручного маршалинга не запрещает нам вручную вызывать функции, производящие стандартный маршалинг интерфейсов. Чтобы сделать это, сначала надо получить системный объект, реализующий этот самый стандартный маршалинг. Это можно сделать с помощью API-функции CoGetStandardMarshal. Именно ее вызывает COM, производя стандартный маршалинг. Эта функция получает набор параметров, сходный с параметрами IMarshal::GetMarshalSizeMax, IMarshal::MarshalInterface, IMarshal::GetUnmarshalClass, так что ее очень удобно вызывать из этих методов нашей реализации IMarshal. Она возвращает указатель на IMarshal стандартного маршалера. Одним из параметров CoGetStandardMarshal является riid – IID интерфейса, подлежащего маршалингу. С помощью IMarshal стандартного маршалера мы можем превратить указатель на интерфейс в описывающий его stream (блоб). Записав такое описание вместе с первым блоком данных в stream, передаваемый нам COM'ом в реализуемый нами метод MarshalInterface, мы можем передать его в proxy-объект который преобразует (с помощью того же стандартного маршалера) бинарное описание в указатель на реальный интерфейс. Напомню, что это указатель на низкоуровневый (серверный) интерфейс. Наша реализация IMarshal::GetUnmarshalClass должна возвратить CLSID proxy-объекта, когда у нее запросят CLSID unmarshaler'а для клиенского интерфейса. Если происходит маршалинг другого интерфейса (например, низкоуровневого), то наша реализация IMarshal должна просто переадресовывать вызовы IMarshal'у стандартного маршалера.

На клиентской стороне COM получит буфер, определит, что происходит ручной маршалинг, распакует CLSID unmarshaler'а (нашего proxy-объекта), создаст наш proxy-объект и вызовет у него метод IMarshal::UnmarshalInterface, передав ему stream с первым буфером данных и запакованным указателем на низкоуровневый интерфейс.

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

Я создал пример, демонстрирующий принципы работы серверного объекта с «умным» proxy. Этот пример демонстрирует передачу списка файлов (и директорий) для заданной директории. Всего в примере три объекта. Первый, RemoteFileBrowser, реализует интерфейс ILowLevelFilesBrowse:

interface ILowLevelFilesBrowse : IUnknown
{
    HRESULT Path([out, retval] BSTR *pVal);
    HRESULT Path([in] BSTR newVal);
    HRESULT GetFirstFiles([in] int BufSize, 
       [out] SAFEARRAY(SimpleFileInfo) * FilesBuf);
    HRESULT GetNextFiles([in] int BufSize, 
       [out] SAFEARRAY(SimpleFileInfo) * FilesBuf);
};

предназначенный для получения списка файлов пакетами, размер которых определяется клиентом (параметр BufSize у методов GetFirstFiles и GetNextFiles). Свойство Path позволяет задать путь для директории, список файлов которой будет передаваться клиенту. Метод GetNextFiles позволяет получить следующий пакет, содержащий список файлов. Пакет передается в виде массива структур (структура будет описана ниже). GetFirstFiles позволяет начать выборку с начала.

Основной код, ради которого и затеян весь сыр-бор – это реализация интерфейса IMarshal:

// Этот метод должен возвратить CLSID unmarshaler'a.
STDMETHODIMP GetUnmarshalClass(REFIID riid, void *pv, 
   DWORD dwDestContext, void *pvDestContext, 
   DWORD mshlflags, CLSID *pCid)
{
   HRESULT hr = S_OK;
   if(dwDestContext == MSHCTX_INPROC 
      || riid != IID_ISmartFileBrowser)
   {
      CComPtr<IMarshal> spStdMsh;
      hr = CoGetStandardMarshal(riid, (IUnknown*)pv, 
         dwDestContext, pvDestContext, mshlflags, &spStdMsh);
      hr = spStdMsh->GetUnmarshalClass(riid, pv, dwDestContext, 
         pvDestContext, mshlflags, pCid);
      return hr;
   }
   if(!pCid)
      return E_POINTER;
   *pCid = CLSID_SmartFileBrowser;
   return S_OK;
}

STDMETHODIMP GetMarshalSizeMax(REFIID riid, void *pv, 
   DWORD dwDestContext, void *pvDestContext, 
   DWORD mshlflags, DWORD *pSize)
{
   HRESULT hr = S_OK;
   DWORD dwSize = 0;
   CComPtr<IMarshal> spStdMsh;
   hr = CoGetStandardMarshal(riid, (IUnknown*)pv, 
      dwDestContext, pvDestContext, mshlflags, &spStdMsh);
   if(dwDestContext == MSHCTX_INPROC 
      || riid != IID_ISmartFileBrowser)
   {
      hr = spStdMsh->GetMarshalSizeMax(riid, pv, 
         dwDestContext, pvDestContext, 
         mshlflags, pSize);
   }
   else
   {
      hr = spStdMsh->GetMarshalSizeMax(IID_ILowLevelFilesBrowse, 
         pv, dwDestContext, pvDestContext, mshlflags, &dwSize);
       // Aey iaoeo aaiiuo...
      *pSize = dwSize + ciFilesInFirstBuf * 
         (sizeof(long) + sizeof(long) + _MAX_FNAME);
   }
   return hr;
}

STDMETHODIMP MarshalInterface(IStream *pStm, REFIID riid, 
   void *pv, DWORD dwDestContext, 
   void *pvDestContext, DWORD mshlflags)
{    
   HRESULT hr = S_OK;
   CComPtr<IMarshal> spStdMsh;
   hr = CoGetStandardMarshal(riid, (IUnknown*)pv, 
      dwDestContext, pvDestContext, mshlflags, &spStdMsh);
   if(dwDestContext == MSHCTX_INPROC 
      || riid != IID_ISmartFileBrowser)
   {
      hr = spStdMsh->MarshalInterface(pStm, riid, pv, 
         dwDestContext, pvDestContext, mshlflags);
   }
   else
   {
      // Производим маршалинг ILowLevelFilesBrowse a stream...
      hr = spStdMsh->MarshalInterface(pStm, 
         IID_ILowLevelFilesBrowse, pv, dwDestContext, 
         pvDestContext, mshlflags);
      if(FAILED(hr))
         return hr;
      // Получаем и записываем первый буфер в тот же stream.
      SAFEARRAY * psa = NULL;
      hr = GetFirstFiles(ciFilesInFirstBuf, &psa);
      if(SUCCEEDED(hr))
      {
         SimpleFileInfo * sfi = NULL;
         hr = SafeArrayAccessData(psa, (void**)&sfi);
         if(FAILED(hr))
            return hr;
         hr = pStm->Write(&ciFilesInFirstBuf, 
                sizeof(ciFilesInFirstBuf), NULL);
         for(int i = 0; i < ciFilesInFirstBuf; i++)
         {
            hr = pStm->Write(&sfi[i].Flags, 
                             sizeof(sfi[i].Flags), NULL);
            int iLen = ::SysStringLen(sfi[i].bstrFileName);
            hr = pStm->Write(&iLen, sizeof(iLen), NULL);
            if(FAILED(hr)) return hr;
            hr = pStm->Write(sfi[i].bstrFileName, 
                             iLen * sizeof(OLECHAR), NULL);
            if(FAILED(hr)) return hr;
         }
         hr = SafeArrayUnaccessData(psa);
         if(FAILED(hr)) return hr;
         hr = SafeArrayDestroy(psa);
      }
   }
   return hr;
}
// Остальные методы не имеют практического значения...

RemoteFileBrowser должен создаваться на сервере, но он не может быть зарегистрирован в COM+. Чтобы обойти это ограничение (так же, как и в случае с MBV), воспользуемся услугами объекта-посредника SmartFileBrowserProxyVB. Не путайте его с proxy-объектом, создаваемым в клиентском прощессе. SmartFileBrowserProxyVB должен быть зарегистрирован в COM+. Он должен создать серверный объект RemoteFileBrowser, задать ему директорию, откуда надо считывать имена файлов, и возвратить ссылку на него, попутно приведя ее к ссылке на интерфейс ISmartFileBrowser клиентского объекта. Вот код его единственного метода:

Public Function GetRemoteFile-Browser(ByVal Path As String) _
As SmartFileBrowser
    Dim obj As New RemoteFileBrowser
    'Свойство ILowLevelFilesBrowse::Path задает путь к
    'директории, откуда будут считываться файлы
    obj.Path = Path
    'Следующее присвоение неявно вызывает QueryInterface
    'и запрашивает ISmartFileBrowser
    Set GetRemoteFileBrowser = obj
End Function

Обратите внимание на то, что объект создается как обычно. У него задается свойство Path, которое принадлежит серверному интерфейсу. Чудесное превращение начинается с неявного вызова QueryInterface, происходящего при задании возвращаемого значения функции.

Третий объект, как вы уже, наверное, догадались, это proxy-объект, который должен создаваться в клиентском процессе и обеспечивать простой интерфейс. Этот объект называется SmartFileBrowser. Его главным интерфейсом является ISmartFileBrowser. Вот его описание:

[
    object,
    uuid(712853E3-86AF-4AE3-B77F-A58A6122DA41),
    dual,
    pointer_default(unique)
]
interface ISmartFileBrowser : IDispatch
{
    [propget, id(1)] 
    HRESULT FileName([out, retval] BSTR *pVal);
    [propget, id(2)] 
    HRESULT FileAtr([out, retval] long *pVal);
    [id(3)] HRESULT FetchNext();
};

Методы этого интерфейса обращаются к кэшированным данным. Когда же SmartFileBrowser доходит до конца кэша, он вызывает методы удаленного объекта. Указатель на интерфейс удаленного объекта (ILowLevelFilesBrowse) получается в методе IMarshal::UnmarshalInterface путем распаковки присланного буфера. Для превращения бинарного описания в указатель на интерфейс используется API-функция CoUnmarshalInterface. После распаковки указателя на интерфейс IMarshal::UnmarshalInterface объекта SmartFileBrowser распаковывает первый пакет со списком файлов. Вот код IMarshal::UnmarshalInterface, реализованного в объекте SmartFileBrowser:

STDMETHODIMP UnmarshalInterface(IStream *pStm, 
   REFIID riid, void **ppv)
{
   *ppv = NULL;
   if(!pStm || !ppv)
      return E_POINTER;
   HRESULT hr = S_OK;
   if(riid == IID_ISmartFileBrowser)
   {
      hr = CoUnmarshalInterface(pStm, IID_ILowLevelFilesBrowse, 
              (void**)&m_spILowLevelFilesBrowse);
      AddRef();
      *ppv = (ISmartFileBrowser*)this;
      // Распаковываем буфер, содержащий первый список файлов...
      hr = pStm->Read(&m_iSfiCnt, sizeof(m_iSfiCnt), NULL);
      if(FAILED(hr)) return hr;
      CComPtr<IRecordInfo> spIRecordInfo;
      hr = GetRecordInfoFrom-Guids(LIBID_NETCALLOPTIMIZATIONLib, 
              1, 0, GetUserDefaultLCID(), 
              __uuidof(SimpleFileInfo), &spIRecordInfo);
      if(FAILED(hr)) return hr;
      ReleasePsa();
      m_psa = SafeArrayCreateVectorEx(VT_RECORD, 0, m_iSfiCnt, 
                                      spIRecordInfo);
      if(!m_psa)
         return E_OUTOFMEMORY;
      hr = SafeArrayAccessData(m_psa, (void**)&m_pSfi);
      if(FAILED(hr)) return hr;
      for(int i = 0; i < m_iSfiCnt; i++)
      {
         hr = pStm->Read(&m_pSfi[i].Flags, 
                         sizeof(m_pSfi[i].Flags), NULL);
         int iLen = 0;
         hr = pStm->Read(&iLen, sizeof(iLen), NULL);
         if(FAILED(hr)) return hr;
         m_pSfi[i].bstrFileName = ::SysAllocStringLen(NULL, 
                                                      iLen);
         hr = pStm->Read(m_pSfi[i].bstrFileName, 
                         iLen * sizeof(OLECHAR), NULL);
         if(FAILED(hr)) return hr;
      }
      m_iSfiCur = 0; 
   }
   else
   {
      hr = E_NOINTERFACE;
   }
   return hr;
}

Как вы видите, основной код этого метода занимается распаковкой первого пакета.

Данные из первого пакета помещаются в SAFEARRAY структур SimpleFileInfo. Структура SimpleFileInfo описана в IDL-файле следующим образом:

[uuid(3339F275-7D2C-4CB3-88E9-186D2A4D9251), version(1.0)]
typedef struct SimpleFileInfo 
{
    BSTR bstrFileName;
    long Flags;
} SimpleFileInfo;
cpp_quote("struct __declspec(uuid(\"3339F275-7D2C-4CB3-88E9-186D2A4D9251\")) SimpleFileInfo;")

Атрибут uuid(...) в описании структуры нужен для совместимости с Automation. После его задания языки программирования типа VB начинают правильно воспринимать массивы структур. Последняя строка («cpp_quote(...)») – это, собственно, не описание, а MIDL-оператор, позволяющий поместить в заголовочный файл некоторый код. В нашем случае мы ассоциировали GUID с нашей структурой. Теперь в C++ коде для получения GUID'а этой структуры можно будет использовать конструкцию «__uuidof(SimpleFileInfo)». GUID структуры необходим при создании SAFEARRAY'я этих структур.

Список файлов выводится в примере (exeSmartFileBrowserTestVB.vbp), написанном на VB. Этот пример создает удаленный COM+-объект и вызывает его метод, передавая ему в качестве параметра путь к директории. Этот метод возвращает наш «умный» объект. Естественно, возвращается не серверный объект, а его «умный» proxy. Имена файлов помещаются в окно списка. Первые десять имен считываются при нажатии кнопки «Refresh», а остальные по событию таймера, которое происходит каждые 30 миллисекунд. Вот код этого приложения:

Private Declare Function GetSystemDirectory Lib "kernel32" _
   Alias "GetSystemDirectoryA" (ByVal lpBuffer As String, _
   ByVal nSize As Long) As Long

Dim SObj As NETCALLOPTIMIZATIONLib.SmartFileBrowser

Private Sub Form_Load()
   'Заносим в поле dfPath путь к системной директории.
   'К сожалению, приложение читает путь на локальной машине
   'так что на серверной машине он может отличаться :(
    Dim s As String * 260
    Dim Length As Long
    Length = GetSystemDirectory(s, Len(s))
    dfPath = Left(s, Length)
End Sub

Private Sub Command1_Click()
    Timer1.Enabled = False
    Dim Prxy As New SmartProxyPrjVB.SmartFileBrowserProxyVB
    Set SObj = Prxy.GetRemoteFileBrowser(dfPath)
    List1.Clear
    Timer1_Timer
    Timer1.Enabled = True
End Sub

Private Sub Timer1_Timer()
    On Error GoTo Err1
    Dim i As Long
    For i = 0 To 9
        If SObj.FileAtr And &H10 Then
            List1.AddItem " [" & SObj.FileName & "]"
        Else
            List1.AddItem SObj.FileName
        End If
        SObj.FetchNext
    Next
    Exit Sub
Err1:
    Timer1.Enabled = False
End Sub

Внешний вид приложения показан на Рис. 12.

Рис. 12 Приложение exeSmartFileBrowserTestVB

Объекты RemoteFileBrowser и SmartFileBrowser находятся в проекте NetCallOptimization\NetCallOptimization.dsp. В реальной жизни имеет смысл разносить их в разные DLL-библиотеки, но в целях упрощения примера я поместил их в один проект и, соответственно, в одну DLL.

Пример, увы, получился довольно надуманным и неуклюжим, но не надо забывать, что его задачей является демонстрация создания объекта-оборотня и умного proxy, а не решение алгоритмических проблем или создание полезной утилиты. Так что не судите строго... Главное, чтобы вы смогли разумно распорядиться «элементами высшего пилотажа», демонстрируемыми этим примером, в реальных, осмысленных приложениях.

 
В.Чистяков
 

Copyright © 1994-2016 ООО "К-Пресс"