Страница:
Если апартамент клиента совместим с моделью организации поточной обработки идентификатора класса CLSID , то все запросы на внутрипроцессную активацию для этого CLSID будут обрабатывать объект непосредственно в апартаменте клиента. Это, безусловно, наиболее эффективный сценарий, так как не требуется никаких промежуточных заместителей[1]. Если же апартамент клиента несовместим с моделью организации поточной обработки, указанной в CLSID, то запросы на внутрипроцесспую активацию для таких CLSID будут приводить к скрытому созданию объекта в отдельном апартаменте, а клиенту будет возвращен заместитель. В случае, когда STA -клиенты активируют классы с ThreadingModel="Free", объект класса (и последующие его экземпляры) будут выполняться в МТА. В случае, когда MTA–клиенты активируют классы с ThreadingModel="Apartment", объект класса (и последующие его экземпляры) будут выполняться в STA , созданном СОМ. В случае, когда клиенты любого типа активируют классы на основе главного STA, объект класса (и последующие его экземпляры) будут выполняться в главном STA процесса. Если же клиент окажется потоком главного STA, то будет осуществлен прямой доступ к объекту. В противном случае клиенту будет возвращен заместитель. Если в процессе нет ни одного STA (то есть если ни один поток не вызвал CoInitiаlizeEx с флагом COINIT_APARTMENTTHREADED), тогда СОМ создаст новый STA с тем, чтобы он стал главным STA для процесса.
Разработчики классов, не предусматривающие модель организации поточной обработки для своих классов, могут большей частью игнорировать проблемы, связанные с потоками, так как доступ к их библиотекам DLL будет осуществляться только из одного потока, а именно из главного STA–потока. Те разработчики, которые предусматривают для своих классов поддержку любой явной модели организации поточной обработки, косвенно свидетельствуют, что каждый из множественных апартаментов в процессе (который может быть многопоточным) может содержать экземпляры класса. Поэтому разработчик должен защитить любые ресурсы, которые совместно используются более чем одним экземпляром класса, от параллельного доступа. Это означает, что все глобальные и статические переменные должны быть защищены с помощью соответствующего примитива синхронизации потоков. Для внутрипроцессного сервера СОМ глобальный счетчик блокировок, отслеживающий время жизни сервера, должен быть защищен с помощью InterlockedIncrement / InterlockedDecrement, как показано в главе 3. Любое другое специфическое для сервера состояние также должно быть защищено.
Разработчики, обозначающие свои классы ThreadingModel= «Apartment», указывают на то, что экземпляры этих классов могут быть доступны только из одного потока в течение всей жизни объекта. Следонательно, нет необходимости защищать режим экземпляра, а нужно защищать только режим, общий для нескольких экземпляров класса, о чем упоминалось ранее. Разработчики, обозначающие свои классы ThreadingModel="Free" или ThreadingModel="Both" устанавливают для экземпляров своего класса режим работы в МТА, что означает, что единовременно возможен доступ только к одному экземпляру класса. Поэтому разработчики должны защищать все ресурсы, используемые одним экземпляром, от параллельного доступа. Это относится не только к общим статическим переменным, но также к элементам данных экземпляра. Для объектов, расположенных в динамически распределяемой области памяти, это означает, что элемент данных, отвечающий за счетчик ссылок, должен быть защищён с помощью InterlockedIncrement/InterlockedDecrement, как было показано в главе 2. Любое другое состояние экземпляра класса также должно быть защищено.
На первый взгляд, совершенно не понятно, зачем существует ThreadingModel="Free", поскольку требования для работы в МТА выглядят как расширенный набор требований соответствия STA. Если разработчик объекта планирует рабочие потоки, которым необходим доступ к объекту, то очень полезно предотвратить создание объекта в каком-либо STA. Дело в том, что рабочие потоки не могут входить в STA, где живет объект, и поэтому вынуждены работать в другом апартаменте. Если класс обозначен ThreadingModel="Both" и запрос на активацию исходит от STA -потока, то объект будет существовать в STA. Это означает, что рабочие потоки (которые будут работать в МТА) должны обращаться к объекту через межапартаментные вызовы методов, значительно менее эффективные, нежели внутриапартаментные вызовы. Тем не менее, если класс помечен как ThreadingModel="Free", то любые запросы на активацию со стороны STA вызовут создание нового экземпляра в МТА, где любые рабочие потоки смогут иметь прямой доступ к объекту. Это означает, что при вызове клиентом, размещенным в STA, методов такого объекта, эффективность будет сниженной, в то время как рабочие потоки будут обрабатываться с большей эффективностью. Это является приемлемым компромиссом, если рабочие потоки будут обращаться к объекту чаще, чем действующий клиент из STA. Было бы весьма соблазнительно смягчить правила СОМ и записать, что не будет ошибкой прямо обращаться к некоторым объектам из более чем одного апартамента. Однако в общем случае это неверно, особенно для объектов, которые используют другие объекты для своей работы.
Межапартаментный доступ
Вспомогательные средства для внутрипроцессного маршалинга
Разработчики классов, не предусматривающие модель организации поточной обработки для своих классов, могут большей частью игнорировать проблемы, связанные с потоками, так как доступ к их библиотекам DLL будет осуществляться только из одного потока, а именно из главного STA–потока. Те разработчики, которые предусматривают для своих классов поддержку любой явной модели организации поточной обработки, косвенно свидетельствуют, что каждый из множественных апартаментов в процессе (который может быть многопоточным) может содержать экземпляры класса. Поэтому разработчик должен защитить любые ресурсы, которые совместно используются более чем одним экземпляром класса, от параллельного доступа. Это означает, что все глобальные и статические переменные должны быть защищены с помощью соответствующего примитива синхронизации потоков. Для внутрипроцессного сервера СОМ глобальный счетчик блокировок, отслеживающий время жизни сервера, должен быть защищен с помощью InterlockedIncrement / InterlockedDecrement, как показано в главе 3. Любое другое специфическое для сервера состояние также должно быть защищено.
Разработчики, обозначающие свои классы ThreadingModel= «Apartment», указывают на то, что экземпляры этих классов могут быть доступны только из одного потока в течение всей жизни объекта. Следонательно, нет необходимости защищать режим экземпляра, а нужно защищать только режим, общий для нескольких экземпляров класса, о чем упоминалось ранее. Разработчики, обозначающие свои классы ThreadingModel="Free" или ThreadingModel="Both" устанавливают для экземпляров своего класса режим работы в МТА, что означает, что единовременно возможен доступ только к одному экземпляру класса. Поэтому разработчики должны защищать все ресурсы, используемые одним экземпляром, от параллельного доступа. Это относится не только к общим статическим переменным, но также к элементам данных экземпляра. Для объектов, расположенных в динамически распределяемой области памяти, это означает, что элемент данных, отвечающий за счетчик ссылок, должен быть защищён с помощью InterlockedIncrement/InterlockedDecrement, как было показано в главе 2. Любое другое состояние экземпляра класса также должно быть защищено.
На первый взгляд, совершенно не понятно, зачем существует ThreadingModel="Free", поскольку требования для работы в МТА выглядят как расширенный набор требований соответствия STA. Если разработчик объекта планирует рабочие потоки, которым необходим доступ к объекту, то очень полезно предотвратить создание объекта в каком-либо STA. Дело в том, что рабочие потоки не могут входить в STA, где живет объект, и поэтому вынуждены работать в другом апартаменте. Если класс обозначен ThreadingModel="Both" и запрос на активацию исходит от STA -потока, то объект будет существовать в STA. Это означает, что рабочие потоки (которые будут работать в МТА) должны обращаться к объекту через межапартаментные вызовы методов, значительно менее эффективные, нежели внутриапартаментные вызовы. Тем не менее, если класс помечен как ThreadingModel="Free", то любые запросы на активацию со стороны STA вызовут создание нового экземпляра в МТА, где любые рабочие потоки смогут иметь прямой доступ к объекту. Это означает, что при вызове клиентом, размещенным в STA, методов такого объекта, эффективность будет сниженной, в то время как рабочие потоки будут обрабатываться с большей эффективностью. Это является приемлемым компромиссом, если рабочие потоки будут обращаться к объекту чаще, чем действующий клиент из STA. Было бы весьма соблазнительно смягчить правила СОМ и записать, что не будет ошибкой прямо обращаться к некоторым объектам из более чем одного апартамента. Однако в общем случае это неверно, особенно для объектов, которые используют другие объекты для своей работы.
Межапартаментный доступ
Для того чтобы объекты могли находиться в апартаментах, отличных от апартаментов клиента, в СОМ предусмотрена возможность экспорта интерфейсов из одного апартамента и импорта их в другой. Чтобы сделать интерфейс объекта видимым вне апартамента этого объекта, нужно экспортировать этот интерфейс. Чтобы сделать внешний интерфейс видимым внутри апартамента, нужно импортировать этот интерфейс. Когда интерфейс импортирован, то результирующий интерфейсный указатель ссылается на заместитель, доступ к которому разрешен для любого потока в импортирующем апартаменте[1]. Обязанностью заместителя является передача управления обратно в апартамент объекта для того, чтобы удостовериться, что все вызовы метода выполняются в нужном апартаменте. Эта передача управления от одного апартамента к другому называется удaленным вызовом метода (method remoting ) и является механизмом действия всех межпотоковых, межпроцессных и межмашинных связей в СОМ.
По умолчанию удаленный вызов метода использует имеющийся в СОМ протокол передачи ORPC (Object Remote Procedure Call – вызов объектом удаленной процедуры). СОМ ORPC является упрощенным протоколом MS-RPC (протокол вызова удаленной процедуры Microsoft), производным от DCE (Distributed Computing Environment – распределенная вычислительная среда). MS-RPC является независимым от протокола механизмом связи, который можно расширять с целью поддержки новых транспортных протоколов (посредством динамически загружаемых транспортных DLL) и новых пакетов аутентификации (посредством динамически загружаемых библиотек поставщика поддержки безопасности Security Support Provider DLL ). СОМ использует наиболее эффективный на доступных транспортных протоколов в зависимости от подобия и типов импортирующего и экспортирующего апартаментов. При связи вне хост-машины СОМ предпочитает UDP (User Datagram Protocol – протокол передачи дейтаграмм пользователя), хотя и поддерживает большинство общеупотребительных сетевых протоколов[2]. При локальной связи СОМ использует один из нескольких транспортных протоколов, каждый из которых оптимален для определенного типа апартаментов.
СОМ осуществляет передачу интерфейсных указателей через границы апартаментов с помощью особой технологии, именуемой маршалингом (marshaling), тo есть расположением в определенном порядке, выстраиванием. Маршалинг интерфейсного указателя – это преобразование его в передающийся байтовый поток, содержимое которого единственным образом идентифицирует объект и его собственный апартамент. Этот байтовый поток является маршалированным состоянием (marshaled state) интерфейсного указателя и дает возможность любому апартаменту импортировать интерфейсный указатель и осуществлять вызовы метода на объект. Отметим, что поскольку СОМ имеет дело исключительно с интерфейсными указателями, а не с самими объектами, это состояние маршалинга не представляет собой состояние объекта, а скорее преобразованное в последовательную форму (serialized) состояние не зависящей от апартаментов ссылки на объект. Такие маршалированные объектные ссылки просто содержат информацию об установлении связи, которая совершенно не зависит от состояния объекта.
Обычно указатели интерфейса маршалируются неявно как часть стандартной операции СОМ. Когда запрос на внутрипроцессную активацию сделан для класса с несовместимой моделью поточной обработки, СОМ неявно маршалирует интерфейс из апартамента объекта и демаршалирует заместитель в апартаменте клиента. Если сделан запрос на внепроцессную или внехостовую активацию, то СОМ также маршалирует результирующий указатель из апартамента объекта и демаршалирует заместитель для клиента. Если вызовы метода выполняются на заместители, то любые интерфейсные указатели, проходящие в качестве параметров метода, будут маршалированы с целью сделать объектные ссылки доступными в апартаментах и клиента, и объекта. Иногда необходимо маршалировать интерфейсы явным образом из одного апартамента в другой вне контекста запроса на активацию или вызова метода. Для поддержки этого режима в СОМ предусмотрена API-функция низкого уровня CoMarshalInterface , предназначенная для явного маршалинга интерфейсных указателей.
CoMarshalInterface принимает на входе интерфейсный указатель и записывает преобразованное в последовательную форму представление указателя в предоставленный вызывающим объектом байтовый поток. Этот байтовый поток может затем быть передан в другой апартамент, где API-функция CoUnmarshalInterface использует байтовый поток для возвращения интерфейсного указателя, который семантически эквивалентен исходному объекту, и к которому можно легально обращаться в апартаменте, выполняющем вызов функции CoUnmarshalInterface. При вызове CoMarshalInterface вызывающий объект должен указать, насколько далеко может располагаться импортирующий апартамент. В СОМ определен список рекомендуемых расстояний:
typedef enum tagMSHCTX
{
MSHCTX_INPROC = 4,
// in-process/same host
// внутрипроцессный/тот же хост
MSHCTX_LOCAL = 0,
// out-of-process/same host
// внепроцессный/тот же хост
MSHCTX_NOSHAREDMEM = 1,
// 16/32 bit/same host
// 16/32-битный/тот же хост
MSHCTX_DIFFERENTMACHINE = 2
// off-host
// внехостовый
} MSHCTX;
Допускается указывать большую дистанцию, чем необходимо, но для большей эффективности следует использовать по мере возможности корректное значение MSHCTX. CoMarshalInterface также позволяет вызывающему объекту специфицировать семантику маршалинга с помощью следующих специфических флагов:
typedef enum tagMSHLFLAGS
{
MSHLFLAGS_NORMAL,
// marshal once, unmarshal once
// маршалируем один раз, демаршалируем один раз
MSHLFLAGS_TABLESTRONG,
// marshal опсе, unmarshal many
// маршалируем один раз. демаршалируем много раз
MSHLFLAGS_TABLEWEAK,
// marshal once, unmarshal many
// маршалируем один раз, демаршалируем много раз
MSHLFLAGS_NOPING = 4,
// suppress dist. garbage collection
// подавляем ненужный набор дистанций
} MSHLFLAGS;
Нормальный (normal) маршалинг, иногда его называют еще маршалингом вызовов (call marshaling), означает, что маршалированная объектная ссылка должна быть демаршалирована только один раз, а если нужны дополнительные заместители, то требуются дополнительные вызовы CoMarshalInterface. Табличный (table) маршалинг означает, что маршалированная объектная ссылка может быть демаршалирована нуль и более раз без требования дополнительных вызовов CoMarshalInterface. Подробности табличного маршалинга будут описаны далее в этой главе.
Чтобы разрешить маршалинг интерфейсных указателей на различные носители, функция CoMarshalInterface преобразует интерфейсный указатель в последовательную форму через интерфейс типа IStream , предоставляемый вызывающим объектом. Интерфейс IStream моделирует произвольное устройство ввода-вывода и выставляет методы Read и Write . Функция CoMarshalInterface просто вызывает метод Write на предоставленный вызывающим объектом интерфейс типа IStream , не интересуясь тем, куда эти фактические байты будут записаны. Вызывающие объекты могут получить обертку IStream на необработанную (raw ) память, вызвав API-функцию CreateStreamOnHGlobal :
HRESULT CreateStreamOnHGlobal(
[in] HGLOBAL hglobal,
// pass null to autoalloc
// передаем нуль для автовыдепения памяти
[in] BOOL bFreeMemoryOnRelease,
[out] IStream **ppStm);
С использованием семантики IStream следующий фрагмент кода:
void UseRawMemoryToPrintString(void)
{
void *pv = 0;
// alloc memory
// выделяем память
pv = malloc(13);
if (pv != 0) {
// write a string to the underlying memory
// пишем строку в основную память
memcpy(pv, «Hello, World», 13);
printf((const char*)pv);
// free all resources
// освобождаем все ресурсы free (pv);
}
}
эквивалентен такому фрагменту кода, использующему интерфейс IStream вместо memcpy:
void UseStreamToPrintString(void)
{
IStream *pStm = 0;
// alloc memory and wrap behind an IStream interface
// выделяем память и затем заворачиваем ее в интерфейс
IStream HRESULT hr = CreateStreamOnHGlobal(0, TRUE, &pStm);
if (SUCCEEDED(hr)) {
// write a string to the underlying memory
// записываем строку в память
hr = pStm->Write(«Hello. World», 13, 0);
assert (SUCCEEDED (hr));
// suck out the memory
// извлекаем память
HGLOBAL hglobal = 0;
hr == GetHglobalFromStream(pStm, &hglobal);
assert(SUCCEEDED(hr));
printf((const char*)GlobalLock(hglobal));
// free all resources
// освобождаем все ресурсы
GlobalUnlock(hglobal); pStm->Release();
}
}
API-функция GetHGlobalFromStream позволяет вызывающему объекту получать дескриптор (handle ) памяти, выделенной функцией CreateStreamOnHGlobal. Использование HGLOBAL сложилось исторически и никоим образом не означает использование разделяемой памяти.
После осмысления всех типов параметров API-функции CoMarshalInterface она выглядит достаточно просто:
HRESULT CoMarshalInterface(
[in] IStream *pStm,
// where to write marshaled state
// куда записывать маршалированное состояние
[in] REFIID riid, // type of ptr being marshaled
// тип маршалируемого указателя
[in, iid_is(riid)] IUnknown *pItf,
// pointer being marshaled
// маршалируемый указатепь
[in] DWORD dwDestCtx,
// MSHCTX for destination apt.
// MSHCTX для апартамента адресата
[in] void *pvDestCtx,
// reserved, must be zero
// зарезервирован, должен равняться нулю
[in] DWORD dwMshlFlags
// normal, vs. table marshal
// нормальный маршалинг против табличного
);
Следующий код маршалирует интерфейсный указатель в блок памяти, пригодный для передачи по сети в любой апартамент:
HRESULT WritePtr(IRacer *pRacer, HGLOBAL& rhglobal)
{ IStream *pStm = 0; гhglobal = 0;
// alloc and wrap block of memory
// выделяем и заворачиваем блок памяти
HRESULT hr = CreateStreamOnHGlobal(0, FALSE, &pStm);
if (SUCCEEDED(hr)) {
// write marshaled object reference to memory
// записываем в память маршалированную объектную ссылку
hr = CoMarshalInterface(pStm, IID_Iracer, pRacer, MSHCTX_DIFFERENTMACHINE, 0, MSHLFLAGS_NORMAL);
// extract handle to underlying memory
// извлекаем дескриптор памяти
if (SUCCEEDED(hr)) hr = GetHGlobalFromStream(pStm, &rhglobal);
pStm->Release();
}
return hr;
}
Рисунок 5.1 иллюстрирует взаимоотношения между интерфейсным указателем и памятью, содержащей маршалированную объектную ссылку. После вызова CoMarshalInterface апартамент объекта готов получить от другого апартамента запрос на соединение. Поскольку был использован флаг MSHCTX_DIFFERENTMACHINE, то импортирующий апартамент может находиться на другой хост-машине.
Для того чтобы декодировать маршалированную объектную ссылку, созданную в предыдущем фрагменте кода, в нормальный интерфейсный указатель, импортирующему апартаменту необходимо вызвать API-функцию CoUnmarshalInterface :
HRESULT CoUnmarshalInterface(
[in] IStream *pStm,
// where to read marshaled state
// откуда читать маршалированное состояние
[in] REFIID riid, // type of ptr being unmaгshaled
// тип демаршалируемого указателя
[out, iid_is(riid)] void **ppv
// where to put unmarshaled ptr
// куда поместить демаршалированный указатель
);
CoUnmarshalInterface просто читает преобразованную в последовательную форму объектную ссылку и возвращает указатель на исходный объект, к которому есть легальный доступ в апартаменте вызывающего потока. Если импортирующий апартамент отличается от того апартамента, который изначально экспортировал интерфейс, то результирующий указатель будет указателем на заместитель. Если по какой-то причине вызов CoUnmarshalInterface осуществлен из исходного апартамента, где располагается объект, то в этом случае будет возвращен указатель на сам объект и не будет создано никакого заместителя. Следующий код переводит маршалированную объектную ссылку в нормальный указатель интерфейса:
HRESULT ReadPtr(HGLOBAL hglobal, IRacer * &rpRacer) {
IStream *pStm = 0; rpRacer = 0;
// wrap block of existing memory passed on input
// заключаем в оболочку блок существующей памяти,
// переданный на вход
HRESULT hr = CreateStreamOnHGlobal(hglobal, FALSE, &pStm);
if (SUCCEEDED(hr)) {
// get a pointer to the object that is legal in this apt.
// получаем указатель на объект, легальный в этом апартаменте
hr = CoUnmarshalInterface(pStm, IID_Iracer, (void**)&rpRacer);
pStm->Release();
}
return hr;
}
Результирующий заместитель будет реализовывать каждый из экспортируемых объектом интерфейсов путем переадресации запросов методов в апартамент объекта.
До появления выпуска СОМ под Windows NT 4.0 формат маршалированной объектной ссылки не был документирован. Для того чтобы позволить сторонним участникам создавать сетевые продукты, совместимые с СОМ, этот формат был документирован для открытого использования в 1996 году и представлен на рассмотрение как проект стандарта для Интернета. На рис. 5.2 показан формат маршалированной объектной ссылки. Заголовок маршалированной объектной ссылки начинается с изысканной сигнатуры «MEOW» (мяу)[3], а поле флагов указывает на выбранную технологию маршалинга (например, стандартный (standard), специальный (custom)), а также IID интерфейса, содержащегося в ссылке. В случае стандартного маршалинга подзаголовок объектной ссылки показывает, сколько внешних ссылок представляет данная маршалированная ссылка.
Этот счетчик внешних ссылок является частью распределенного протокола «сборки мусора» (garbage collection) СОМ и не полностью совпадает со счетчиком ссылок AddRef/Release, который может быть реализован объектом. Интересным элементом объектной ссылки является кортеж (tuple) OXID/OID/IPID, который единственным образом идентифицируют интерфейсный указатель. Каждому апартаменту в сети во время его создания присваивается уникальный Идентификатор Экспортера Объектов (Object Exporter Identifier – OXID). Этот OXID используется для нахождения сетевой или IPC -адресной информации при первом соединении заместителя с объектом. Идентификатор Объекта (Object Identifier – OID) единственным образом определяет идентификационную единицу СОМ в сети и использует CoUnmarshalInterface для поддержания правил идентификации СОМ для заместителей. Идентификатор Интерфейсного Указателя (Interface Pointer Identifier – IPID) единственным образом определяет интерфейсный указатель в апартаменте и помещается в заголовок каждого последующего запроса метода. IPID используется для эффективной диспетчеризации запросов ORPC на нужный интерфейсный указатель в апартаменте объекта.
Хотя OXID представляют интерес как логические идентификаторы, сами по себе они бесполезны, так как заместителям нужен какой-нибудь механизм IРС или сетевой протокол для связи с апартаментом объекта. Для перевода OXID в полностью квалифицированные адреса сети или IPC в каждой хост-машине, поддерживающей СОМ, существует специальная служба распознавателя OXID (OXID Resolver – OR). Под Windows NT 4.0 OR является частью службы RPCSS. Когда апартамент инициализируется впервые, СОМ назначает OXID и регистрирует его с помощью локального OR. Это означает, что каждый OR знает обо всех работающих апартаментах локальной системы. Кроме того, OR следит за каналом локального IPC-порта для каждого апартамента. Когда CoUnmarshalInterface требуется соединить новый заместитель с апартаментом объекта, то для преобразования OXID в действительный адрес сети или IРС используется локальный OR. Если демаршалированный указатель встречается на той же машине, где находится апартамент объекта, то OR просто ищет OXID в своей локальной OXID -таблице и возвращает локальный IPC-адрес. Если же демаршалированный указатель встречается не на той машине, на которой находится объект, то локальный OR сначала проверяет, встречался ли данный OXID ранее, для чего смотрит в специальном кэше, где хранятся недавно распознанные OXID удаленного доступа. Если OXID ранее не встречался, то он передает запрос OR на хост-машину объекта, используя RPC. Отметим, что маршалированная объектная ссылка содержит адрес хост-машины объекта в формате, подходящем для целого ряда сетевых протоколов, так что OR с демаршалированной стороны знает, куда направлять запрос.
Чтобы распознавать распределенные OXID, служба OR на каждой хост-машине ждет удаленные запросы на распознавание OXID на известном адресе (порт 135 для TCP и UDP) для каждого поддерживаемого сетевого протокола. Когда запрос на распознавание OXID получен из сети, опрашивается локальная таблица OXID. Запрос на распознавание покажет, какие сетевые протоколы поддерживает машина клиента. Если запрошенный процесс из апартамента еще не начал использовать один из запрошенных протоколов, то OR свяжется с СОМ-библиотекой в апартаменте объекта, используя локальный IРС, и инициирует использование запрошенного протокола в процессе объекта. Как только это произойдет, OR занесет новый сетевой адрес апартамента в локальную таблицу OXID. После записи новый сетевой адрес возвращается к OR демаршалирующей стороны, где он кэшируется, чтобы предотвратить дополнительные сетевые запросы на часто используемые апартаменты. Может показаться странным, что контрольное считывание полностью квалифицированных сетевых адресов в ссылке маршалировапного объекта не выполняется сразу. Но уровень косвенности, который допускают не зависящие от протокола идентификаторы апартаментов (OXID), разрешает процессу на основе СОМ откладывать использование сетевых протоколов до тех пор, пока они не потребуются. Это особенно важно, поскольку СОМ может иметь дело с множеством различных протоколов (не только TCP), и требовать от каждого процесса слежения за запросами с использованием всех поддерживаемых протоколов было бы чрезвычайно неэффективно. Фактически, если СОМ-процесс никогда не экспортирует указатели внехостовым клиентам, он никогда не потратит вообще никаких сетевых ресурсов.
По умолчанию удаленный вызов метода использует имеющийся в СОМ протокол передачи ORPC (Object Remote Procedure Call – вызов объектом удаленной процедуры). СОМ ORPC является упрощенным протоколом MS-RPC (протокол вызова удаленной процедуры Microsoft), производным от DCE (Distributed Computing Environment – распределенная вычислительная среда). MS-RPC является независимым от протокола механизмом связи, который можно расширять с целью поддержки новых транспортных протоколов (посредством динамически загружаемых транспортных DLL) и новых пакетов аутентификации (посредством динамически загружаемых библиотек поставщика поддержки безопасности Security Support Provider DLL ). СОМ использует наиболее эффективный на доступных транспортных протоколов в зависимости от подобия и типов импортирующего и экспортирующего апартаментов. При связи вне хост-машины СОМ предпочитает UDP (User Datagram Protocol – протокол передачи дейтаграмм пользователя), хотя и поддерживает большинство общеупотребительных сетевых протоколов[2]. При локальной связи СОМ использует один из нескольких транспортных протоколов, каждый из которых оптимален для определенного типа апартаментов.
СОМ осуществляет передачу интерфейсных указателей через границы апартаментов с помощью особой технологии, именуемой маршалингом (marshaling), тo есть расположением в определенном порядке, выстраиванием. Маршалинг интерфейсного указателя – это преобразование его в передающийся байтовый поток, содержимое которого единственным образом идентифицирует объект и его собственный апартамент. Этот байтовый поток является маршалированным состоянием (marshaled state) интерфейсного указателя и дает возможность любому апартаменту импортировать интерфейсный указатель и осуществлять вызовы метода на объект. Отметим, что поскольку СОМ имеет дело исключительно с интерфейсными указателями, а не с самими объектами, это состояние маршалинга не представляет собой состояние объекта, а скорее преобразованное в последовательную форму (serialized) состояние не зависящей от апартаментов ссылки на объект. Такие маршалированные объектные ссылки просто содержат информацию об установлении связи, которая совершенно не зависит от состояния объекта.
Обычно указатели интерфейса маршалируются неявно как часть стандартной операции СОМ. Когда запрос на внутрипроцессную активацию сделан для класса с несовместимой моделью поточной обработки, СОМ неявно маршалирует интерфейс из апартамента объекта и демаршалирует заместитель в апартаменте клиента. Если сделан запрос на внепроцессную или внехостовую активацию, то СОМ также маршалирует результирующий указатель из апартамента объекта и демаршалирует заместитель для клиента. Если вызовы метода выполняются на заместители, то любые интерфейсные указатели, проходящие в качестве параметров метода, будут маршалированы с целью сделать объектные ссылки доступными в апартаментах и клиента, и объекта. Иногда необходимо маршалировать интерфейсы явным образом из одного апартамента в другой вне контекста запроса на активацию или вызова метода. Для поддержки этого режима в СОМ предусмотрена API-функция низкого уровня CoMarshalInterface , предназначенная для явного маршалинга интерфейсных указателей.
CoMarshalInterface принимает на входе интерфейсный указатель и записывает преобразованное в последовательную форму представление указателя в предоставленный вызывающим объектом байтовый поток. Этот байтовый поток может затем быть передан в другой апартамент, где API-функция CoUnmarshalInterface использует байтовый поток для возвращения интерфейсного указателя, который семантически эквивалентен исходному объекту, и к которому можно легально обращаться в апартаменте, выполняющем вызов функции CoUnmarshalInterface. При вызове CoMarshalInterface вызывающий объект должен указать, насколько далеко может располагаться импортирующий апартамент. В СОМ определен список рекомендуемых расстояний:
typedef enum tagMSHCTX
{
MSHCTX_INPROC = 4,
// in-process/same host
// внутрипроцессный/тот же хост
MSHCTX_LOCAL = 0,
// out-of-process/same host
// внепроцессный/тот же хост
MSHCTX_NOSHAREDMEM = 1,
// 16/32 bit/same host
// 16/32-битный/тот же хост
MSHCTX_DIFFERENTMACHINE = 2
// off-host
// внехостовый
} MSHCTX;
Допускается указывать большую дистанцию, чем необходимо, но для большей эффективности следует использовать по мере возможности корректное значение MSHCTX. CoMarshalInterface также позволяет вызывающему объекту специфицировать семантику маршалинга с помощью следующих специфических флагов:
typedef enum tagMSHLFLAGS
{
MSHLFLAGS_NORMAL,
// marshal once, unmarshal once
// маршалируем один раз, демаршалируем один раз
MSHLFLAGS_TABLESTRONG,
// marshal опсе, unmarshal many
// маршалируем один раз. демаршалируем много раз
MSHLFLAGS_TABLEWEAK,
// marshal once, unmarshal many
// маршалируем один раз, демаршалируем много раз
MSHLFLAGS_NOPING = 4,
// suppress dist. garbage collection
// подавляем ненужный набор дистанций
} MSHLFLAGS;
Нормальный (normal) маршалинг, иногда его называют еще маршалингом вызовов (call marshaling), означает, что маршалированная объектная ссылка должна быть демаршалирована только один раз, а если нужны дополнительные заместители, то требуются дополнительные вызовы CoMarshalInterface. Табличный (table) маршалинг означает, что маршалированная объектная ссылка может быть демаршалирована нуль и более раз без требования дополнительных вызовов CoMarshalInterface. Подробности табличного маршалинга будут описаны далее в этой главе.
Чтобы разрешить маршалинг интерфейсных указателей на различные носители, функция CoMarshalInterface преобразует интерфейсный указатель в последовательную форму через интерфейс типа IStream , предоставляемый вызывающим объектом. Интерфейс IStream моделирует произвольное устройство ввода-вывода и выставляет методы Read и Write . Функция CoMarshalInterface просто вызывает метод Write на предоставленный вызывающим объектом интерфейс типа IStream , не интересуясь тем, куда эти фактические байты будут записаны. Вызывающие объекты могут получить обертку IStream на необработанную (raw ) память, вызвав API-функцию CreateStreamOnHGlobal :
HRESULT CreateStreamOnHGlobal(
[in] HGLOBAL hglobal,
// pass null to autoalloc
// передаем нуль для автовыдепения памяти
[in] BOOL bFreeMemoryOnRelease,
[out] IStream **ppStm);
С использованием семантики IStream следующий фрагмент кода:
void UseRawMemoryToPrintString(void)
{
void *pv = 0;
// alloc memory
// выделяем память
pv = malloc(13);
if (pv != 0) {
// write a string to the underlying memory
// пишем строку в основную память
memcpy(pv, «Hello, World», 13);
printf((const char*)pv);
// free all resources
// освобождаем все ресурсы free (pv);
}
}
эквивалентен такому фрагменту кода, использующему интерфейс IStream вместо memcpy:
void UseStreamToPrintString(void)
{
IStream *pStm = 0;
// alloc memory and wrap behind an IStream interface
// выделяем память и затем заворачиваем ее в интерфейс
IStream HRESULT hr = CreateStreamOnHGlobal(0, TRUE, &pStm);
if (SUCCEEDED(hr)) {
// write a string to the underlying memory
// записываем строку в память
hr = pStm->Write(«Hello. World», 13, 0);
assert (SUCCEEDED (hr));
// suck out the memory
// извлекаем память
HGLOBAL hglobal = 0;
hr == GetHglobalFromStream(pStm, &hglobal);
assert(SUCCEEDED(hr));
printf((const char*)GlobalLock(hglobal));
// free all resources
// освобождаем все ресурсы
GlobalUnlock(hglobal); pStm->Release();
}
}
API-функция GetHGlobalFromStream позволяет вызывающему объекту получать дескриптор (handle ) памяти, выделенной функцией CreateStreamOnHGlobal. Использование HGLOBAL сложилось исторически и никоим образом не означает использование разделяемой памяти.
После осмысления всех типов параметров API-функции CoMarshalInterface она выглядит достаточно просто:
HRESULT CoMarshalInterface(
[in] IStream *pStm,
// where to write marshaled state
// куда записывать маршалированное состояние
[in] REFIID riid, // type of ptr being marshaled
// тип маршалируемого указателя
[in, iid_is(riid)] IUnknown *pItf,
// pointer being marshaled
// маршалируемый указатепь
[in] DWORD dwDestCtx,
// MSHCTX for destination apt.
// MSHCTX для апартамента адресата
[in] void *pvDestCtx,
// reserved, must be zero
// зарезервирован, должен равняться нулю
[in] DWORD dwMshlFlags
// normal, vs. table marshal
// нормальный маршалинг против табличного
);
Следующий код маршалирует интерфейсный указатель в блок памяти, пригодный для передачи по сети в любой апартамент:
HRESULT WritePtr(IRacer *pRacer, HGLOBAL& rhglobal)
{ IStream *pStm = 0; гhglobal = 0;
// alloc and wrap block of memory
// выделяем и заворачиваем блок памяти
HRESULT hr = CreateStreamOnHGlobal(0, FALSE, &pStm);
if (SUCCEEDED(hr)) {
// write marshaled object reference to memory
// записываем в память маршалированную объектную ссылку
hr = CoMarshalInterface(pStm, IID_Iracer, pRacer, MSHCTX_DIFFERENTMACHINE, 0, MSHLFLAGS_NORMAL);
// extract handle to underlying memory
// извлекаем дескриптор памяти
if (SUCCEEDED(hr)) hr = GetHGlobalFromStream(pStm, &rhglobal);
pStm->Release();
}
return hr;
}
Рисунок 5.1 иллюстрирует взаимоотношения между интерфейсным указателем и памятью, содержащей маршалированную объектную ссылку. После вызова CoMarshalInterface апартамент объекта готов получить от другого апартамента запрос на соединение. Поскольку был использован флаг MSHCTX_DIFFERENTMACHINE, то импортирующий апартамент может находиться на другой хост-машине.
Для того чтобы декодировать маршалированную объектную ссылку, созданную в предыдущем фрагменте кода, в нормальный интерфейсный указатель, импортирующему апартаменту необходимо вызвать API-функцию CoUnmarshalInterface :
HRESULT CoUnmarshalInterface(
[in] IStream *pStm,
// where to read marshaled state
// откуда читать маршалированное состояние
[in] REFIID riid, // type of ptr being unmaгshaled
// тип демаршалируемого указателя
[out, iid_is(riid)] void **ppv
// where to put unmarshaled ptr
// куда поместить демаршалированный указатель
);
CoUnmarshalInterface просто читает преобразованную в последовательную форму объектную ссылку и возвращает указатель на исходный объект, к которому есть легальный доступ в апартаменте вызывающего потока. Если импортирующий апартамент отличается от того апартамента, который изначально экспортировал интерфейс, то результирующий указатель будет указателем на заместитель. Если по какой-то причине вызов CoUnmarshalInterface осуществлен из исходного апартамента, где располагается объект, то в этом случае будет возвращен указатель на сам объект и не будет создано никакого заместителя. Следующий код переводит маршалированную объектную ссылку в нормальный указатель интерфейса:
HRESULT ReadPtr(HGLOBAL hglobal, IRacer * &rpRacer) {
IStream *pStm = 0; rpRacer = 0;
// wrap block of existing memory passed on input
// заключаем в оболочку блок существующей памяти,
// переданный на вход
HRESULT hr = CreateStreamOnHGlobal(hglobal, FALSE, &pStm);
if (SUCCEEDED(hr)) {
// get a pointer to the object that is legal in this apt.
// получаем указатель на объект, легальный в этом апартаменте
hr = CoUnmarshalInterface(pStm, IID_Iracer, (void**)&rpRacer);
pStm->Release();
}
return hr;
}
Результирующий заместитель будет реализовывать каждый из экспортируемых объектом интерфейсов путем переадресации запросов методов в апартамент объекта.
До появления выпуска СОМ под Windows NT 4.0 формат маршалированной объектной ссылки не был документирован. Для того чтобы позволить сторонним участникам создавать сетевые продукты, совместимые с СОМ, этот формат был документирован для открытого использования в 1996 году и представлен на рассмотрение как проект стандарта для Интернета. На рис. 5.2 показан формат маршалированной объектной ссылки. Заголовок маршалированной объектной ссылки начинается с изысканной сигнатуры «MEOW» (мяу)[3], а поле флагов указывает на выбранную технологию маршалинга (например, стандартный (standard), специальный (custom)), а также IID интерфейса, содержащегося в ссылке. В случае стандартного маршалинга подзаголовок объектной ссылки показывает, сколько внешних ссылок представляет данная маршалированная ссылка.
Этот счетчик внешних ссылок является частью распределенного протокола «сборки мусора» (garbage collection) СОМ и не полностью совпадает со счетчиком ссылок AddRef/Release, который может быть реализован объектом. Интересным элементом объектной ссылки является кортеж (tuple) OXID/OID/IPID, который единственным образом идентифицируют интерфейсный указатель. Каждому апартаменту в сети во время его создания присваивается уникальный Идентификатор Экспортера Объектов (Object Exporter Identifier – OXID). Этот OXID используется для нахождения сетевой или IPC -адресной информации при первом соединении заместителя с объектом. Идентификатор Объекта (Object Identifier – OID) единственным образом определяет идентификационную единицу СОМ в сети и использует CoUnmarshalInterface для поддержания правил идентификации СОМ для заместителей. Идентификатор Интерфейсного Указателя (Interface Pointer Identifier – IPID) единственным образом определяет интерфейсный указатель в апартаменте и помещается в заголовок каждого последующего запроса метода. IPID используется для эффективной диспетчеризации запросов ORPC на нужный интерфейсный указатель в апартаменте объекта.
Хотя OXID представляют интерес как логические идентификаторы, сами по себе они бесполезны, так как заместителям нужен какой-нибудь механизм IРС или сетевой протокол для связи с апартаментом объекта. Для перевода OXID в полностью квалифицированные адреса сети или IPC в каждой хост-машине, поддерживающей СОМ, существует специальная служба распознавателя OXID (OXID Resolver – OR). Под Windows NT 4.0 OR является частью службы RPCSS. Когда апартамент инициализируется впервые, СОМ назначает OXID и регистрирует его с помощью локального OR. Это означает, что каждый OR знает обо всех работающих апартаментах локальной системы. Кроме того, OR следит за каналом локального IPC-порта для каждого апартамента. Когда CoUnmarshalInterface требуется соединить новый заместитель с апартаментом объекта, то для преобразования OXID в действительный адрес сети или IРС используется локальный OR. Если демаршалированный указатель встречается на той же машине, где находится апартамент объекта, то OR просто ищет OXID в своей локальной OXID -таблице и возвращает локальный IPC-адрес. Если же демаршалированный указатель встречается не на той машине, на которой находится объект, то локальный OR сначала проверяет, встречался ли данный OXID ранее, для чего смотрит в специальном кэше, где хранятся недавно распознанные OXID удаленного доступа. Если OXID ранее не встречался, то он передает запрос OR на хост-машину объекта, используя RPC. Отметим, что маршалированная объектная ссылка содержит адрес хост-машины объекта в формате, подходящем для целого ряда сетевых протоколов, так что OR с демаршалированной стороны знает, куда направлять запрос.
Чтобы распознавать распределенные OXID, служба OR на каждой хост-машине ждет удаленные запросы на распознавание OXID на известном адресе (порт 135 для TCP и UDP) для каждого поддерживаемого сетевого протокола. Когда запрос на распознавание OXID получен из сети, опрашивается локальная таблица OXID. Запрос на распознавание покажет, какие сетевые протоколы поддерживает машина клиента. Если запрошенный процесс из апартамента еще не начал использовать один из запрошенных протоколов, то OR свяжется с СОМ-библиотекой в апартаменте объекта, используя локальный IРС, и инициирует использование запрошенного протокола в процессе объекта. Как только это произойдет, OR занесет новый сетевой адрес апартамента в локальную таблицу OXID. После записи новый сетевой адрес возвращается к OR демаршалирующей стороны, где он кэшируется, чтобы предотвратить дополнительные сетевые запросы на часто используемые апартаменты. Может показаться странным, что контрольное считывание полностью квалифицированных сетевых адресов в ссылке маршалировапного объекта не выполняется сразу. Но уровень косвенности, который допускают не зависящие от протокола идентификаторы апартаментов (OXID), разрешает процессу на основе СОМ откладывать использование сетевых протоколов до тех пор, пока они не потребуются. Это особенно важно, поскольку СОМ может иметь дело с множеством различных протоколов (не только TCP), и требовать от каждого процесса слежения за запросами с использованием всех поддерживаемых протоколов было бы чрезвычайно неэффективно. Фактически, если СОМ-процесс никогда не экспортирует указатели внехостовым клиентам, он никогда не потратит вообще никаких сетевых ресурсов.
Вспомогательные средства для внутрипроцессного маршалинга
Хотя фрагменты кода для WritePtr и ReadPtr из предыдущего раздела достаточно просто реализовать, большинство явных вызовов CoMarshalInterface будут использоваться для передачи интерфейсного указателя от одного потока к другому в том же самом процессе. Для упрощения этой задачи в СОМ предусмотрены две оберточные функции (wrapper functions), которые реализуют нужный стандартный код вокруг CoMarshalInterface и CoUnmarshalInterface. API-функция СОМ CoMarshalInterThreadInterfaceInStream
HRESULT CoMarshalInterThreadInterfaceInStream( [in] REFIID riid, [in, iid_is(riid)] IUnknown *pItf, [out] IStream **ppStm );
обеспечивает простую обертку вокруг CreateStreamOnHGlobal и CoMarshalInterface, как показано ниже:
// from OLE32.DLL (approx.)
// из OLE32.DLL (приблизительно)
HRESULT CoMarshalInterThreadInterfaceInStream( REFIID riid, IUnknown *pItf, IStream **ppStm) {
HRESULT hr = CreateStreamOnHGlobal(0, TRUE, ppStm);
if (SUCCEEDED(hr))
hr = CoMarshalInterface(*ppStm, riid, pItf, MSHCTX_INPROC, 0, MSHLFLAGS_NORMAL);
return hr;
}
В СОМ предусмотрена также обертка вокруг CoUnmarshalInterface:
HRESULT CoMarshalInterThreadInterfaceInStream( [in] REFIID riid, [in, iid_is(riid)] IUnknown *pItf, [out] IStream **ppStm );
обеспечивает простую обертку вокруг CreateStreamOnHGlobal и CoMarshalInterface, как показано ниже:
// from OLE32.DLL (approx.)
// из OLE32.DLL (приблизительно)
HRESULT CoMarshalInterThreadInterfaceInStream( REFIID riid, IUnknown *pItf, IStream **ppStm) {
HRESULT hr = CreateStreamOnHGlobal(0, TRUE, ppStm);
if (SUCCEEDED(hr))
hr = CoMarshalInterface(*ppStm, riid, pItf, MSHCTX_INPROC, 0, MSHLFLAGS_NORMAL);
return hr;
}
В СОМ предусмотрена также обертка вокруг CoUnmarshalInterface: