Страница:
Для того чтобы построить интерфейсный маршалер из этих сгенерированных файлов, необходимо лишь создать сборочный файл (makefile), который скомпилирует три исходных С-файла (sports_i.с, sports_p.c, dlldata.c) и скомпонует их имеете для создания библиотеки DLL. Четыре стандартные точки входа СОМ должны быть явно экспортированы с помощью либо файла определения модуля, либо переключателей компоновщика. Отметим, что по умолчанию dlldata.c содержит только определения DllGetClassObject и DllCanUnloadNow . Это происходит потому, что поддерживающая RPC динамическая библиотека под Windows NT 3.50 поддерживала только эти две подпрограммы. Если интерфейсный маршалер будет использоваться только под Windows NT 3.51 или под более поздние версии (а также под Windows 95), то символ С-препроцсссора REGISTER_PROXY_DLL должен быть определен при компиляции файла dlldata.c , чтобы стандартные входные точки саморегистрации также были скомпилированы. Интерфейсный маршалер после создания должен быть установлен в локальном реестре и/или в хранилище классов.
В реализацию библиотеки СОМ под Windows NT 4.0 введена поддержка полностью интерпретируемого (interpretive) маршалинга. В зависимости от интерфейса использование интерпретируемого маршалера может значительно увеличить эффективность приложения путем сокращения объема рабочей памяти (working set). Предварительно установленные интерфейсные маршалеры для всех стандартных интерфейсов СОМ используют интерпретируемый маршалер. Microsoft Transaction Server (MTS) обязывает интерфейсные маршалеры использовать интерпретируемый маршалер[1]. Чтобы задействовать интерпретируемый маршалер, просто запустите компилятор MIDL с переключателем /Oicf в командной строке:
midl.exe /0icf sports.idl
В то время, когда пишется этот текст, компилятор MIDL не перезаписывает существующий файл _p.c, так что при изменении данной установки этот файл должен быть удален. Поскольку интерфейсные маршалеры, основанные на /Oicf, не будут работать на версиях СОМ до Windows NT 4.0, то при компиляции исходного кода маршалера символу С-препроцессора _WIN32_WINNT нужно присвоить целое значение, большее или равное 0х400. С-компилятор сделает это во время компиляции.
Третья методика для генерирования интерфейсных маршалеров поддерживается ограниченным числом интерфейсных классов. Если интерфейс использует только простые типы данных, которые поддерживаются VARIANT[2], то можно использовать универсальный маршалер. Использование универсального маршалера разрешается путем добавления атрибута [oleautomation] к определению интерфейса:
[ uuid(F99D19A3-D8BA-11d0-8C4F-0080C73925BA), version(1.0)]
library SportsLib {
importlib(«stdole32.tlb»);
[
uuid(F99D1907-D8BA-11D0-8C4F-0080C73925BA), object,
oleautomation
]
interface IWrestler : IUnknown {
import «oaidl.idl»;
HRESULT HalfNelson([in] double nmsec);
} }
Наличие атрибута [oleautomation] информирует функцию RegisterTypeLib , что при регистрации библиотеки типов ей следует добавить следующие дополнительные элементы реестра:
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}]
@="IWrestler"
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}\ProxyStubClsid32]
@="{O0020424-0000-0000-C000-000000000046}"
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}\ProxyStubClsid]
@="{O0020424-0000-0000-C000-000000000046}"
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}\TypeLib]
@="{F99D19AЗ-08BA-11d0-8C4F-0080C73925BA}"
Version="1.0"
CLSID {O0020424-0000-0000-C000-000000000046} соответствует универсальному маршалеру, который предварительно устанавливается на всех платформах, поддерживающих СОМ, в том числе в 16-разрядных Windows.
Основное преимущество использования универсального маршалера заключается в том, что это – единственная поддерживаемая методика осуществления стандартного маршалинга между 16– и 32-разрядными приложениями. Кроме того, универсальный маршалер совместим с Microsoft Transaction Server. Другое преимущество универсального марщалера заключается в следующем: если библиотека типов установлена на хост-машинах и клиента, и объекта, то не потребуется никакой дополнительной DLL интерфейсного марщалера. Основной же недостаток использования универсального маршалера – ограниченная поддержка типов данных параметров. Это то же самое ограничение, которое устанавливают динамический вызов и среды выполнения сценариев, но является серьезным ограничением при разработке интерфейсов системного программирования низкого уровня[3]. Под Windows NT 4.0 начальные затраты на вызов CoMarshalInterface / CoUnmarshalInterface несколько повысятся при использовании универсального маршалера. Однако после обработки интерфейсных заместителя и заглушки затраты на вызов метода становятся эквивалентными затратам на /0icf -маршалер.
Стандартный маршалинг, потоки и протоколы
Управление жизненным циклом и маршалинг
В реализацию библиотеки СОМ под Windows NT 4.0 введена поддержка полностью интерпретируемого (interpretive) маршалинга. В зависимости от интерфейса использование интерпретируемого маршалера может значительно увеличить эффективность приложения путем сокращения объема рабочей памяти (working set). Предварительно установленные интерфейсные маршалеры для всех стандартных интерфейсов СОМ используют интерпретируемый маршалер. Microsoft Transaction Server (MTS) обязывает интерфейсные маршалеры использовать интерпретируемый маршалер[1]. Чтобы задействовать интерпретируемый маршалер, просто запустите компилятор MIDL с переключателем /Oicf в командной строке:
midl.exe /0icf sports.idl
В то время, когда пишется этот текст, компилятор MIDL не перезаписывает существующий файл _p.c, так что при изменении данной установки этот файл должен быть удален. Поскольку интерфейсные маршалеры, основанные на /Oicf, не будут работать на версиях СОМ до Windows NT 4.0, то при компиляции исходного кода маршалера символу С-препроцессора _WIN32_WINNT нужно присвоить целое значение, большее или равное 0х400. С-компилятор сделает это во время компиляции.
Третья методика для генерирования интерфейсных маршалеров поддерживается ограниченным числом интерфейсных классов. Если интерфейс использует только простые типы данных, которые поддерживаются VARIANT[2], то можно использовать универсальный маршалер. Использование универсального маршалера разрешается путем добавления атрибута [oleautomation] к определению интерфейса:
[ uuid(F99D19A3-D8BA-11d0-8C4F-0080C73925BA), version(1.0)]
library SportsLib {
importlib(«stdole32.tlb»);
[
uuid(F99D1907-D8BA-11D0-8C4F-0080C73925BA), object,
oleautomation
]
interface IWrestler : IUnknown {
import «oaidl.idl»;
HRESULT HalfNelson([in] double nmsec);
} }
Наличие атрибута [oleautomation] информирует функцию RegisterTypeLib , что при регистрации библиотеки типов ей следует добавить следующие дополнительные элементы реестра:
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}]
@="IWrestler"
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}\ProxyStubClsid32]
@="{O0020424-0000-0000-C000-000000000046}"
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}\ProxyStubClsid]
@="{O0020424-0000-0000-C000-000000000046}"
[HKCR\Interface\{F99D1907-D8BA-11d0-8C4F-0080C73925BA}\TypeLib]
@="{F99D19AЗ-08BA-11d0-8C4F-0080C73925BA}"
Version="1.0"
CLSID {O0020424-0000-0000-C000-000000000046} соответствует универсальному маршалеру, который предварительно устанавливается на всех платформах, поддерживающих СОМ, в том числе в 16-разрядных Windows.
Основное преимущество использования универсального маршалера заключается в том, что это – единственная поддерживаемая методика осуществления стандартного маршалинга между 16– и 32-разрядными приложениями. Кроме того, универсальный маршалер совместим с Microsoft Transaction Server. Другое преимущество универсального марщалера заключается в следующем: если библиотека типов установлена на хост-машинах и клиента, и объекта, то не потребуется никакой дополнительной DLL интерфейсного марщалера. Основной же недостаток использования универсального маршалера – ограниченная поддержка типов данных параметров. Это то же самое ограничение, которое устанавливают динамический вызов и среды выполнения сценариев, но является серьезным ограничением при разработке интерфейсов системного программирования низкого уровня[3]. Под Windows NT 4.0 начальные затраты на вызов CoMarshalInterface / CoUnmarshalInterface несколько повысятся при использовании универсального маршалера. Однако после обработки интерфейсных заместителя и заглушки затраты на вызов метода становятся эквивалентными затратам на /0icf -маршалер.
Стандартный маршалинг, потоки и протоколы
Подробности того. как СОМ на самом деле преобразует запросы ORPC в потоки, нигде не документированы и подлежат изменениям но мере развития реализации библиотеки СОМ. Описания, содержащиеся в данном разделе, относятся только ко времени написания этого текста, но, конечно, некоторые детали реализации могут измениться в последующих выпусках СОМ.
Когда в процессе инициализируется первый апартамент, СОМ включает RPC-слой времени выполнения, переводя процесс в RPC-сервер. Если апартамент имеет тип МТА или RTA, то используется RPC-протокол ncalrpc , который является оберткой вокруг портов LPC (local procedure call – локальный вызов процедуры) под Windows NT. Если тип апартамента – STA, то используется последовательность закрытого протокола, основанная на очередях сообщений MSG Windows. При первом обращении внехостовых клиентов к объектам, постоянно находящимся в процессе, в процессе регистрируются дополнительные сетевые протоколы. Когда процесс впервые начинает использовать протоколы, отличные от протокола MSG Windows , запускается кэш потоков RPC. Этот кэш потоков начинается с одного потока, который ожидает приходящих запросов на установление соединения, запросов RPC или других действий, специфических для протоколов. Как только произойдет любое из этих событий, кэш потоков RPC переключит поток на обслуживание запроса и будет ждать следующих действий. Для предотвращения излишних затрат на создание/уничтожение потоков эти потоки возвращаются в потоковый кэш, где они будут ждать дальнейшей работы. Если работы нет, то потоки самоуничтожатся по истечении заранее определенного периода бездействия. Теневая сторона этого заключается в том, что кэш потоков RPC растет или сжимается в зависимости от занятости объектов, экспортированных из апартаментов процессов. С точки зрения программирования важно заметить, что кэш потоков RPC динамически размещает потоки, основанные на ORPC-запросах, приходящие по любым протоколам, кроме протокола Windows MSG, который будет обсуждаться позднее в этом разделе.
Когда поступающий ORPC-запрос переадресуется потоку из кэша. поток выделяет из заголовка ORPC-вызова IPID (идентификатор указателя интерфейса) и находит соответствующий администратор заглушек и интерфейсную заглушку. Поток определяет тип того апартамента, в котором хранится объект, и если объект находится в апартаменте типа МТА или RTA, поток входит в апартамент объекта и вызывает метод IRpcStubBuffer::Invoke на интерфейсную заглушку. Если апартамент имеет тип RTA, то в течение вызова метода последующие потоки не будут допускаться к объекту. Если апартамент имеет тип МТА, то последующие потоки могут обращаться к объекту одновременно. В случае внутрипроцессных RTA/MTA-связей канал может сократить кэш потоков RPC и использовать поток клиента повторно, временно входя в апартамент объекта. Если бы МТА и RTA были единственными типами апартаментов, то этого было бы достаточно.
Диспетчеризация вызовов в STA более сложна, так как в существующий STA не могут войти никакие другие потоки. К сожалению, когда ORPC-запросы поступают от внехостовых клиентов, они координируются с использованием потоков из RPC кэша потоков, которые по определению не могут выполняться в STA объекта. Для того чтобы войти в STA и направить вызов потока STA, RPC-поток использует АРI-функцию PostMessage , которая ставит сообщение в специальную MSG -очередь сообщений STA-потоков, как показано на рис. 5.5. Эта очередь представляет собой ту же очередь FIFO (first-in, first-out), которую применяет оконная система. Это означает, что для завершения диспетчеризации вызова STA-поток должен обслуживать очередь с помощью одной из вариаций следующего кода:
MSG msg;
while (GetMessage(&msg, 0, О, 0))
DispatchMessage(&msg);
Этот код означает, что STA-поток имеет по меньшей мере одно окно, которое может принимать сообщения. Когда поток входит в новый STA посредством вызова CoInitializeEx , СОМ создает новое невидимое окно, вызывая CreateWindowEx. Это окно связано с зарегистрированным в СОМ оконным классом, элемент которого WndProc ищет определенные заранее оконные сообщения и обслуживает соответствующий ORPC-запрос посредством вызова метода IRpcStubBuffer::Invoke на интерфейсную заглушку. Отметим, что поскольку окна, подобно объектам на основе STA, обладают потоковой привязкой, WndProc будет выполняться в апартаменте объекта. Чтобы избежать чрезмерного количества переключения потоков, в версии СОМ для Windows 95 предусмотрен транспортный RPC-механизм, который обходит RPC-кэш потоков и вызывает PostMessage из потока вызывающего объекта. Этот перенос возможен только в том случае, если клиент находится на том же хосте, что и объект, поскольку API-функция PostMessage не работает в сети.
Для предотвращения взаимоблокировки все типы апартаментов СОМ поддерживают реентерабельность (повторную входимость)[1]. Когда поток в апартаменте делает запрос на объект вне апартамента вызывающего объекта посредством заместителя, то могут обслуживаться и другие поступающие запросы методов, пока поток вызывающего объекта находится в ожидании ORPC-ответа па первый запрос. Без этой поддержки было бы невозможно создавать системы, основанные на совместно работающих объектах. При написании следующего кода предполагалось, что CLSID_Callback является внутрипроцессным сервером, поддерживающим модель вызывающего потока, и что CLSID_Object является классом, сконфигурированным для активации на удаленной машине:
ICallback *pcb = 0;
HRESULT hr = CoCreateInstance(CLSID_Callback, 0, CLSCTX_ALL,
IID_ICallback, (void**)&pcb);
assert(SUCCEEDED(hr));
// callback object lives in this apt.
// объект обратного вызова живет в этом апартаменте
I0bject "po = 0;
hr = CoCreateInstance(CLSID_Object, 0, CLSCTX_REMOTE_SERVER,
IID_Iobject, (void **)&po);
assert(SUCCEEDED(hr));
// object lives in different apt.
// объект живет в другом апартаменте
// make a call to remote object, marshaling a reference to
// the callback object as an [in] parameter
// делаем вызов удаленного объекта, маршалируя ссылку
// на объект обратного вызова как на [in]-параметр
hr = po->UseCallback(pcb);
// clean up resources
// очищаем ресурсы
pcb->Release();
pco->Release();
На рис. 5.6 показано, что если апартамент вызывающего объекта не поддерживает реентерабельность, то следующая реализация метода UseCallback вызовет взаимоблокировку:
STDMETHODIMP Object::UseCallback(ICallback *pcb) {
HRESULT hr = pcb->GetBackToCallersApartment();
assert(SUCCEEDED(hr));
return S_OK;
Напомним, что когда [in]–параметр передается через метод заместителя UseCallback, то заместитель вызывает CoMarshalInterface для маршалинга интерфейсного указателя ICallback. Поскольку указатель ссылается на объект, находящийся в апартаменте вызывающего объекта, то этот апартамент становится экспортером объектов и поэтому любые межапартаментные вызовы объекта должны обслуживаться в апартаменте вызывающего объекта. Когда заглушка интерфейса IObject демаршалирует интерфейс ICallback, она создает заместитель для передачи его реализации метода UseCallback. Этот заместитель представляет объект при промежуточном соединении с объектом обратного вызова, которое продолжается на протяжении всего времени вызова. Время существования этого заместителя/соединения может превысить время вызова, если реализация метода просто вызовет AddRef на заместитель[2]:
STDMETHODIMP Object::UseCallback(ICallback *pcb) {
if (!pcb) return E_INVALIDARG;
// hold onto proxy for later use
// сохраняем в заместителе для дальнейшего использования
(m_pcbMyCaller = pcb)->AddRef();
return S_OK; }
Обратное соединение с апартаментом клиента будет длиться до тех пор, пока заместитель не будет освобожден объектом. Поскольку все апартаменты СОМ могут получать ORPC-запросы, объект может послать обратный вызов в апартамент клиента в любое время.
Реентерабельность реализуется для каждого типа апартаментов по-разному. Наиболее проста реализация в случае МТА, так как МТА-апартаменты не гарантируют параллелизма и не указывают, какой поток будет обслуживать заданный вызов метода. Повторный вызов может прийти в то время, когда МТА-поток заблокирован в канале в ожидании ORPC-ответа. Тогда RPC-поток, получающий повторный запрос, просто входит в МТА и обслуживает вызов своими ресурсами. Тот факт, что другой поток апартамента заблокирован и ожидании ORPC-ответа, не влияет на диспетчеризацию вызова. В случае реализации RTA – когда поток, выполняющийся в RTA, делает межапартаментный вызов посредством заместителя, – канал уступает контроль над апартаментом, снимая блокировку всего RTA и разрешая тем самым обработку поступивших вызовов. И снова, но причине отсутствия привязки объектов к потокам в RTA, RPC-поток, получающий ORPC-запрос, может просто войти в апартамент RTA и обслужить вызов сразу после блокирования всего RTA.
Реализация реентерабельности для апартаментов STA более сложна. Поскольку STA-объекты обладают привязкой к потоку, то когда поток делает межапартаментный вызов из STA, СОМ не может разрешить потоку сделать блокирующий вызов, который предотвратил бы обработку входящих ORPC-запросов. Когда поток вызывающего объекта входит в метод канала SendReceive , чтобы послать ORPC-запрос и получить ORPC-ответ, этот канал захватывает поток вызывающего объекта и помещает его во внутренний оконный MSG -цикл. Это аналогично тому, что происходит при создании потоком модальных диалоговых окон. В обоих случаях поток вызывающего объекта вынужден обслуживать определенные классы оконных сообщений во время выполнения этой операции. В случае модальных диалоговых окон поток должен обслуживать основные оконные сообщения, чтобы разморозить основной пользовательский интерфейс. В случае межапартаментного вызова метода в СОМ поток должен обслуживать не только обычные оконные сообщения пользовательского интерфейса, но и оконные сообщения, относящиеся к поступающим ORPC-запросам. По умолчанию канал будет разрешать обслуживание всех поступающих ORPC-вызовов, пока клиентский поток ожидает ORPC-ответа. Такой режим можно настроить с помощью установки в потоке специального фильтра сообщений.
Фильтры сообщений являются уникальными для STA. Фильтр сообщений – это объект СОМ для каждого STA, который используется для решения вопроса, организовать диспетчеризацию поступающих ORPC-запросов или нет. Кроме того, фильтры сообщений используются для размещения задержанных сообщений пользовательского интерфейса, пока поток STA ожидает ORPC-ответа внутри канала. Фильтры сообщений выставляют интерфейс IMessageFilter:
[ uuid(00000016-0000-0000-C000-000000000046),local, object ]
interface IMessageFilter : IUnknown {
typedef struct tagINTERFACEINFO {
IUnknown *pUnk;
// which object?
// чей объект?
IID iid;
// which interface?
// чей интерфейс?
WORD wMethod;
// which method?
// чей метод?
} INTERFACEINFO;
// called when an incoming ORPC request arrives in an STA
// вызывается, когда входящий ORPC-запрос поступает в STA
DWORD HandleInComingCall(
[in] DWORD dwCallType,
[in] HTA5K dwThreadIdCaller,
[in] DWORD dwTickCount,
[in] INTERFACEINFO *pInterfaceInfo
);
// called when another STA rejects or postpones an ORPC request
// вызывается, когда другой STA отклоняет или откладывает ORPC-запрос
DWORD RetryRejectedCall(
[in] HTASK dwThreadIdCallee,
[in] DWORD dwTickCount,
[in] DWORD dwRejectType
);
// called when a non-COM MSG arrives while the thread is
// awaiting an ORPC response
// вызывается, когда поступает не СОМ'овское MSG, пока
// поток ожидает ORPC-ответа
DWORD MessagePending(
[in] HTASK dwThreadIdCallee,
[in] DWORD dwTickCount,
[in] DWORD dwPendingType
); }
Для установки специального фильтра сообщений в СОМ существует API-функция CoRegisterMessageFilter:
HRESULT CoRegisterMessageFilter([in] IMessageFilter *pmfNew, [out] IMessageFilter **ppmfOld);
CoRegisterMessageFilter связывает указанный фильтр сообщений с текущим STA. Предыдущий фильтр сообщений возвращается для того, чтобы вызывающий объект мог восстановить его в дальнейшем.
Когда бы входящий ORPC-запрос ни пришел в STA-поток, вызывается метод фильтра сообщений HandleIncomingCall, который дает апартаменту возможность принять, отклонить или отложить вызов. HandleIncomingCall используется как реентерабельными, так и нереентерабельными вызовами. Параметр dwCallType показывает, какой тип вызова был получен:
typedef enum tagCALLTYPE {
CALLTYPE_TOPLEVEL,
// STA not in outbound call
// STA не в исходящем вызове
CALLTYPE_NESTED,
// callback on behalf of outbound call
// обратный вызов от имени исходящего вызова
CALLTYPE_ASYNC,
// asynchronous call
// асинхронный вызов
CALLTYPE_TOPLEVEL_CALLPENDING,
// new call while waiting
// новый вызов во время ожидания
CALLTYPE_ASYNC_CALLPENDING
// async call while waiting
// асинхронный вызов во время ожидания
} CALLTYPE;
Вложенный (реентерабельный) вызов и незаконченный (нереентерабельный) вызов верхнего уровня происходят, пока поток ожидает ORPC-ответа в канале. Вызовы верхнего уровня происходят в тех случаях, когда в апартаменте нет активных вызовом.
В СОМ определено перечисление, которое должна возвратить реализация HandleIncomingCall, чтобы указать, что произошло с вызовом:
typedef enum tagSERVERCALL {
SERVERCALL_ISHANDLED,
// accept call and forward to stub
// принимаем вызов и направляем его заглушке
SERVERCALL_REJECTED,
// tell caller that call is rejected
// сообщаем вызывающему объекту, что вызов отклонен
SERVERCALL RETRYLATER
// tell caller that call is postponed
// сообщаем вызывающему объекту, что вызов отложен
} SERVERCALL;
Если функция HandleIncomingCall фильтра сообщений возвращает SERVERCALL_ISHANDLED, то вызов будет направлен в интерфейсную заглушку для демаршалинга. Фильтр сообщений, принятый по умолчанию, всегда возвращает SERVERCALL_ISHANDLED. Если HandleIncomingCall возвращает SERVERCALL_REJECTED или SERVERCALL_RETRYLATER, то фильтр сообщений вызывающего объекта будет информирован о положении вызова и ORPC-запрос будет отклонен.
Когда фильтр сообщений отвергает или откладывает вызов, то фильтр сообщений вызывающего объекта информируется об этом с помощью метода RetryRejectedCall. Этот вызов происходит в контексте апартамента вызывающего объекта, и реализация метода RetryRejectedCall фильтра сообщений может решать, повторять ли отложенный вызов. Параметр dwRejectType указывает, был ли вызов отклонен или отложен. Реализация канала вызывающего объекта будет решать, какое действие предпринять, в зависимости от значения, возвращенного RetryRejectedCall. Если RetryRejectedCall возвращает –1, то канал предположит, что повторных попыток не требуется, и немедленно заставит заместитель возвратить HRESULT, равный RPC_E_CALL_REJECTED. По умолчанию фильтр сообщений всегда возвращает –1. Любое другое значение, возвращаемое методом RetryRejectedCall, интерпретируется как число миллисекунд, через которое следует повторить вызов. Поскольку это согласование осуществляется внутри канала, то не требуется повторного ORPC-запроса со стороны заместителя. В сущности, интерфейсные маршалеры не имеют ни малейшего понятия о процессах в фильтре сообщений.
Когда размещенный в STA поток блокирован в канале в ожидании ORPC-ответа, то не связанные с СОМ оконные сообщения могут поступать в MSG -очередь потока. Когда это происходит, то фильтр сообщений STA уведомляется об этом посредством метода MessagePending. Фильтр сообщений, принятый по умолчанию, разрешает диспетчеризацию некоторых оконных сообщений, чтобы предотвратить замораживание всей оконной системы. Тем не менее, действия ввода (например, щелчки мышью, нажатие клавиш) не учитываются, чтобы конечный пользователь не начал новое взаимодействие с системой. Как уже отмечалось ранее, фильтры сообщений существуют только в апартаментах STA и не поддерживаются в RTA или МТА. Фильтры сообщений лишь обеспечивают лучшую интеграцию СОМ с потоками, обрабатывающими события от пользовательского интерфейса. Из этого следует, что все эти потоки должны выполняться в однопотоковых апартаментах. Большинство потоков, обрабатывающих события от пользовательского интерфейса, захотят установить специальный фильтр сообщений, чтобы убедиться в том, что входящие запросы не обслуживаются, пока приложение находится в такой критической фазе, в которой реентерабельность может привести к семантическим ошибкам. Фильтры сообщений не следует применять в качестве универсального механизма для управления потоками. Реализация фильтров сообщений печально известна своей неэффективностью в тех случаях, когда вызовы отклоняются или откладываются. Это делает фильтры сообщений малоприспособленными в качестве механизма для управления потоками в высокопроизводительных приложениях.
Когда в процессе инициализируется первый апартамент, СОМ включает RPC-слой времени выполнения, переводя процесс в RPC-сервер. Если апартамент имеет тип МТА или RTA, то используется RPC-протокол ncalrpc , который является оберткой вокруг портов LPC (local procedure call – локальный вызов процедуры) под Windows NT. Если тип апартамента – STA, то используется последовательность закрытого протокола, основанная на очередях сообщений MSG Windows. При первом обращении внехостовых клиентов к объектам, постоянно находящимся в процессе, в процессе регистрируются дополнительные сетевые протоколы. Когда процесс впервые начинает использовать протоколы, отличные от протокола MSG Windows , запускается кэш потоков RPC. Этот кэш потоков начинается с одного потока, который ожидает приходящих запросов на установление соединения, запросов RPC или других действий, специфических для протоколов. Как только произойдет любое из этих событий, кэш потоков RPC переключит поток на обслуживание запроса и будет ждать следующих действий. Для предотвращения излишних затрат на создание/уничтожение потоков эти потоки возвращаются в потоковый кэш, где они будут ждать дальнейшей работы. Если работы нет, то потоки самоуничтожатся по истечении заранее определенного периода бездействия. Теневая сторона этого заключается в том, что кэш потоков RPC растет или сжимается в зависимости от занятости объектов, экспортированных из апартаментов процессов. С точки зрения программирования важно заметить, что кэш потоков RPC динамически размещает потоки, основанные на ORPC-запросах, приходящие по любым протоколам, кроме протокола Windows MSG, который будет обсуждаться позднее в этом разделе.
Когда поступающий ORPC-запрос переадресуется потоку из кэша. поток выделяет из заголовка ORPC-вызова IPID (идентификатор указателя интерфейса) и находит соответствующий администратор заглушек и интерфейсную заглушку. Поток определяет тип того апартамента, в котором хранится объект, и если объект находится в апартаменте типа МТА или RTA, поток входит в апартамент объекта и вызывает метод IRpcStubBuffer::Invoke на интерфейсную заглушку. Если апартамент имеет тип RTA, то в течение вызова метода последующие потоки не будут допускаться к объекту. Если апартамент имеет тип МТА, то последующие потоки могут обращаться к объекту одновременно. В случае внутрипроцессных RTA/MTA-связей канал может сократить кэш потоков RPC и использовать поток клиента повторно, временно входя в апартамент объекта. Если бы МТА и RTA были единственными типами апартаментов, то этого было бы достаточно.
Диспетчеризация вызовов в STA более сложна, так как в существующий STA не могут войти никакие другие потоки. К сожалению, когда ORPC-запросы поступают от внехостовых клиентов, они координируются с использованием потоков из RPC кэша потоков, которые по определению не могут выполняться в STA объекта. Для того чтобы войти в STA и направить вызов потока STA, RPC-поток использует АРI-функцию PostMessage , которая ставит сообщение в специальную MSG -очередь сообщений STA-потоков, как показано на рис. 5.5. Эта очередь представляет собой ту же очередь FIFO (first-in, first-out), которую применяет оконная система. Это означает, что для завершения диспетчеризации вызова STA-поток должен обслуживать очередь с помощью одной из вариаций следующего кода:
MSG msg;
while (GetMessage(&msg, 0, О, 0))
DispatchMessage(&msg);
Этот код означает, что STA-поток имеет по меньшей мере одно окно, которое может принимать сообщения. Когда поток входит в новый STA посредством вызова CoInitializeEx , СОМ создает новое невидимое окно, вызывая CreateWindowEx. Это окно связано с зарегистрированным в СОМ оконным классом, элемент которого WndProc ищет определенные заранее оконные сообщения и обслуживает соответствующий ORPC-запрос посредством вызова метода IRpcStubBuffer::Invoke на интерфейсную заглушку. Отметим, что поскольку окна, подобно объектам на основе STA, обладают потоковой привязкой, WndProc будет выполняться в апартаменте объекта. Чтобы избежать чрезмерного количества переключения потоков, в версии СОМ для Windows 95 предусмотрен транспортный RPC-механизм, который обходит RPC-кэш потоков и вызывает PostMessage из потока вызывающего объекта. Этот перенос возможен только в том случае, если клиент находится на том же хосте, что и объект, поскольку API-функция PostMessage не работает в сети.
Для предотвращения взаимоблокировки все типы апартаментов СОМ поддерживают реентерабельность (повторную входимость)[1]. Когда поток в апартаменте делает запрос на объект вне апартамента вызывающего объекта посредством заместителя, то могут обслуживаться и другие поступающие запросы методов, пока поток вызывающего объекта находится в ожидании ORPC-ответа па первый запрос. Без этой поддержки было бы невозможно создавать системы, основанные на совместно работающих объектах. При написании следующего кода предполагалось, что CLSID_Callback является внутрипроцессным сервером, поддерживающим модель вызывающего потока, и что CLSID_Object является классом, сконфигурированным для активации на удаленной машине:
ICallback *pcb = 0;
HRESULT hr = CoCreateInstance(CLSID_Callback, 0, CLSCTX_ALL,
IID_ICallback, (void**)&pcb);
assert(SUCCEEDED(hr));
// callback object lives in this apt.
// объект обратного вызова живет в этом апартаменте
I0bject "po = 0;
hr = CoCreateInstance(CLSID_Object, 0, CLSCTX_REMOTE_SERVER,
IID_Iobject, (void **)&po);
assert(SUCCEEDED(hr));
// object lives in different apt.
// объект живет в другом апартаменте
// make a call to remote object, marshaling a reference to
// the callback object as an [in] parameter
// делаем вызов удаленного объекта, маршалируя ссылку
// на объект обратного вызова как на [in]-параметр
hr = po->UseCallback(pcb);
// clean up resources
// очищаем ресурсы
pcb->Release();
pco->Release();
На рис. 5.6 показано, что если апартамент вызывающего объекта не поддерживает реентерабельность, то следующая реализация метода UseCallback вызовет взаимоблокировку:
STDMETHODIMP Object::UseCallback(ICallback *pcb) {
HRESULT hr = pcb->GetBackToCallersApartment();
assert(SUCCEEDED(hr));
return S_OK;
Напомним, что когда [in]–параметр передается через метод заместителя UseCallback, то заместитель вызывает CoMarshalInterface для маршалинга интерфейсного указателя ICallback. Поскольку указатель ссылается на объект, находящийся в апартаменте вызывающего объекта, то этот апартамент становится экспортером объектов и поэтому любые межапартаментные вызовы объекта должны обслуживаться в апартаменте вызывающего объекта. Когда заглушка интерфейса IObject демаршалирует интерфейс ICallback, она создает заместитель для передачи его реализации метода UseCallback. Этот заместитель представляет объект при промежуточном соединении с объектом обратного вызова, которое продолжается на протяжении всего времени вызова. Время существования этого заместителя/соединения может превысить время вызова, если реализация метода просто вызовет AddRef на заместитель[2]:
STDMETHODIMP Object::UseCallback(ICallback *pcb) {
if (!pcb) return E_INVALIDARG;
// hold onto proxy for later use
// сохраняем в заместителе для дальнейшего использования
(m_pcbMyCaller = pcb)->AddRef();
return S_OK; }
Обратное соединение с апартаментом клиента будет длиться до тех пор, пока заместитель не будет освобожден объектом. Поскольку все апартаменты СОМ могут получать ORPC-запросы, объект может послать обратный вызов в апартамент клиента в любое время.
Реентерабельность реализуется для каждого типа апартаментов по-разному. Наиболее проста реализация в случае МТА, так как МТА-апартаменты не гарантируют параллелизма и не указывают, какой поток будет обслуживать заданный вызов метода. Повторный вызов может прийти в то время, когда МТА-поток заблокирован в канале в ожидании ORPC-ответа. Тогда RPC-поток, получающий повторный запрос, просто входит в МТА и обслуживает вызов своими ресурсами. Тот факт, что другой поток апартамента заблокирован и ожидании ORPC-ответа, не влияет на диспетчеризацию вызова. В случае реализации RTA – когда поток, выполняющийся в RTA, делает межапартаментный вызов посредством заместителя, – канал уступает контроль над апартаментом, снимая блокировку всего RTA и разрешая тем самым обработку поступивших вызовов. И снова, но причине отсутствия привязки объектов к потокам в RTA, RPC-поток, получающий ORPC-запрос, может просто войти в апартамент RTA и обслужить вызов сразу после блокирования всего RTA.
Реализация реентерабельности для апартаментов STA более сложна. Поскольку STA-объекты обладают привязкой к потоку, то когда поток делает межапартаментный вызов из STA, СОМ не может разрешить потоку сделать блокирующий вызов, который предотвратил бы обработку входящих ORPC-запросов. Когда поток вызывающего объекта входит в метод канала SendReceive , чтобы послать ORPC-запрос и получить ORPC-ответ, этот канал захватывает поток вызывающего объекта и помещает его во внутренний оконный MSG -цикл. Это аналогично тому, что происходит при создании потоком модальных диалоговых окон. В обоих случаях поток вызывающего объекта вынужден обслуживать определенные классы оконных сообщений во время выполнения этой операции. В случае модальных диалоговых окон поток должен обслуживать основные оконные сообщения, чтобы разморозить основной пользовательский интерфейс. В случае межапартаментного вызова метода в СОМ поток должен обслуживать не только обычные оконные сообщения пользовательского интерфейса, но и оконные сообщения, относящиеся к поступающим ORPC-запросам. По умолчанию канал будет разрешать обслуживание всех поступающих ORPC-вызовов, пока клиентский поток ожидает ORPC-ответа. Такой режим можно настроить с помощью установки в потоке специального фильтра сообщений.
Фильтры сообщений являются уникальными для STA. Фильтр сообщений – это объект СОМ для каждого STA, который используется для решения вопроса, организовать диспетчеризацию поступающих ORPC-запросов или нет. Кроме того, фильтры сообщений используются для размещения задержанных сообщений пользовательского интерфейса, пока поток STA ожидает ORPC-ответа внутри канала. Фильтры сообщений выставляют интерфейс IMessageFilter:
[ uuid(00000016-0000-0000-C000-000000000046),local, object ]
interface IMessageFilter : IUnknown {
typedef struct tagINTERFACEINFO {
IUnknown *pUnk;
// which object?
// чей объект?
IID iid;
// which interface?
// чей интерфейс?
WORD wMethod;
// which method?
// чей метод?
} INTERFACEINFO;
// called when an incoming ORPC request arrives in an STA
// вызывается, когда входящий ORPC-запрос поступает в STA
DWORD HandleInComingCall(
[in] DWORD dwCallType,
[in] HTA5K dwThreadIdCaller,
[in] DWORD dwTickCount,
[in] INTERFACEINFO *pInterfaceInfo
);
// called when another STA rejects or postpones an ORPC request
// вызывается, когда другой STA отклоняет или откладывает ORPC-запрос
DWORD RetryRejectedCall(
[in] HTASK dwThreadIdCallee,
[in] DWORD dwTickCount,
[in] DWORD dwRejectType
);
// called when a non-COM MSG arrives while the thread is
// awaiting an ORPC response
// вызывается, когда поступает не СОМ'овское MSG, пока
// поток ожидает ORPC-ответа
DWORD MessagePending(
[in] HTASK dwThreadIdCallee,
[in] DWORD dwTickCount,
[in] DWORD dwPendingType
); }
Для установки специального фильтра сообщений в СОМ существует API-функция CoRegisterMessageFilter:
HRESULT CoRegisterMessageFilter([in] IMessageFilter *pmfNew, [out] IMessageFilter **ppmfOld);
CoRegisterMessageFilter связывает указанный фильтр сообщений с текущим STA. Предыдущий фильтр сообщений возвращается для того, чтобы вызывающий объект мог восстановить его в дальнейшем.
Когда бы входящий ORPC-запрос ни пришел в STA-поток, вызывается метод фильтра сообщений HandleIncomingCall, который дает апартаменту возможность принять, отклонить или отложить вызов. HandleIncomingCall используется как реентерабельными, так и нереентерабельными вызовами. Параметр dwCallType показывает, какой тип вызова был получен:
typedef enum tagCALLTYPE {
CALLTYPE_TOPLEVEL,
// STA not in outbound call
// STA не в исходящем вызове
CALLTYPE_NESTED,
// callback on behalf of outbound call
// обратный вызов от имени исходящего вызова
CALLTYPE_ASYNC,
// asynchronous call
// асинхронный вызов
CALLTYPE_TOPLEVEL_CALLPENDING,
// new call while waiting
// новый вызов во время ожидания
CALLTYPE_ASYNC_CALLPENDING
// async call while waiting
// асинхронный вызов во время ожидания
} CALLTYPE;
Вложенный (реентерабельный) вызов и незаконченный (нереентерабельный) вызов верхнего уровня происходят, пока поток ожидает ORPC-ответа в канале. Вызовы верхнего уровня происходят в тех случаях, когда в апартаменте нет активных вызовом.
В СОМ определено перечисление, которое должна возвратить реализация HandleIncomingCall, чтобы указать, что произошло с вызовом:
typedef enum tagSERVERCALL {
SERVERCALL_ISHANDLED,
// accept call and forward to stub
// принимаем вызов и направляем его заглушке
SERVERCALL_REJECTED,
// tell caller that call is rejected
// сообщаем вызывающему объекту, что вызов отклонен
SERVERCALL RETRYLATER
// tell caller that call is postponed
// сообщаем вызывающему объекту, что вызов отложен
} SERVERCALL;
Если функция HandleIncomingCall фильтра сообщений возвращает SERVERCALL_ISHANDLED, то вызов будет направлен в интерфейсную заглушку для демаршалинга. Фильтр сообщений, принятый по умолчанию, всегда возвращает SERVERCALL_ISHANDLED. Если HandleIncomingCall возвращает SERVERCALL_REJECTED или SERVERCALL_RETRYLATER, то фильтр сообщений вызывающего объекта будет информирован о положении вызова и ORPC-запрос будет отклонен.
Когда фильтр сообщений отвергает или откладывает вызов, то фильтр сообщений вызывающего объекта информируется об этом с помощью метода RetryRejectedCall. Этот вызов происходит в контексте апартамента вызывающего объекта, и реализация метода RetryRejectedCall фильтра сообщений может решать, повторять ли отложенный вызов. Параметр dwRejectType указывает, был ли вызов отклонен или отложен. Реализация канала вызывающего объекта будет решать, какое действие предпринять, в зависимости от значения, возвращенного RetryRejectedCall. Если RetryRejectedCall возвращает –1, то канал предположит, что повторных попыток не требуется, и немедленно заставит заместитель возвратить HRESULT, равный RPC_E_CALL_REJECTED. По умолчанию фильтр сообщений всегда возвращает –1. Любое другое значение, возвращаемое методом RetryRejectedCall, интерпретируется как число миллисекунд, через которое следует повторить вызов. Поскольку это согласование осуществляется внутри канала, то не требуется повторного ORPC-запроса со стороны заместителя. В сущности, интерфейсные маршалеры не имеют ни малейшего понятия о процессах в фильтре сообщений.
Когда размещенный в STA поток блокирован в канале в ожидании ORPC-ответа, то не связанные с СОМ оконные сообщения могут поступать в MSG -очередь потока. Когда это происходит, то фильтр сообщений STA уведомляется об этом посредством метода MessagePending. Фильтр сообщений, принятый по умолчанию, разрешает диспетчеризацию некоторых оконных сообщений, чтобы предотвратить замораживание всей оконной системы. Тем не менее, действия ввода (например, щелчки мышью, нажатие клавиш) не учитываются, чтобы конечный пользователь не начал новое взаимодействие с системой. Как уже отмечалось ранее, фильтры сообщений существуют только в апартаментах STA и не поддерживаются в RTA или МТА. Фильтры сообщений лишь обеспечивают лучшую интеграцию СОМ с потоками, обрабатывающими события от пользовательского интерфейса. Из этого следует, что все эти потоки должны выполняться в однопотоковых апартаментах. Большинство потоков, обрабатывающих события от пользовательского интерфейса, захотят установить специальный фильтр сообщений, чтобы убедиться в том, что входящие запросы не обслуживаются, пока приложение находится в такой критической фазе, в которой реентерабельность может привести к семантическим ошибкам. Фильтры сообщений не следует применять в качестве универсального механизма для управления потоками. Реализация фильтров сообщений печально известна своей неэффективностью в тех случаях, когда вызовы отклоняются или откладываются. Это делает фильтры сообщений малоприспособленными в качестве механизма для управления потоками в высокопроизводительных приложениях.
Управление жизненным циклом и маршалинг
Ранее в этой главе обсуждались взаимоотношения между администратором заглушек и объектом. Администратор заглушек создается при первом вызове CoMarshalInterface для определенного идентифицированного объекта. Администратор заглушек хранит неосвобожденные ссылки на тот объект, который он предсиавляет, и существует до тех пор, пока остается хотя бы одна неосвобожденная внешняя ссылка на заглушку. Эти внешние ссылки обычно являются заместителями, хотя учитываются и маршалированные объектные ссылки, так как они могут представлять заместители. Когда все внешние ссылки на администратор заглушек уничтожены, он самоуничтожается и освобождает все хранящиеся в нем ссылки на текущий объект. Такое поведение по умолчанию в точности имитирует обычную внутрипроцессную семантику AddRef и Release. Многие объекты не имеют никаких специальных требований относительно жизненного цикла и целиком удовлетворяются этой схемой. Некоторые объекты предпочитают дифференцировать взаимоотношения между внешними ссылками, администратором заглушек и объектом. К счастью, СОМ предоставляет администратору заглушек на время жизненного цикла достаточно приемов работы, которые позволяют реализовывать различные стратегии. Для того чтобы понять, как организован жизненный цикл заглушки, необходимо в первую очередь исследовать алгоритм распределенной сборки мусора (garbage collection) СОМ.
Когда администратор заглушек создан, то идентификатор объекта (OID) регистрируется в распределенном сборщике мусора СОМ, который в настоящее время реализован в службе распознавателя идентификаторов экспортера объектов (OXID Resolver – OR). OR отслеживает, какие идентификаторы объектов экспортируются из каких апартаментов локальной хост-машины. Когда создается администратор заместителей, то CoUnmarshalInterface информирует локальный OR о том, что в апартамент импортируется объектная ссылка. Это означает, что локальный OR также знает, какие OID импортированы в каждый апартамент локальной хост-машины. Если определенный OID импортирован на хост-машину впервые, OR импортирующего хоста устанавливает отношения тестового опроса (ping relationship) с экспортирующим хостом. Тогда OR импортирующей стороны будет передавать периодические тестовые сообщения через RPC, подтверждая тем самым, что импортирующая хост-машина все еще функционирует и доступна в сети. Текущая реализация посылает такое сообщение один раз в две минуты. Если за последний тестовый интервал (ping interval) не было импортировано никаких дополнительных OID, то посылается простое уведомление. Если же были импортированы новые ссылки или освобождены уже существующие, то посылается более сложное сообщение, показывающее разницу между прошлым и нынешним наборами хранимых ссылок.
В рамках реализации СОМ для Windows NT 4.0 установлено, что если три последовательных тестовых интервала (шесть минут) пройдут без получения уведомления от определенного хоста, то OR будет считать, что хост либо сам вышел из строя, либо недоступен из-за сбоя и сети. В этом случае OR проинформирует всех администраторов заглушек, импортированных ныне отказавшим хостом, что все неосвобожденные ссылки теперь неверны и подлежат освобождению. Если какой-то определенный объект использовался исключительно клиентами ныне мертвого хоста, то в администраторе заглушек более не останется неосвобожденных ссылок и он самоликвидируется, что, в свою очередь, освободит ссылки СОМ на данный объект.
Когда администратор заглушек создан, то идентификатор объекта (OID) регистрируется в распределенном сборщике мусора СОМ, который в настоящее время реализован в службе распознавателя идентификаторов экспортера объектов (OXID Resolver – OR). OR отслеживает, какие идентификаторы объектов экспортируются из каких апартаментов локальной хост-машины. Когда создается администратор заместителей, то CoUnmarshalInterface информирует локальный OR о том, что в апартамент импортируется объектная ссылка. Это означает, что локальный OR также знает, какие OID импортированы в каждый апартамент локальной хост-машины. Если определенный OID импортирован на хост-машину впервые, OR импортирующего хоста устанавливает отношения тестового опроса (ping relationship) с экспортирующим хостом. Тогда OR импортирующей стороны будет передавать периодические тестовые сообщения через RPC, подтверждая тем самым, что импортирующая хост-машина все еще функционирует и доступна в сети. Текущая реализация посылает такое сообщение один раз в две минуты. Если за последний тестовый интервал (ping interval) не было импортировано никаких дополнительных OID, то посылается простое уведомление. Если же были импортированы новые ссылки или освобождены уже существующие, то посылается более сложное сообщение, показывающее разницу между прошлым и нынешним наборами хранимых ссылок.
В рамках реализации СОМ для Windows NT 4.0 установлено, что если три последовательных тестовых интервала (шесть минут) пройдут без получения уведомления от определенного хоста, то OR будет считать, что хост либо сам вышел из строя, либо недоступен из-за сбоя и сети. В этом случае OR проинформирует всех администраторов заглушек, импортированных ныне отказавшим хостом, что все неосвобожденные ссылки теперь неверны и подлежат освобождению. Если какой-то определенный объект использовался исключительно клиентами ныне мертвого хоста, то в администраторе заглушек более не останется неосвобожденных ссылок и он самоликвидируется, что, в свою очередь, освободит ссылки СОМ на данный объект.