WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 |   ...   | 3 | 4 ||

«Основы 2-е издание, исправленное и переработанное Дейл Роджерсон Оглавление ОТ АВТОРА ...»

-- [ Страница 5 ] --

// Подготовить параметры и осуществить вызов // Выделить и инициализировать аргумент VARIANT VARIANTARG varg;

::VariantInit(&varg);

// Инициализировать VARIANT.

// Тип данных VARIANT varg.vt = VT_BSTR;

// Данные для VARIANT varg.bstrVal = bstrIn;

// Заполнить структуру DISPPARAMS DISPPARAMS param;

// Один аргумент param.cArgs = 1;

// Указатель на аргумент param.rgvarg = &varg;

// Нет именованных аргументов param.cNamedArgs = 0;

param.rgdispidNamedArgs = NULL;

trace("Вызвать метод \"FxStringIn\".");

hr = pIDispatch->Invoke(dispid, IID_NULL, GetUserDefaultLCID(), DISPATCH_METHOD, ¶m, NULL, NULL, NULL);

// Очистка ::SysFreeString(bstrIn);

На заполнение структур VARIANTARG и DISPPARAMS может уйти много строк. К счастью, Вы можете написать вспомогательные функции, которые значительно упростят вызов Invoke. Некоторые подобные функции можно найти внутри MFC. Кроме того, ClassWizard генерирует для disp-интерфейсов класс-оболочку C++.

Подобные классы содержат удобные для работы на С++ функции, которые преобразуют свои параметры в формат, необходимый для вызова Invoke.

Давайте воспользуемся случаем более подробно рассмотреть тип VARIANT. При рассмотрении типов мы также кратко познакомимся с типами BSTR и SAFEARRAY.

Тип VARIANT Мы уже видели, как выглядит структура VARIANT (или VARIANTARG). Теперь давайте несколько подробнее рассмотрим, как она используется. Как видно из предыдущего фрагмента кода, структура VARIANT инициализируется при помощи VariantInit. Эта функция устанавливает поле vt в VT_EMPTY. После вызова VariantInit поле vt используется для указания типа данных, хранящихся в объединении VARIANT. В предыдущем примере мы сохраняли BSTR и поэтому использовали поле bstrVal.

Позднее связывание При использовании класса С++ или интерфейса СОМ все параметры функций класса или интерфейса описываются в заголовочном файле. На этапе компиляции компилятор проверяет, чтобы каждой функции передавались параметры надлежащих типов. Строгая типизация — важное средство повышения надежности программ. Однако, это средство может оказаться слишком сильным, если Вы хотите написать простой макрос, где гибкость и простота важнее надежности.

Возможно, Вы не обратили внимания, но мы не предоставляли Visual Basic ничего, эквивалентного заголовочному файлу C++. Для того, чтобы разрешить программе вызвать метод диспетчерского интерфейса, Visual Basic не требуется знание аргументов этого метода. Достигается это при помощи структуры VARIANT.

Пусть в программе на Visual Basic имеется следующий фрагмент:

Dim Bullwinkle As Object Set Bullwinkle = CreateObject(“TalkingMoose”) Bullwinkle.PullFromHat 1, “Topolino” Visual Basic не требуется знать о Bullwinkle ничего, кроме того, что тот поддерживает IDispatch. Поскольку же Bullwinkle поддерживает IDispatch, Visual Basic может получить DISPID для PullFromHat с помощью вызова IDispatch::GetIDsOfNames. Но у него нет никакой информации об аргументах PullFromHat. Здесь можно прибегнуть к помощи библиотеки типа, независимого от языка программирования эквивалента заголовочного файла С++. Мы будем рассматривать библиотеки типа далее в этой главе.

Но на самом деле Visual Basic не требует, чтобы ему сообщили допустимые типы параметров (через заголовочный файл или некий его эквивалент). Он может взять аргументы, введенные пользователем, и «засунуть» их в VARIANT. В предыдущем примере Visual Basic может предположить, что тип первого параметра — long, а второго — BSTR. Затем созданные таким образом «варианты» передаются функции Invoke. Если типы параметров не совпадают, сервер Автоматизации возвратит ошибки, возможно, вместе с индексом неправильного параметра. Конечно, программисту в любом случае необходима некая документация функций, чтобы знать, как их вызывать, но самой программе никакая информация о типе не требуется. Использование VARIANT позволяет практически полностью отказаться от статической проверки типов — за счет того, что компонент будет проверять их во время выполнения. Это более похоже на Smalltalk, где нет проверки типов, чем на строгую типизацию С++.

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

Преобразование типов Если хорошие disp-интерфейсы возвращают код ошибки при получении параметров неадекватного типа, то очень хорошие выполняют преобразование типа полученные аргументов за программиста. Возьмем функцию PullFromHat из предыдущего фрагмента. Visual Basic мог предположить, что функция принимает long и BSTR.

Но может оказаться, что на самом деле функция принимает не long, а double. Disp-интерфейс должен уметь выполнять такое преобразование автоматически. Кроме того, disp-интерфейсы должны выполнять преобразование в BSTR и из BSTR. Например, если функция установки значения свойства, описанная в IDL так:

[propput] HRESULT Title([in] BSTR bstrTitle);

вызывается следующим кодом на Visual Basic:

component.Title = то эта функция должна преобразовать число 100 в BSTR и использовать результат преобразования в качестве заголовка (title). И, наоборот, функция установки значения свойства:

[propput] HRESULT Age([in] short sAge);

должна быть способна корректно выполнить следующий вызов из Visual Basic:

component.Age = “16” Я не сомневаюсь, что у Вас нет никакого желания писать код этих преобразований. Даже если у Вас оно есть, то у других его нет. Хуже того, если преобразования будут писать все, все преобразования будут разными. В результате какие-то методы и свойства будут выполнять преобразования одним способом, а какие-то — другим.

Поэтому Автоматизация предоставляет функцию VariantChangeType, которая выполняет преобразование за Вас:

HRESULT VariantChangeType( // Преобразованное значение VARIANTARG* pVarDest, // Исходное значение VARIANTARG* pVarSrc, unsigned short wFlags, // Целевой тип преобразования VARTYPE vtNew } Пользоваться этой функцией очень легко. Например, приведенная ниже процедура преобразует VARIANT в double с помощью VariantChangeType:

BOOL VariantToDouble(VARIANTARG* pvarSrc, double dp) { VARIANTARG varDest;

VariantInit(&varDest);

HRESULT hr = VariantChangeType(&varDest, pvarSrc, 0, VT_R8);

if (FAILED(hr)) { return FALSE;

} *pd = varDest.dblVal;

return TRUE;

} Необязательные аргументы Метод disp-интерфейса может иметь необязательные аргументы. Если Вы не хотите задавать значение такого аргумента, просто передайте вместо него VARIANT с полем vt, установленным в VT_ERROR, и полем scode, равным DISP_E_PARAMNOTFOUND. В этом случае вызываемый метод должен использовать собственное значени по умолчанию.

Теперь давайте рассмотрим BSTR.

Тип данных BSTR BSTR, сокращение от Basic STRing или Binary STRing (в зависимости от того, кого Вы спросите) — это указатель на строку символов Unicode. У BSTR есть три интересных особенности. Во-первых, в BSTR хранится число символов строки. Вторая важная особенность — то, что число хранится перед самим массивом символов (рис. 11 4). Следовательно, нельзя объявить переменную типа BSTR и инициализировать ее массивом символов:

BSTR bstr = L“Где же счетчик?”;

// Неправильно Поскольку при этом не будет инициализирован счетчик. Вместо этого следует использовать функцию API Win SysAllocString:

wchar_t wsz[] = L“Вот где счетчик”;

BSTR bstr;

bstr = SysAllocString(wsz);

BSTR Массив символов Unicode Счетчик символов 'a' 'b' 'c' '\0' 'd' 'd' 'f' '\0' 'A' 'B' 'X' 'Y' 'Z' '\0' Может содержать несколько нулевых символов Рис. 11-4 Счетчик символов хранится перед тем участком памяти, на который указывает BSTR По окончании использования BSTR следует освободить с помощью SysFreeString. Преобразовать BSTR обратно в строку wchar_t легко;

в конце концов, BSTR указывает на начало массива wchar_t. Но у BSTR есть и третья интересная черта — в строке может содержаться несколько символов ‘\0’. Следовательно, Вы должны писать код, готовый к обработке нескольких символов ‘\0’, если для Вашей функции это имеет смысл.

Тип данных SAFEARRAY Другой специальный тип данных, который можно передавать disp-интерфейсу, — SAFEARRAY. Как следует из названия*, это массив, содержащий информацию о своих границах. Ниже приведено объявление из OAIDL.IDL:

typedef struct tagSAFEARRAY { // Число измерений unsigned short cDims;

unsigned short fFeatures;

// Размер каждого элемента unsigned long cbElements;

// Счетчик блокировок unsigned long clocks;

// Указатель на данные BYTE* pvData;

[size_is(cDims) SAFEARRAYBOUND rgsabound[];

} SAFEARRAY;

typedef struct tagSAFEARRAYBOUND { // Число элементов в данном измерении ULONG cElements;

// Нижняя граница по данному измерению LONG lLBound;

} SAFEARRAYBOUND;

Поле fFeatures описывает, какого типа данные хранятся в SAFEARRAY. Возможны следующие значения:

* «Безопасный массив». — Прим.перев.

FADF_BSTR Массив BSTR FAFD_UNKNOWN Массив IUnknown* FADF_DISPATCH Массив IDispatch* FADF_VARIANT Массив VARIANT Это поле также описывает, как массив был выделен:

FADF_AUTO Массив размещен в стеке FADF_STATIC Массив размещен статически FADF_EMBEDDED Массив входит в структуру FADF_FIXEDSIZE Размер и местоположение массива нельзя менять Библиотека Автоматизации — OLEAUT32.DLL — включает целый ряд функций для манипулирования SAFEARRAY. Названия всех таких функций начинаются с префикса SafeArray. Поищите их сами в диалоговой справочной системе.

Мы знаем, как заполнять переменные VARIANT, которые используются для построения структуры DISPPARAMS, которая передается IDispatch::Invoke, с помощью которой мы можем вызвать диспетчерские методы и получать доступ к диспетчерским свойствам. Теперь пришла пора кратко рассмотреть библиотеки типа — независимый от языка эквивалент заголовочных файлов C++.

Библиотеки типа Как мы уже видели, программа на Visual Basic или С++ может управлять компонентом через disp-интерфейс, ничего не зная о типах, связанных с этим интерфейсом или его методами. Однако, если Вы можете засунуть горошину в ухо, это не означает, что так и следует поступать. Точно так же, если Вы можете писать программу на Visual Basic без информации о типах, это не означает, что так и надо делать.

Описанные в предыдущем разделе проверка и преобразование типов VARIANT на этапе выполнения требуют много времени и могут привести к скрытым ошибкам в программе. Программист может случайно перепутать местами два параметра в вызове функции, и компонент успешно преобразует их типы. Большое преимущество С++ перед С — более строгая проверка типов;

она до некоторой степени обеспечивает уверенность в том, что программа работает, как предполагалось.

Нам нужен независимый от языка эквивалент заголовочных файлов С++, который подходил бы для интерпретируемых языков и сред макропрограммирования. Решение есть — библиотека типа (type library) СОМ, которая предоставляет информацию типа о компонентах, интерфейсах, методах, свойствах, аргументах и структурах. Содержимое библиотеки типа аналогично содержимому заголовочного файла С++. Библиотека типа — это откомпилированная версия файла IDL, к которой возможен доступ из программы. Это не текст на каком-то языке, требующий синтаксического разбора, а двоичный файл. Библиотека Автоматизации предоставляет стандартные компоненты для создания и чтения таких двоичных файлов.

Без библиотеки типа возможности Visual Basic работать с компонентами ограничены disp-интерфейсами. Если же библиотека типа имеется, Visual Basic может работать с компонентом напрямую через vtbl дуального интрфейса.

Доступ через vtbl быстрее, и он безопаснее с точки зрения приведения типа.

Да, пока не забыл, — библиотека типа может также содержать строки справочной информации для всех содержащихся в ней компонентов, интерфейсов и функций. С помощью средства просмотра объектов, подобного имеющемуся в Visual Basic, программист может легко получить подсказку о любом свойстве или методе. Не правда ли, замечательно?

Создание библиотеки типа Библиотеку типа создает функция CreateTypeLib из библиотеки Автоматизации. CreateTypeLib возвращает интерфейс IcreateTypeLib, который можно использовать для занесения в библиотеку различной информации.

Вряд ли Вам когда-нибудь потребуется использовать этот интерфейс;

вместо него можно пользоваться IDL и компилятором MIDL. В гл. 10 мы использовали IDL и компилятор MIDL для генерации кода DLL заместителя/заглушки, но они подходят и для генерации библиотек типа.

ODL и MkTypLib В «старые» времена компилятор MIDL нельзя было использовать для генерации библиотек типа. Вместо описания библиотек на IDL приходилось использовать другой язык — ODL. ODL компилировался в библиотеку типа с помощью программы MkTypLib. ODL был похож на IDL, но отличий было достаточно, чтобы затруднить их совместное использование. Поддержка двух файлов, содержащих одну и ту же информацию, — также напрасный расход времени. К счастью, IDL и MIDL при разработке Windows NT 4. были расширены для поддержки создания библиотек типа. Теперь ODL и MkTypLib стали не нужны и более не используются.

Оператор library Основа создания библиотеки типа при помощи IDL — оператор library. Все, что находится внутри блока кода, ограниченного фигурными скобками, которые следуют за ключевым словом library, будет компилироваться в библиотеку типа. Файл IDL из примера гл. 11 показан в листинге 11-1. как видите, у библиотеки типа есть свои GUID, версия и helpstring.

SERVER.IDL // // Server.idl – Исходный файл IDL для Server.dll // // Этот файл будет обрабатываться компилятором MIDL для // генерации библиотеки типа (Server.tlb) кода маршалинга.

// // Интерфейс IX [ object, uuid(32BB8326-B41B-11CF-A6BB-0080C7B2D682), helpstring("Интерфейс IX"), pointer_default(unique), dual, oleautomation ] interface IX : IDispatch { import "oaidl.idl";

HRESULT Fx();

HRESULT FxStringIn([in] BSTR bstrIn);

HRESULT FxStringOut([out, retval] BSTR* pbstrOut);

HRESULT FxFakeError();

};

// // Описание компонента и библиотеки типа // [ uuid(D3011EE1-B997-11CF-A6BB-0080C7B2D682), version(1.0), helpstring("Основы COM, Глава 11 1.0 Библиотека типа") ] library ServerLib { importlib("stdole32.tlb");

// Компонент [ uuid(0C092C2C-882C-11CF-A6BB-0080C7B2D682), helpstring("Класс компонента") ] coclass Component { [default] interface IX;

};

};

Листинг 11-1 Файл IDL, используемый для генерации библиотеки SERVER.TLB Оператор coclass определяет компонент;

в данном случае это Component с единственным интерфейсом IX.

Компилятор MIDL сгенерирует библиотеку типа, содержащую Component и IX. Component добавляется к библиотеке типа, так как оператор coclass находится внутри оператора library. Интерфейс IX включается в библиотеку потому, что на него есть ссылка внутри оператора library.

Когда компилятор MIDL встречает в файле IDL оператор library, он автоматически генерирует библиотеку типа.

В гл. 10 Вы видели, что компилятор MIDL генерировал библиотеку типа SERVER.TLB, даже когда она не была нам нужна.

Распространение библиотек типа После генерации библиотеки типа Вы можете либо поставлять ее в виде отдельного файла, либо включить ее в Ваш EXE или DLL как ресурс. Большинство разработчиков предпочитает второй вариант, поскольку он упрощает установку приложения.

Использование библиотек типа Первый шаг при использовании библиотек типа — ее загрузка. Для этого имеется несколько функций. Первая, которую следует попробовать, — LoadRegTypeLib, пытающаяся загрузить библиотеку по информации из Реестра Windows. Если эта функция потерпела неудачу, Вам следует использовать LoadTypeLib, которая загружает библиотеку с диска по имени файла, либо LoadTypeLibFromResource, которая загружает библиотеку типа из ресурса в EXE или DLL. LoadTypeLib должна в процесса загрузки регистрировать для Вас библиотеку типа.

Однако если ей задано имя полного пути, библиотека зарегистрирована не будет (см. PSS ID Number Q131055).

Следовательно, после успешного вызова LoadTypeLib стоит вызвать RegisterTypeLib. Соответствующий код приведен в листинге 11-2.

Модифицированный код инициализации компонента из CMPNT.CPP HRESULT CA::Init() { HRESULT hr;

// Динамически загрузить TypeInfo, если он еще не загружен if (m_pITypeInfo == NULL) { ITypeLib* pITypeLib = NULL;

hr = ::LoadRegTypeLib(LIBID_ServerLib, // Номера версии 1, 0, 0x00, &pITypeLib);

if (FAILED(hr)) { // Загрузить и зарегистрировать библиотеку типа hr = ::LoadTypeLib(wszTypeLibFullName, &pITypeLib);

if(FAILED(hr)) { trace("Вызов LoadTypeLib неудачен", hr);

return hr;

} // Убедиться, что библиотека типа зарегистрирована hr = RegisterTypeLib(pITypeLib, wszTypeLibFullName, NULL);

if(FAILED(hr)) { trace("Вызов RegisterTypeLib неудачен", hr);

return hr;

} } // Получить информацию типа для интерфейса объекта hr = pITypeLib->GetTypeInfoOfGuid(IID_IX, &m_pITypeInfo);

pITypeLib->Release();

if (FAILED(hr)) { trace("Вызов GetTypeInfoOfGuid неудачен", hr);

return hr;

} } return S_OK;

} Листинг 11-2 Загрузка, регистрация и использование библиотеки типа После загрузки библиотеку можно использовать. LoadTypeLib и другие функции возвращают указатель на интерфейс ItypeLib, который используется для доступа к библиотеке типа. Обычно от библиотеки типа Вам требуется информация об интерфейсе или компоненте. Чтобы ее получить, функции ITypeLib::GetTypeInfo передается CLSID или IID, и она возвращает указатель на ItypeInfo для запрошенного элемента.

С помощью указателя ItypeInfo можно получить практически любую необходимую информацию о компонентах, интерфейсах, методах, свойствах, структурах и т.п. Но на самом деле большинство программистов на С++ никогда этим не пользуются — кроме, как Вы увидите в следующем разделе, тех случаев, когда нужно реализовать IDispatch. ITypeInfo может реализовать IDispatch автоматически.

Наиболее часто эти интерфейсы используются инструментами просмотра библиотек типа, т.е. программами, показывающими программисту содержимое библиотеки. Такой инструмент имеется в Visual Basic, он называется Object Browser. С его помощью Вы можете найти конкретный метод данного интерфейса и получить о нем справку. Программа OleView — тоже средство просмотра библиотеки типа. Одна из замечательных возможностей OleView — способность создавать по информации библиотеки типа файл, похожий на файл IDL/ODL. Эта возможность очень полезна.

Библиотеки типа в Реестре Мне по-настоящему нравятся компоненты, которые сами регистрируют себя в Реестре. Я испытываю огромное отвращение к написанию кода, помещающего данные в Реестр. К счастью, библиотеки типа регистрируются сами. Любопытным может быть интересно, какую именно информацию помещает в Реестр библиотека типа.

Запустите REGEDIT.EXE и откройте раздел HKEY_CLASSES_ROOT\TypeLib. Здесь Вы увидите множество LIBID, которые представляют собой GUID, идентифицирующие библиотеки типа. Откройте один из таких GUID и Вы найдете информацию, похожую на ту, что представлена на рис. 11-5.

HKEY_CLASSES_ROOT TypeLib {D3011EE1-B997-11CF-A6BB-0080C7B2D682} Версия Inside COM Chapter 11 1. 1. Type Library Win32 C:\CHAP11\SERVER.TLB Код языка FLAGS HELPDIR C:\CHAP Рис. 11-5 Информация, добавляемая в Реестр библиотекой типа Но одну вещь библиотеки типа не регистрируют: Вашему компоненту нужен в Реестре указатель на информацию его библиотеки типа. Поэтому Вы должны добавить раздел с именем TypeLib с GUID библиотеки в раздел CLSID Вашего компонента. Например, следующий раздел должен содержать указанный LIBID:

HKEY_CLASSES_ROOT\ CLSID\ {0C092C29-882C-11CF-A6BB-0080C7B2D682}\ TypeLib Библиотека типа создает раздел TypeLib для описанных в ней интерфейсов.

Как мы увидим в следующем разделе, библиотеки типа очень важны для реализации IDispatch.

Реализация IDispatch Вероятно, способов реализации IDispatch не меньше, чем способов ободрать кошку. MFC сторит собственную таблицу имен и указателей на функции. Но реализация дуальных интерфейсов в MFC далека от элегантности. Я покажу Вам самый простой и популярный метод реализации IDispatch. В его основе лежит делегирование вызовов GetIDsOfNames и Invoke методам интерфейса ITypeInfo.

Я уже продемонстрировал Вам, как можно получить указатель ITypeInfo для интерфейса. Просто загрузите библиотеку типа и вызовите ITypeLib::GetTypeInfoOfGuid, передавая ей IID интерфейса. GetTypeInfoOfGuid возвращает указатель интерфейса ITypeInfo, который можно использовать для реализации IDispatch.

Приведенный ниже код демонстрирует реализацию IDispatch (файл CMPNT.CPP из примера этой главы):

HRESULT stdcall CA::GetTypeInfoCount(UINT* pCountTypeInfo) { *pCountTypeInfo = 1;

return S_OK;

} HRESULT stdcall CA::GetTypeInfo( UINT iTypeInfo, // Этот объект не поддерживает локализацию LCID, ITypeInfo** ppITypeInfo) { *ppITypeInfo = NULL;

if(iTypeInfo != 0) { return DISP_E_BADINDEX ;

} // Вызвать AddRef и вернуть указатель m_pITypeInfo->AddRef();

*ppITypeInfo = m_pITypeInfo;

return S_OK;

} HRESULT stdcall CA::GetIDsOfNames( const IID& iid, OLECHAR** arrayNames, UINT countNames, // Локализация не поддерживается LCID, DISPID* arrayDispIDs) { if (iid != IID_NULL) { return DISP_E_UNKNOWNINTERFACE;

} HRESULT hr = m_pITypeInfo->GetIDsOfNames(arrayNames, countNames, arrayDispIDs);

return hr;

} HRESULT stdcall CA::Invoke( DISPID dispidMember, const IID& iid, // Локализация не поддерживается LCID, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pvarResult, EXCEPINFO* pExcepInfo, UINT* pArgErr) { if (iid != IID_NULL) { return DISP_E_UNKNOWNINTERFACE;

} ::SetErrorInfo(0, NULL);

HRESULT hr = m_pITypeInfo->Invoke( static_cast(this), dispidMember, wFlags, pDispParams, pvarResult, pExcepInfo, pArgErr);

return hr;

} Восхитительно просто, не так ли? У этого метода есть свои ограничения: например, он не поддерживает интернационализацию. К счастью, большинству компонентов это не нужно. Если Ваш компонент не таков, Вы можете загружать разные библиотеки типа на основании LCID, переданного в вызове Invoke.

Генерация исключений Как упоминалось в разделе, посвященном параметрам IDispatch::Invoke, предпоследний параметр — структура EXCEPINFO. Чтобы заставить ITypeInfo::Invoke заполнить ее, Вы должны проделать следующую последовательность действий:

1. Реализуйте для своего компонента интерфейс ISupportErrorInfo с единственной функцией-членом:

// ISupportErrorInfo virtual HRESULT stdcall InterfaceSupportsErrorInfo(const IID& iid) { return (iid == IID_IX) ? S_OK : S_FALSE;

} 2. В своей реализации IDispatch::Invoke вызовите SetErrorInfo(0, NULL) перед вызовом ITypeInfo::Invoke.

3. При возникновении исключительной ситуации вызовите CreateErrorInfo, чтобы получить указатель на интерфейс ICreateErrorInfo.

4. С помощью этого интерфейса предоставьте информацию об ошибке.

5. Наконец, получите указатель на интерфейс IErrorInfo и вызовите SetErrorInfo, передав ей в качестве второго параметра полученный указатель. Первый параметр зарезервирован и всегда равен 0. Все остальное — дело ITypeInfo и клиента.

Ниже приведен пример генерации исключения. (Код взят из функции CA::FxFakeError, которая находится в файле CMPNT.CPP из примера гл. 11.) // Создать объект «Информация об ошибке» ICreateErrorInfo* pICreateErr;

HRESULT hr = ::CreateErrorInfo(&pICreateErr);

if (FAILED(hr)) { return E_FAIL;

} // pICreateErr->SetHelpFile(...);

// pICreateErr->SetHelpContext(...);

pICreateErr->SetSource(L"InsideCOM.Chap11");

pICreateErr->SetDescription( L"Это фиктивная ошибка, сгенерированная компонентом");

IErrorInfo* pIErrorInfo = NULL;

hr = pICreateErr->QueryInterface(IID_IErrorInfo, (void**)&pIErrorInfo);

if (SUCCEEDED(hr)) { ::SetErrorInfo(0L, pIErrorInfo);

pIErrorInfo->Release();

} pICreateErr->Release();

return E_FAIL;

Маршалинг Если Вы посмотрите на make-файл из примера гл. 11, то увидите, что я не создаю DLL заместителя/заглушки. Это связано с тем, что система, а именно OLEAUT32.DLL, автоматически реализует маршалинг интерфейсов, совместимых с Автоматизацией1. Интерфейс, совместимый с Автоматизацией, наследует IDispatch и использует только такие типы параметров, которые можно поместить в VARIANT. Для таких типов OLEAUT32.DLL выполняет маршалинг автоматически.

Чтобы понять, как это работает, рассмотрим информацию в Реестре для версии интерфейса IX этой главы:

Я должен отметить, что в данном случае наш старый метод создания DLL заместителя/заглушки с помощью кода, сгенерированного MIDL, не работает, по крайней мере, для Windows 95 и Windows NT до версии 4.0. Компилятор MIDL не в состоянии сгенерировать код маршалинга VARIANT и BSTR, который будет работать на этих системах. Поэтому, если Вы не хотите использовать OLEAUT32.DLL, Вам придется написать код маршалинга самим.

HKEY_CLASSES_ROOT\ Interfaces\ {32BB8326-B41B-11CF-A6BB-0080C7B2D682}\ ProxyStubClsid В этом разделе должен находиться следующий CLSID:

{00020424-0000-0000-C000-000000000046} Теперь найдем этот CLSID в разделе Реестра CLSID:

HKEY_CLASSES_ROOT\ CLSID\ {00020424-0000-0000-C000-000000000046}\ InprocServer Вы увидите, что значением InprocServer32 является OLEAUT32.DLL Что Вы хотите сделать сегодня?

Теперь Вы это получили: есть еще один способ коммуникации между клиентом и компонентом. Как обычно, если одно и то же можно сделать по-разному, Вы должны решить, что выбрать. Есть три варианта: интерфейсы vtbl, дуальные интерфейсы и disp-интерфейсы. Какой из них подойдет Вам? Как в таких случаях говорит мой отец: «С одной стороны шесть, с другой полдюжины». Есть, однако, вполне четкие рекомендации, какой тип интерфейса кода следует использовать.

Если доступ к Вашему компоненту будет осуществляться только из компилируемых языков типа С и С++, используйте vtbl или нормальный интерфейс СОМ. Интерфейсы vtbl работают значительно быстрее disp интерфейсов. Кроме того, с ними гораздо легче работать на С++. Если к Вашему компоненту будут обращаться из Visual Basic или Java, следует реализовать дуальный интерфейс. Visual Basic и Java могут работать с ним либо как с disp-интерфейсом, либо через vtbl. На С++ также можно будет использовать оба эти способа.

Однако реализованная с помощью vtbl часть дуального интерфейса, который разработан специально для использования Visual Basic, вряд ли осчастливит большинство программистов на С++ (если только Вы не используете расширения компилятора Visual C++ 5.0). В связи с этим я рекомендую разработать низкоуровневый интерфейс vtbl и высокоуровневый дуальный интерфейс. Низкоуровневый интерфейс способен дать программисту на С++ дополнительную информацию, необходимую для эффективного агрегирования компонента.

Если только всерьез не нужно создавать компоненты во время выполнения, я бы вообще избегал реализации чистых disp-интерфейсов. Дуальные интерфейсы гораздо более гибки.

Кроме того, есть еще один фактор, влияющий на Ваше решение, — скорость. Если Вы имеете дело с компонентом внутри процесса, интерфейс vtbl работает примерно в 100 раз быстрее disp-интерфейса. (Точное значение несколько меняется в зависимости от набора типов аргументов функций.) В случае же компонента вне процесса накладные расходы маршалинга более существенны, чем накладные расходы IDispatch::Invoke, и интерфейс vtbl работает примерно лишь в два с половиной раза быстрее disp-интерфейса. Если же Ваш компонент является удаленным, то тип используемого интерфейса вообще не имеет значения.

12 глава Многопоточность Входящие в мой офис посетители постоянно бьются лбом о прикрепленный к потолку черный предмет сантиметров 30 длиной. Это копия вертолета Bell 206B-III Jet Ranger в масштабе 1:32. Копия не абсолютно точная — вместо хвостового винта сзади толкающий пропеллер благодаря которому модель может летать по кругу.

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

У этого маленького вертолета своя история. Его подарил мне Рёдигер Эш (Ruediger Asche), с которым мы вместе писали статьи для Microsoft Developer Network. Он знаток мрачных глубин ядра Windows NT, куда никогда не проникает свет GUI. Одна из областей специализации Рёдигера — многопоточное программирование. Вот мы и добрались до темы этой главы.

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

Однако для моделирования летающего по кругу вертолета многопоточность необязательна. По-настоящему она бывает полезна при построении пользовательского интерфейса с малым временем отклика. Интерфейс можно сделать живее и «доступнее», если переложить вычисления на фоновый поток. Наиболее это заметно в программах просмотра Web. Большинство из них перекачивают страницу данных в рамках одного потока, а выводят на экран в рамках другого;

третий же дает возможность пользователю работать со страницей во время ее загрузки. Лично я не выношу сидеть и ждать, пока загружается куча ненужных картинок, — так что обычно щелкаю мышью на следующей гиперссылке еще до окончания загрузки и отрисовки. Эта удобная возможность обеспечивается многопоточностью.

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

посмотрим лишь, как многопоточность влияет на разработку и использование компонентов СОМ. Более подробно о многопоточном программировании можно прочитать в статьях Рёдигера Эша в MSDN.

Потоковые модели COM COM использует потоки Win32 и не вводит новых типов потоков или процессов. В СОМ нет своих примитивов синхронизации, для создания и синхронизации потоков просто используется API Win32. Использование потоков в СОМ, кроме некоторых нюансов, не отличается от их использования в приложениях Win32. Мы рассмотрим эти нюансы, но сначала позвольте мне привести общий обзор потоков Win32.

Потоки Win В обычном приложении Win32 имеются потоки двух типов: потоки пользовательского интерфейса (user interface threads) и рабочие потоки (worker threads). С потоком пользовательского интерфейса связаны одно или несколько окон. Такие потоки имеют циклы выборки сообщений, которые обеспечивают работу окон и реакцию на действия пользователя. Рабочие потоки используются для фоновой обработки и не связаны с окнами;

в них обычно нет циклов выборки сообщений. В каждом процессе может быть несколько потоков пользовательского интерфейса и несколько рабочих потоков.

У потоков пользовательского интерфейса есть интересная особенность поведения. Как я только что сказал, у каждого потока пользовательского интерфейса есть одно или несколько окон. Оконная процедура данного окна вызывается только потоком, который владеет этим окном, — т.е. потоком, создавшим окно. Таким образом, оконная процедура всегда выполняется в одном и том же потоке, независимо от того, какой поток послал сообщение этой процедуре на обработку. Следовательно, все посланные данному окну сообщения синхронизированы, и окно с гарантией будет получать сообщения упорядоченно.

Преимущества для Вас, программиста, — в том, что нет нужды писать «потокобезопасные» оконные процедуры (а их писать не просто и, возможно, небыстро). Поскольку синхронизацию сообщений гарантирует Windows, Вам не нужно беспокоиться о том, что оконную процедуру могут вызвать одновременно несколько потоков. Эта синхронизация весьма полезна потокам, управляющим пользовательским интерфейсом. В конце концов, мы хотим, чтобы информация о действиях пользователя достигала окна в той же последовательности, в какой эти действия производились.

Потоки СОМ СОМ использует те же два типа потоков, хотя и называет их по-другому. Вместо «поток пользовательского интерфейса» в СОМ говорят разделенный поток (apartment thread). Термин свободный поток (free thread) используют вместо термина «рабочий поток». Самая сложная часть потоковой модели СОМ — терминология.

Основная же сложность в ней состоит в несогласованности документации. Набор терминов, используемых в Win32 SDK, отличается от набора, используемого спецификацией СОМ. Я буду максимально избегать этой терминологии либо вводить термины как можно раньше. В этой главе я буду использовать термин разделенный поток для обозначения потока, подобного потоку пользовательского интерфейса, а термин свободный поток — для обозначения потока, подобного рабочему потоку.

Почему в СОМ вообще рассматривается потоковая модель, если она ничем не отличается от Win32? Причин две:

маршалинг и синхронизация. Более подробно мы рассмотрим маршалинг и синхронизацию после того, как разберемся, что такое подразделение (apartment), модель разделенных потоков (apartment threading) и модель свободных потоков (free threading).

Подразделение Хотя мне всерьез хотелось бы избежать новой терминологии, сейчас я определю термин подразделение (apartment). Подразделение — это концептуальный конгломерат, состоящий из потока в стиле пользовательского интерфейса (так называемый разделенный поток) и цикла выборки сообщений.

Возьмем типичное приложение Win32, которое состоит из процесса, цикла выборки сообщений и оконной процедуры. У каждого процесса есть как минимум один поток. Схематически приложение Windows представлено на рис. 12-1. Рамка пунктирными краями обозначает процесс. Рамка, внутри которой изображен цикл, представляет циклю выборки сообщений Windows. Две другие рамки изображают оконную процедуру и код программы. Все они расположены поверх линии, обозначающей поток управления.

Кроме процесса, рис. 12-1 иллюстрирует и подразделение. Один поток — это разделенный поток.

На рис. 12-2 та же схема иллюстрирует организацию типичного приложения СОМ, состоящего из клиента и двух компонентов внутри процесса. Программа работает внутри одного процесса и имеет единственный поток управления. У компонентов внутри процесса нет своих циклов выборки сообщений — они используют тот же цикл, что и клиентский EXE. И снова рисунок иллюстрирует одно подразделение.

Цикл выборки сообщений Граница процесса Код программы Оконная процедура Поток управления Рис. 12-1 Приложение Windows. Показаны: поток управления, цикл выборки сообщений, граница процесса и код программы CoInitialize Компонент внутри процесса Компонент Клиент Компонент CoUninitialize Рис. 12-2 Клиент и два компонента внутри процесса. Имеется только один поток, и компоненты используют цикл выборки сообщений клиента совместно с клиентом Использование компонентов внутри процесса не изменяет базовой структуры приложения Windows. Самой существенное различие между двумя изображенными процессами — в том, что процесс с компонентами обязан, прежде чем использовать какие-либо функции библиотеки СОМ, вызвать CoInitialize, а перед завершением вызывать CoUninitialize.

Добавим компонент вне процесса Когда клиент подсоединяется к компоненту вне процесса, картина меняется. Такой клиент показан на рис. 12-3.

Компонент находится в процессе, отдельном от процесса клиента. У каждого процесса свой поток управления.

Цикл выборки сообщений предоставляется компоненту его сервером вне процесса. Если вернуться к примеру гл.

10, код такого цикла можно найти в OUTPROC.CPP. Другое существенное отличие от случая с компонентом внутри процесса — необходимость маршалинга вызовов между процессами. На рисунке такой вызов представлен «молнией». В гл. 10 мы узнали, как создать DLL заместителя/заглушки, которая используется для маршалинга данных между клиентом и компонентом вне процесса.

На рис. 12-3 изображены два подразделения. В одном из них находится клиент, а в другом — компонент. Может показаться, что подразделение — то же самое, что и процесс, но это неверно. В одном процессе может быть несколько подразделений.

Сервер компонента вне процесса CoInitialize CoInitialize Для вызовов внутри процесса маршалинг Компонент не выполняется Для вызовов Клиент между Компонент процессами необходим маршалинг CoUninitialize CoUninitialize Цикл выборки сообщений компонента вне процесса Рис. 12-3 У компонента вне процесса есть собственный цикл выборки сообщений и поток На рис. 12-4 я превратил компонент рис. 12-3 из компонента вне процесса в компонент внутри процесса, расположенный в другом подразделении.

Штриховыми линиями изображены подразделения. Пунктирная линия по-прежнему обозначает границу процесса.

Обратите внимание, как похожи два рисунка. По существу, я нарисовал вокруг старой картинки новую рамку и объявил, что теперь объекты находятся в одном процессе. Из этого должна стать очевидной моя точка зрения — подразделения аналогичны (однопоточным) процессам в следующих моментах. И у процесса, и у подразделения есть собственный цикл выборки сообщений. Маршалинг вызовов функций внутри (однопоточного) процесса и внутри подразделения не нужен. Имеет место естественная синхронизация, так как и у процесса, и у подразделения только один поток. Синхронизация вызовов функций между процессами и между подразделениями производится при помощи цикла выборки сообщений. И последняя деталь — каждый процесс должен инициализировать библиотеку СОМ. Точно так же и каждое подразделение должно инициализировать библиотеку СОМ. Теперь, если вернуться к рис. 12-2, Вам станет понятно, как клиент и два компонента сосуществуют внутри одного подразделения.

Для вызовов между Компонент внутри Граница подразделениями процесса находится в процесса необходим маршалинг другом подразделении CoInitialize CoInitialize Компонент Клиент Компонент CoUninitialize CoUninitialize Цикл выборки сообщений Границы подразделений используется процедурой потока Рис. 12-4 Клиент взаимодействует внутри процесса с компонентом, расположенным в другом подразделении Разделенные потоки Разделенный поток — это единственный поток внутри подразделения. Но когда Вы слышите термин «разделенный поток», представляйте себе поток пользовательского интерфейса. Вспомните, что поток пользовательского интерфейса владеет созданным им окном. Оконная процедура вызывается только им. Между разделенным потоком и созданным им компонентом существуют такие же отношения. Разделенный поток владеет созданным им компонентом. Компонент внутри подразделения будет вызываться только соответствующим разделенным потоком.

Если поток посылает сообщение окну, принадлежащему другому потоку, Windows помещает это сообщение в очередь сообщений соответствующего окна. Цикл выборки сообщений этого окна выполняется потоком, создавшим окно. Когда цикл выбирает очередное сообщение и вызывает оконную процедуру, для вызова процедуры используется тот же поток, который создал окно.

То же самое верно для компонента внутри подразделения. Предположим, что метод Вашего компонента, находящегося в подразделении, вызывается другим потоком. СОМ автоматически помещает этот вызов в очередь подразделения. Цикл выборки сообщений извлекает этот вызов и вызывает метод с помощью потока подразделения.

Таким образом, компонент внутри подразделения вызывается только потоком подразделения, и ему нет нужды заботиться о синхронизации. Так как СОМ гарантирует, что все вызовы такого компонента будут упорядочены, компоненту не требуется быть «потокобезопасным». Это значительно облегчает написание кода компонента. Ни один из компонентов, которые мы написали в этой книге, не был «потокобезопасным». Но, пока их создают разделенный потоки, мы можем быть уверены, что их методы никогда не будут вызваны разными потоками одновременно.

Именно в этом состоит отличие свободных потоков от разделенных.

Свободные потоки СОМ упорядочивает вызовы компонентов для разделенных потоков. Однако синхронизация не выполняется для компонентов, созданных свободными потоками. Если компонент создан свободным потоком, он может вызываться любым потоком и в любой момент времени. Разработчик должен гарантировать, что его компонент сам синхронизирует доступ к себе. Такой компонент должен быть «потокобезопасным». Модель свободных потоков переносит заботу о синхронизации с СОМ на компонент.

Поскольку СОМ не выполняет синхронизацию вызовов компонентов, свободным потокам не нужен цикл выборки сообщений. Компонент, созданный свободным потоком, называется компонентом свободных потоков.

Такой компонент не принадлежит создавшему его потоку, а используется всеми потоками совместно: все потоки имеют к нему свободный доступ.

Разделенные потоки — единственный тип потоков, которые можно использовать при работе СОМ в Microsoft Windows NT 3.51 и Microsoft Windows 95. В Windows NT 4.0 и в Windows 95 с установленной поддержкой DCOM можно использовать свободные потоки.

Мы познакомились со свободными потоками в общем. С более интересными подробностями мы столкнемся при обсуждении маршалинга и синхронизации.

Маршалинг и синхронизация Для правильного маршалинга и синхронизации вызовов компонента СОМ надо знать, потоком какого типа он исполняется. В случае разделенных потоков СОМ обычно выполняет необходимые маршалинг и синхронизацию.

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

Запомните следующие общие правила:

!" Вызовы между процессами всегда выполняются с использованием маршалинга. Мы обсуждали это в гл.

10.

!" Вызовы внутри одного потока никогда не используют маршалинг.

!" Вызов компонента в разделенном потоке выполняется с маршалингом.

!" Вызов компонента в свободном потоке не всегда использует маршалинг.

!" Вызовы с помощью разделенного потока синхронизируются.

!" Вызовы с помощью свободного потока не синхронизируются.

!" Вызовы внутри потока синхронизируются самим потоком.

Теперь рассмотрим возможные комбинации вызовов разделенных и свободных потоков. Давайте начнем с простых случаев. Если явно не указано иное, подразумевается, что вызовы осуществляются в пределах одного процесса.

Вызовы внутри одного потока Если клиент, выполняющийся в каком-либо потоке, вызывает компонент, выполняющийся в том же потоке, то вызов синхронизирован просто потому, что поток всего один. СОМ не нужно выполнять какую-либо синхронизацию, и компонент не должен быть «потокобезопасным». Вызовы в пределах одного потока не требуют маршалинга. Это правило мы использовали на протяжении всей книги.

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

Свободный — свободный Если клиент, выполняющийся в свободном потоке, вызывает компонент свободных потоков, то СОМ не будет синхронизировать этот вызов. Вызов будет выполнять поток клиента. Компонент должен сам синхронизировать доступ к себе, так как одновременно его может вызвать другой клиент посредством другого потока. Если клиент и компонент находятся внутри одного процесса, маршалинг вызова не выполняется.

Свободный — разделенный Если клиент в свободном потоке вызывает компонент в подразделении, то синхронизацию вызова осуществляет СОМ. Компонент будет вызван потоком подразделения. Маршалинг интерфейса также необходим, независимо от того, в одном ли процессе или в разных находятся оба потока. В большинстве случаев маршалинг за Вас выполнит СОМ. Но иногда, как Вы скоро увидите, маршалинг приходится выполнять вручную.

Разделенный — свободный Если клиент в разделенном потоке вызывает свободный поток, то СОМ не выполняет синхронизацию вызова. Эта обязанность лежит на компоненте свободного потока. Маршалинг интерфейса выполняется, но, если оба потока принадлежат одному процессу, СОМ может оптимизировать маршалинг, чтобы передавать указатели клиенту непосредственно. Подробнее мы рассмотрим это при реализации свободных потоков.

Как Вы видите, потоковые модели компонентов СОМ не слишком отличаются от обычных моделей потоков Win32. В процессе может быть любое число потоков. Эти потоки могут быть разделенными или свободными. С точки зрения программиста, в модели потоков СОМ есть только два интересных момента: синхронизация и маршалинг. СОМ синхронизирует вызовы компонентов в разделенных потоках. Разработчик синхронизирует вызовы компонентов в свободных потоках. Синхронизация компонентов в свободных потоках — это общая проблема многопоточности, а не специфика СОМ. Однако маршалинг специфичен для СОМ — и это единственное, что действительно уникально при работе с компонентами СОМ в многопоточной среде. Подробно мы рассмотрим ручной маршалинг интерфейсов позже, когда реализуем разделенный поток и поток клиента.

Реализация модели разделенных потоков С компонентами внутри подразделений хорошо то, что им нет необходимости быть «потокобезопасными».

Доступ к ним синхронизируется СОМ. При этом ни имеет значения, приходит ли вызов из потоков других подразделений или из свободных потоков. СОМ автоматически использует скрытую очередь сообщений Windows для синхронизации клиентских вызовов таких компонентов. Благодаря этому реализация компонентов в однопоточных подразделениях очень схожа с написанием оконных процедур. (Для синхронизации доступа к оконной процедуре используется цикл выборки сообщений;

СОМ использует тот же механизм для синхронизации доступа к однопоточному подразделению.) Ниже следуют основные требования к подразделению:

!" Оно должно вызывать CoInitialize или OleInitialize.

!" В нем может быть только один поток.

!" У него должен быть цикл выборки сообщений.

!" Оно обязано выполнять маршалинг указателей на интерфейсы при передаче их другим подразделениям.

!" В случае компонента внутри процесса подразделение должно иметь «потокобезопасные» точки входа DLL.

!" Ему может понадобиться «потокобезопасная» фабрика класса.

В следующих параграфах некоторые из этих требований рассматриваются подробнее.

Компонент может существовать только в одном потоке Компонент в однопоточном подразделении должен выполняться в единственном потоке. Доступ к компоненту имеет только создавший его поток. Именно та работает оконная процедура — ее вызывает только поток, создавший данное окно. Так как доступ к компоненту возможен только из одного потока, такой компонент всегда выполняется в однопоточном подразделении и ему не нужно заботиться о синхронизации. Однако компонент должен защищать свои глобальные данные, поскольку он, как и оконная процедура, доступен для повторного входа (реентерабелен).

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

Точки входа DLL должны быть «потокобезопасны» Компоненту в однопоточном подразделении не нужно быть «потокобезопасным», так как доступ к нему возможен только из создавшего его потока. Однако «потокобезопасны» должны быть точки входа DLL, такие как DllGetClassObject и DllCanUnloadNow. Функцию DllGetClassObject могут одновременно вызывать несколько клиентов из разных потоков. Чтобы сделать эти функции «потокобезопасными», убедитесь, что все совместно используемые данные защищены от параллельного доступа. В некоторых случаях это означает, что фабрика класса также должна быть «потокобезопасна».

Фабрикам класса может понадобиться «потокобезопасность» Если для каждого компонента Вы создаете отдельную фабрику класса, такой фабрике «потокобезопасность» не требуется, поскольку доступ к ней возможен только для одного клиента. Но если DllGetClassObject создает одну фабрику класса, которая используется для порождения всех экземпляров компонента, Вы должны гарантировать «потокобезопасность» фабрики, поскольку к ней возможен одновременный доступ из разных потоков.

Компонент вне процесса может использовать один экземпляр фабрики класса для создания всех экземпляров компонента. Такая фабрика класса также должна быть «потокобезопасна». Обеспечение потокобезопасности большинства фабрик класса просто, так как они не изменяют никаких совместно используемых данных, кроме счетчика ссылок. Для защиты последних можно использовать InterlockedIncrement и InterlockedDecrement, что я и демонстрировал уже много лун тому назад в гл. 4.

Удовлетворяющий перечисленным требованиям компонент внутри процесса помечает в Реестре, что поддерживает модель разделенных потоков. О том, как компонент регистрирует свою потоковую модель, рассказывается в конце этой главы, в разделе «Информация о потоковой модели в Реестре». Теперь же мы детально рассмотрим, что необходимо сделать для маршалинга указателя на интерфейс, который передается другому потоку.

Когда компонент в разделенном потоке передает свой интерфейс компоненту в другом потоке, для этого интерфейса требуется маршалинг. Неважно, является ли другой поток разделенным или свободным, маршалинг всегда необходим.

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

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

Ручной маршалинг Итак, когда же программист должен выполнять маршалинг указателя интерфейса самостоятельно? В основном тогда, когда он пересекает границу подразделения без помощи СОМ.

Давайте рассмотрим два примера. Сначала пусть клиент создает разделенный поток, создающий компонент и управляющий им. Как в главном потоке, так и в потоке подразделения может потребоваться доступ к такому компоненту. У разделенного потока есть указатель на интерфейс компонента, поскольку этот поток его и создал.

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

Последний должен выполнить демаршалинг указателя на интерфейс перед использованием.

Второй случай имеет место тогда, когда фабрика класса компонента внутри процесса создает его экземпляры в разных потоках. Этот сценарий похож на предыдущий, но теперь создание компонента выполняет в разных потоках сервер (в предыдущем случае это делал клиент). Клиент вызывает CoCreateInstance, в результате чего запускается фабрика класса компонента. Когда клиент вызывает IClassFactory::CreateInstance, фабрика класса создает новый разделенный поток. Этот новый поток создает компонент. IClassFactory::CreateInstance должна вернуть клиенту указатель на интерфейс компонента. Но CreateInstance не может непосредственно передать клиенту указатель на интерфейс, созданный в новом подразделении, так как клиент находится в другом потоке.

Таким образом, поток подразделения должен выполнить маршалинг указателя на интерфейс для CreateInstance, которая затем выполняет демаршалинг указателя и возвращает его клиенту.

Самое длинное имя API Win Теперь, когда мы узнали, где нужен маршалинг интерфейса, нам нужно знать, как его осуществлять. Вы можете выполнить всю работу сами при помощи функций CoMarshalInterface и CoUnMarshalInterface. Но если у Вас есть более интересные занятия, используйте вспомогательные функции с самыми длинными именами в API Win32, CoMarshalInterThreadInterfaceInStream и CoGetInterfaceAndReleaseStream. (Если так пойдет и дальше, скоро имя функции будет занимать целый абзац.) Использовать эти функции просто. Маршалинг указателя на интерфейс IX выполняется так:

IStream* pIStream = NULL;

HRESULT hr = CoMarshalInterThreadInterfaceInStream( // ID интерфейса, маршалинг которого нужно выполнить IID_IX, // Интерфейс, для которого выполняется маршалинг pIX, &pIStream);

// Поток, куда будут помещены результаты маршалинга Демаршалинг выполняется следующим образом:

IX* pIXmarshaled;

HRESULT hr = CoGetInterfaceAndReleaseStream( // Поток, содержащий интерфейс pIStream, // ID демаршализуемого интерфейса IID_IX, // Демаршализованный указатель на интерфейс (void**)&pIXmarshaled);

Все очень просто, не так ли? Это так просто потому, что СОМ незаметно для программиста и автоматически использует DLL заместителя/заглушки.

Настало время написать программу До этого места данная глава носила весьма концептуальный характер, и тому была основательная причина:

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

Здесь мы будем делать то же самое, но вместо рабочего потока используем разделенный поток. Главный поток создает разделенный поток. Разделенный поток создает компонент и периодически обновляет его счетчик. Этот поток будет передавать главному потоку указатель на интерфейс, чтобы главный поток мог получать и отображать значение счетчика. Все, как в обычном многопоточном программировании в Win32 — за исключением того, что поток подразделения:

!" Инициализирует библиотеку СОМ.

!" Имеет собственный цикл выборки сообщений.

!" Выполняет маршалинг интерфейса для передачи его обратно главному потоку.

Компонент в точности похож на те, что мы писали ранее.

Теперь самая сложная часть в разработке однопоточного подразделения состоит в том, что у нас есть лишь концепция, а не код. Как обычно, Вы создаете поток. Как обычно, Вы создаете цикл выборки сообщений. Так как я хотел, чтобы подразделение выглядело более «настоящим», то создал для выполнения этих действий небольшой класс CSimpleApartment.

CSimpleApartment и CClientApartment CSimpleApartment — это простой класс, инкапсулирующий создание компонента в другом потоке.

Соответствующий код находится в файлах APART.H и APART.CPP в каталоге CHAP12\APT_THD на прилагающемся к книге диске. CSimpleApartment::StartThread запускает новый поток.

CSimpleApartment::CreateComponent принимает CLSID компонента и создает его в потоке, запущенном StartThread.

Именно здесь все становится интересным (или непонятным). CSimpleApartment охватывает оба потока. Часть CSimpleApartment вызывается первоначальным потоком, а другая часть — новым потоком. CSimpleApartment обеспечивает коммуникацию двух потоков. Поскольку CSimpleApartment::CreateComponent вызывается из первоначального потока, постольку она не может создать компонент непосредственно. Компонент надо создать в новом потоке. Поэтому CreateComponent использует событие, чтобы дать потоку нового подразделения сигнал к созданию компонента. Для собственно создания поток подразделения вызывает функцию CreateComponentOnThread. CSimpleApartment::CreateComponentOnThread — это чисто виртуальная функция, которую следует определить в производном классе. В этом первом примере производный класс CClientApartment реализует версию CreateComponentOnThread, которая создает компонент самым обычным способом — при помощи CoCreateInstance.

Пример с разделенным потоком В табл. 12-1 показана структура вызовов функций в коде, который мы собираемся рассматривать. Весь код в правой части таблицы исполняется в разделенном потоке, созданном CSimpleApartment::StartThread.

Таблица 12-1 Структура вызовов функций в примере с разделенным потоком Главный поток Разделенный поток WinMain CSimpleApartment CSimpleApartment InitializeApartment StartThread RealThreadProc ClassThreadProc CClientApartment::WorkerFunction CreateComponent CreateComponentOnThread CClientApartment::CreateComponentOnThread CSimpleApartment::StartThread Все самое интересное начинается в CLIENT.CPP с функции InitializeApartment. Она вызывает CSimpleApartment::StartThread, реализация которой приведена ниже:

BOOL CSimpleApartment::StartThread(DWORD WaitTime) { if (IsThreadStarted()) { return FALSE;

} // Создать поток // Защита по умолчанию m_hThread = ::CreateThread(NULL, 0, // Размер стека по умолчанию RealThreadProc, (void*)this, // Создать приостановленный CREATE_SUSPENDED, // поток // Получить идентификатор &m_ThreadId);

// потока if (m_hThread == NULL) { trace("StartThread не может создать поток", GetLastError());

return FALSE;

} trace("StartThread успешно создала поток");

// Создать событие для выдачи потоку команды на создание компонента m_hCreateComponentEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);

if (m_hCreateComponentEvent == NULL) { return FALSE;

} // Создать событие, которое сигнализируется потоком при завершении m_hComponentReadyEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);

if (m_hComponentReadyEvent == NULL) { return FALSE;

} trace("StartThread успешно создала события");

// Инициализировать время ожидания m_WaitTime = WaitTime;

// Поток был создан приостановленным;

запустить его DWORD r = ResumeThread(m_hThread);

assert(r != 0xffffffff);

// Дождаться начала выполнения потока перед продолжением WaitWithMessageLoop(m_hComponentReadyEvent);

return TRUE;

} CSimpleApartment::StartThread создает новый поток при помощи ::CreateThread. Она также создает два события для синхронизации двух потоков. Функция CSimpleApartment::ClassThreadProc, выполняющаяся в потоке подразделения, использует m_hComponentReadyEvent дважды — сначала для сигнализации о том, что новый поток начал выполняться, и в конце для сигнализации о том, что он остановлен. Функция CSimpleApartment::CreateComponent использует событие m_hCreateComponentEvent, чтобы выдать потоку подразделения команду на вызов CSimpleApartment::CreateComponentOnThread для создания компонента. После создания компонента CreateComponentOnThread устанавливает m_hCreateComponentEvent, чтобы уведомить об окончании создания CreateComponent.

CSimpleApartment::WaitWithMessageLoop — это вспомогательная функция, которая ожидает события. Она не просто ждет, а обрабатывает события Windows. Если Вы будете ждать события без обработки сообщений, пользователю покажется, что программа «зависла». Пользовательский интерфейс должен всегда обрабатывать сообщения в процесса ожидания. WaitWithMessageLoop использует функцию API Win MsgWaitForMultipleObjects, которую мы рассмотрим ниже.

CSimpleApartment::ClassThreadProc При запуске потока вызывается статическая функция RealThreadProc, которая вызывает ClassThreadProc.

Windows не может вызывать функции С++, поэтому функции обратного вызова Win32 обязаны быть статическими. При создании потока его процедуре передается указатель нашего класса, чтобы она могла вызвать ClassThreadProc. Код ClassThreadProc приведен ниже:

DWORD CSimpleApartment::ClassThreadProc() { // Инициализировать библиотеку СОМ HRESULT hr = CoInitialize(NULL);

if (SUCCEEDED(hr)) { // Сигнализировать, что поток запущен SetEvent(m_hComponentReadyEvent);

// Ждать команды на создание компонента BOOL bContinue = TRUE;

while (bContinue ) { switch(::MsgWaitForMultipleObjects( 1, &m_hCreateComponentEvent, FALSE, m_WaitTime, QS_ALLINPUT)) { // Создать компонент case WAIT_OBJECT_0:

CreateComponentOnThread();

break;

// Обработать сообщения Windows case (WAIT_OBJECT_0 + 1):

MSG msg;

while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) { bContinue = FALSE;

break;

} DispatchMessage(&msg);

} break;

// Выполнить фоновую обработку case WAIT_TIMEOUT:

WorkerFunction();

break;

default:

trace("Ошибка ожидания", GetLastError());

} } // Освободить библиотеку COM CoUninitialize();

} // Сигнализировать о завершении потока SetEvent(m_hComponentReadyEvent);

return 0;

} Подразделения должны инициализировать библиотеку СОМ и содержать циклы выборки сообщений.

ClassThreadProc удовлетворяет этим требованиям. Вместо того, чтобы просто использовать цикл GetMessage/DispatchMessage, ClassThreadProc использует MsgWaitForMultipleObjects, которая ожидает, пока не произойдет одно из трех событий: m_hCreateComponentEvent, сообщение Windows или истечение времени тайм аута. Если устанавливается событие m_hCreateComponentEvent, то MsgWaitForMultipleObjects прекращает ожидание и ClassThreadProc вызывает CreateComponentOnThread. Если потоку посылается сообщение Windows, то цикл PeekMessage/DispatchMessage извлекает и распределяет это сообщение (а также любые другие, находящиеся в очереди). Если истекает время тайм-аута, вызывается CSimpleApartment::WorkerFunction. Эта функция реализована производным классом CClientApartment, о котором мы поговорим ниже.

При желании Вы можете использовать GetMessage/DispatchMessage в чистом виде. Для выдачи команды на создание компонента вместо события можно использовать PostThreadMessage. Однако MsgWaitForMultipleObjects более эффективна.

CSimpleApartment::CreateComponent Теперь, когда мы создали поток, пришла пора создавать компонент. Создание начинается с вызова главным потоком CSimpleApartment::CreateComponent. Код этой функции приведен ниже:

HRESULT CSimpleApartment::CreateComponent(const CLSID& clsid, const IID& iid, IUnknown** ppI) { // Инициализировать совместно используемые данные m_pIStream = NULL;

m_piid = &iid;

m_pclsid = &clsid;

// Выдать потоку команду на создание компонента SetEvent(m_hCreateComponentEvent);

// Ожидать завершения создания компонента trace("Ожидать завершения создания компонента ");

if (WaitWithMessageLoop(m_hComponentReadyEvent)) { trace("Ожидание закончилось успешно");

// Ошибка GetClassFactory?

if (FAILED(m_hr)) { return m_hr;

} // Ошибка при маршалинге?

if (m_pIStream == NULL) { return E_FAIL;

} trace("Демаршалинг указателя на интерфейс");

// Выполнить демаршалинг интерфейса HRESULT hr = ::CoGetInterfaceAndReleaseStream(m_pIStream, iid, (void**)ppI);

m_pIStream = NULL;

if (FAILED(hr)) { trace("Ошибка CoGetInterfaceAndReleaseStream", hr);

return E_FAIL;

} return S_OK;

} trace("Что случилось?");

return E_FAIL;

} Функция CreateComponent выполняет четыре основных действия. Во-первых, она копирует свои параметры в переменные-члены. Во-вторых, она выдает потоку команду на создание компонента. В-третьих, она ждет завершения создания компонента. И в-четвертых, она выполняет демаршалинг запрошенного интерфейса компонента.

CSimpleApartment::CreateComponentOnThread Когда CreateComponent устанавливает m_hCreateComponentEvent, ClassThreadProc вызывает приватную внутреннюю версию CreateComponentOnThread, а та выполняет два основных действия. Во-первых, она вызывает чисто виртуальную версию CreateComponentOnThread с параметрами, которые были переданы CreateComponent.

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

void CSimpleApartment::CreateComponentOnThread() { IUnknown* pI = NULL;

// Вызвать производный класс для фактического создания компонента m_hr = CreateComponentOnThread(*m_pclsid, *m_piid, &pI);

if (SUCCEEDED(m_hr)) { trace("Компонент создан успешно");

// Выполнить маршалинг интерфейса для основного потока HRESULT hr = ::CoMarshalInterThreadInterfaceInStream(*m_piid, pI, &m_pIStream);

assert(SUCCEEDED(hr));

// Освободить указатель pI pI->Release();

} else { trace("Ошибка CreateComponentOnThread", m_hr);

} trace("Сигнализировать главному потоку, что компонент создан");

SetEvent(m_hComponentReadyEvent);

} CreateComponentOnThread использует функцию CoMarshalInterThreadInterfaceInStream для маршалинга указателя на интерфейс в другой поток. Код CreateComponent выполняет демаршалинг интерфейса.

CClientApartment В этом примере CClientApartment реализует две виртуальные функции: CreateComponentOnThread и WorkerFunction. CClientApartment предназначена для использования клиентами, которые хотят создавать компоненты в разных потоках. Она переопределяет CreateComponentOnThread, чтобы вызвать CoCreateInstance:

HRESULT CClientApartment::CreateComponentOnThread(const CLSID& clsid, const IID& iid, IUnknown** ppI) { HRESULT hr = ::CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, iid, (void**)ppI);

if (SUCCEEDED(hr)) { // Запросить интерфейс IX, который используется в WorkerFunction hr = (*ppI)->QueryInterface(IID_IX, (void**)&m_pIX);

if (FAILED(hr)) { // Если мы не можем с этим работать, на дадим и другим (*ppI)->Release();

return E_FAIL;

} } return hr;

} CClientApartment::CreateComponentOnThread запрашивает у созданного ею компонента интерфейс IX, который используется затем функцией WorkerFunction:

void CClientApartment::WorkerFunction() { if (m_pIX) { m_pIX->Tick();

} } CLIENT.CPP Теперь созданы и поток и компонент. Всякий раз, когда истекает интервал времени CSimpleApartment::m_WaitTime, CSimpleApartment::ClassThreadProc вызывает CClientApartment::WorkerFunction.

Таким образом, наш компонент обновляется каждые несколько миллисекунд. Для отображения этих изменений в своем окне клиент создает таймер. Получив сообщение WM_TIMER, клиент вызывает OnTick, которая обращается к IX::GetCurrentCount и затем отображает значение счетчика в окне. Когда клиент вызывает IX::GetCurrentCount, происходит маршалинг вызова через границу подразделения. Когда же WorkerFunction вызывает IX::Tick, имеет место вызов из того же самого подразделения, и маршалинг не производится.

Разделенные потоки могут создаваться не только клиентами. Можно разработать компоненты для создания разделенных потоков. Фактически можно создать фабрику класса, которая создает компоненты в разных разделенных потоках.

Вот и все. Как видите, самая сложная часть реализации разделенного потока — это создание и управление потоками.

Теперь, когда мы стали экспертами по разделенным потокам, давайте рассмотрим модель свободных потоков.

Реализация модели свободных потоков Если Вам приходилось писать многопоточные программы, то свободные потоки вряд ли составят для Вас по настоящему новые проблемы. Свободные потоки создаются и управляются обычными функциями Win32 для работы с потоками, такими как CreateThread, ResumeThread, WaitForMultipleObjects, WaitForSingleObject, CreateMutex и CreateEvent. Используя стандартные синхронизационные объекты — мьютексы, критические секции и семафоры, — Вы можете управлять доступом к внутренним данным своего компонента, сделав его «потокобезопасным». Хотя обеспечить настоящую потокобезопасность компонента — всегда непростая задача, хорошо разработанный интерфейс СОМ совершенно ясно покажет Вам, когда происходит доступ к компоненту.

Если Вы еще не писали многопоточных программ, то пример из этого раздела — хорошая отправная точка для обучения. Мы будем использовать несколько мютексов, чтобы предотвратить одновременный доступ нескольких потоков к один и тем же данным.

Помимо «потокобезопасности» компонентов, для использования свободных потоков надо выполнить, по существу, только три требования. Первое состоит в том, что Ваша операционная система должна поддерживать модель свободных потоков СОМ. Ее поддерживает Windows NT 4.0 и Windows 95 тоже, если Вы установили расширения DCOM. В гл. 10 мы рассматривали, как программным путем определить, что операционная система поддерживает свободные потоки. (В основном для этого нужно установить наличие в OLE32.DLL функции CoInitializeEx.) Говоря о CoInitializeEx: поток должен вызвать эту функцию с параметром COINIT_MULTITHREADED, чтобы обозначить себя как свободный. Что значит объявить поток свободным? Поток, создающий компонент, определяет, как компонент обрабатывает вызовы из других потоков. Если компонент создается свободным потоком, то он может быть вызван любым другим свободным потоком в любой момент.

После того, как поток вызывал CoInitializeEx с параметром COINIT_MULTITHREADED, он не может вызвать ее с другим параметром. Поскольку OleInitialize вызывает CoInitializeEx с параметром COINIT_APARTMENTTHREADED, постольку Вы не можете использовать библиотеку OLE из свободного потока.

Третье требование является фактически требованием не свободных, а разделенных потоков. Необходимо выполнять маршалинг указателей на интерфейсы при передаче их разделенным потокам. Кстати, это имеет значение только в том случае, если указатель передается не посредством интерфейса COM. Если указатель передается через интерфейс СОМ, то СОМ выполняет маршалинг автоматически. Если клиент находится в другом процессе, то и здесь СОМ выполняет маршалинг автоматически. Конечно, для автоматического маршалинга Вы должны предоставить СОМ DLL заместителя/заглушки. Маршалинг между разделенными потоками мы обсуждали в предыдущем разделе. Свободные потоки используют для маршалинга интерфейсов вручную те же самые функции CoMarshalInterThreadInterfaceInStream и CoGetInterfaceAndReleaseStream. Как мы увидим далее, СОМ может незаметно для нас оптимизировать маршалинг.

Для компонентов внутри процесса имеется четвертое требование. Они должны регистрировать себя в Реестре в качестве поддерживающих свободные потоки. Мы рассмотрим этот пункт в разделе «Информация о потоковой модели в Реестре».

Как видите, за исключением маршалинга интерфейсов в другие подразделения, требования модели свободных потоков просты. Самая сложная задача, связанная с этой моделью, состоит в обеспечении «потокобезопасности» компонентов. Однако это не требование СОМ, а стандартная проблема многопоточности.

Пример со свободным потоком Создание свободного потока не слишком отличается от создания разделенного потока. Каталог \CHAP12\FREE_THD содержит код создания двух свободных потоков, совместно использующих один компонент. Первый свободный поток увеличивает счетчик компонента (как в примере с разделенным потоком).

Другой счетчик уменьшает его. Кроме того, теперь мы будем представлять себе счетчик как находящийся то на одной, то на другой «стороне». Первый свободный поток переводит его «налево», а второй — «направо».

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

Очевидные отличия Самое очевидное отличие — замена имени CSimpleApartment на CSimpleFree. При создании свободного потока создается не новое подразделение, а лишь поток. Аналогично, CClientApartment теперь называется CClientFree.

Подчеркну, что CSimpleFree — не универсальный подход к созданию и управлению свободными потоками. Сам по себе CSimpleFree не «потокобезопасен». Он предназначен только для создания свободных потоков клиентом, использующим модель разделенных потоков. Недостаток устойчивости CSimpleFree компенсируется простотой.

CSimpleFree::ClassThreadProc Единственная функция, которой CSimpleFree существенно отличается от CSimpleApartment, — это ClassThreadProc. Вместо вызова CoInitialize, как это делается в CSimpleApartment, она вызывает CoInitializeEx(0, COINIT_MULTITHREADED). Прежде чем использовать CoInitializeEx, необходимо сделать две вещи. Во-первых, нужно определить _WIN32_WINNT = 0x400 или _WIN32_DCOM. Если этого не сделать, то в OBJBASE.H не будет определения CoInitializeEx. Во-вторых, мы должны во время выполнения программы убедиться, что операционная система поддерживает CoInitializeEx. Все это показано ниже:

BOOL CSimpleFree::ClassThreadProc() { BOOL bReturn = FALSE;

// Проверить наличие CoInitializeEx typedef HRESULT (stdcall *FPCOMINITIALIZE)(void*, DWORD);

FPCOMINITIALIZE pCoInitializeEx = reinterpret_cast( ::GetProcAddress(::GetModuleHandle("ole32"), "CoInitializeEx"));

if (pCoInitializeEx == NULL) { trace("Эта программа требует поддержки свободных потоков в DCOM");

SetEvent(m_hComponentReadyEvent);

return FALSE;

} // Инициализировать библиотеку COM HRESULT hr = pCoInitializeEx(0, COINIT_MULTITHREADED);

if (SUCCEEDED(hr)) { // Сигнал о начале работы SetEvent(m_hComponentReadyEvent);

// Создать массив событий HANDLE hEventArray[2] = { m_hCreateComponentEvent, m_hStopThreadEvent };

// Ждать команды на создание компонента BOOL bContinue = TRUE;

while (bContinue) { switch(::WaitForMultipleObjects(2, hEventArray, FALSE, m_WaitTime)) { // Создать компонент case WAIT_OBJECT_0:

CreateComponentOnThread();

break;

// Остановить поток case (WAIT_OBJECT_0 +1):

bContinue = FALSE;

bReturn = TRUE;

break;

// Выполнить фоновую обработку case WAIT_TIMEOUT:

WorkerFunction();

break;

default:

trace("Ошибка при ожидании", GetLastError());

} } // Освободить библиотеку COM CoUninitialize();

} // Сигнализировать, что мы закончили SetEvent(m_hComponentReadyEvent);

return bReturn;

} Поскольку CSimpleFree создает свободные потоки, ей не нужен цикл выборки сообщений. Поэтому я заменил MsgWaitForMultipleObjects на WaitForMultipleObjects. Для остановки потока вместо WM_QUIT используется m_hStopThreadEvent.

Хотя MsgWaitForMultipleObjects больше не нужна нам в ClassThreadProc, она по-прежнему используется в CSimpleFree::StartThread и в CSimpleFree::CreateComponent. Эти функции вызываются главным потоком (разделенным), поэтому они должны обрабатывать сообщения, чтобы не блокировать пользовательский интерфейс.

Фактически только в этом и состоят различия между CSimpleFree и CSimpleApartment.

CClientFree Я хочу продемонстрировать Вам работу двух свободных потоков, которые совместно используют один компонент, без маршалинга их указателей на интерфейсы. Для этого я добавил в CClientFree два метода.

CClientFree в этом примере со свободным потоком служит эквивалентом CClientApartment из предыдущего примера. CClientFree наследует CSimpleFree и реализует виртуальные функции CreateComponentOnThread и WorkerFunction. В CClientFree две новые функции — ShareUnmarshaledInterfacePointer и UseUnmarshaledInterfacePointer. (Меня до того воодушевили длинные имена некоторых функций СОМ, что я решил так называть и свои функции.) Первая, ShareUnmarshaledInterfacePointer, возвращает указатель на интерфейс IX, используемый CClientFree в его функции WorkerFunction. Маршалинг этого интерфейса не выполняется, поэтому его можно использовать только в свободном потоке. Вторая функция, UseUnmarshaledInterfacePointer, устанавливает указатель на IX, который объект CClientFree будет использовать в своей функции WorkerFunction. Теперь посмотрим, как эти функции используются в CLIENT.CPP.

Функция InitializeThread используется в CLIENT.CPP для создания свободного потока и компонента. Эта функция похожа на вызов InitializeApartment из примера однопоточного подразделения. После вызова InitializeThread клиент вызывает InitializeThread2. Эта функция создает второй поток. Однако вместо создания второго компонента этот поток использует компонент, созданный первым потоком. Код InitializeThread2 показан ниже:

BOOL InitializeThread2() { if (g_pThread == NULL) { return FALSE;

} // Создать второй поток // У этого потока другая WorkerFunction g_pThread2 = new CClientFree2;

// Запустить поток if (g_pThread2->StartThread()) { trace("Второй поток получен успешно");

// Получить тот же указатель, который использует первый поток IX* pIX = NULL;

pIX = g_pThread->ShareUnmarshaledInterfacePointer();

assert(pIX != NULL);

// Использовать этот указатель во втором потоке g_pThread2->UseUnmarshaledInterfacePointer(pIX);

pIX->Release();

return TRUE;

} else { trace("Ошибка при запуске второго потока");

return FALSE;

} } InitializeThread2 вместо объекта CClientFree создает объект CClientFree2. CClientFree2 отличается от CClientFree только реализацией WorkerFunction. Обе реализации приведены ниже:

void CClientFree::WorkerFunction() { CSimpleLock Lock(m_hInterfaceMutex);

if (m_pIX) { m_pIX->Tick(1);

m_pIX->Left();

} } void CClientFree2::WorkerFunction() { CSimpleLock Lock(m_hInterfaceMutex);

if (m_pIX) { m_pIX->Tick(-1);

m_pIX->Right();

} } CSimpleLock мы скоро обсудим. Я изменил IX::Tick, чтобы она принимала в качестве параметра размер увеличения счетчика. Я также добавил методы Left и Right. Эти функции управляют тем, на какой «стороне» находится счетчик. CClientFree увеличивает счетчик и помещает его «налево». CClientFree2 уменьшает его и помещает «направо». Функция InRightHand возвращает TRUE, если счетчик находится на правой стороне. Таким образом, с ее помощью мы можем определить, какой поток использовал компонент последним.

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

class CSimpleLock { public:

// Заблокировать CSimpleLock(HANDLE hMutex) { m_hMutex = hMutex;

WaitForSingleObject(hMutex, INFINITE);

} // Разблокировать ~CSimpleLock() { ReleaseMutex(m_hMutex);

} private:

HANDLE m_hMutex;

};

Конструктору CSimpleLock передается описатель мьютекса. Конструктор не возвращает управление, пока не дождется мьютекса. Деструктор CsimpleLock освобождает мьютекс, когда поток управления выходит из области действия переменной. Для защиты функции нужно просто создать объект CSimpleLock:

HRESULT stdcall CA::Tick(int delta) { CSimpleLock Lock(m_hCountMutex);

m_count += delta;

return S_OK;

} HRESULT stdcall CA::Left() { CSimpleLock Lock(m_hHandMutex);

m_bRightHand = FALSE;

return S_OK;

} Наш компонент использует два разных мьютекса — m_hHandMutex и m_hCountMutex. Один из них защищает счетчик, а второй — переменную, указывающую сторону. Наличие двух разных мьютексов позволяет одному потоку работать с переменной, указывающей сторону, пока второй работает со счетчиком. Доступ к компонентам в подразделении возможен только для одного потока — потока этого подразделения. Если бы компонент выполнялся в потоке подразделения, один поток не смог бы вызвать Left, если другой уже вызывает Tick. Однако при использовании свободных потоков синхронизация возлагается на разработчика компонента, который может использовать свое знание внутреннего устройства компонента для оптимальной синхронизации.

Оптимизация маршалинга для свободных потоков Как маршалинг, так и синхронизация работают медленно. Если возможно, избегайте их. Одно из правил, связанных с разделенными потоками, — необходимость маршалинга интерфейсов перед передачей разделенным потокам. Но предположим, что клиент в разделенном потоке хочет использовать интерфейс компонента свободных потоков в том же самом процессе. Нам в действительности не нужен маршалинг, так как процесс один и тот же. Нам также не нужна синхронизация вызовов нашего компонента, выполняемая СОМ;

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

Оптимизация не просто возможна — библиотека СОМ еще и предоставляет специальный агрегируемый компонент, который выполнит для Вас эту оптимизацию. CoCreateFreeThreadedMarshaler создает компонент с интерфейсом IMarshal, который определяет, находится ли клиент интерфейса в том же самом процессе. Если это так, то при маршалинге указатели передаются без изменений. Если клиент находится в другом процессе, то выполняется стандартный маршалинг интерфейса. Самое замечательное в CoCreateFreeThreadedMarshaler то, что Вам не нужно знать, кто является клиентом, — все чудеса происходят автоматически. Эта оптимизация работает и в сочетании с CoMarshalInterThreadInterfaceInStream и CoGetInterfaceAndReleaseStream. Это позволяет Вам выполнять явный маршалинг своих интерфейсов и предоставить заботу об оптимизации СОМ. Ниже приведен код, создающий маршалер свободных потоков. Также показана реализация QueryInterface, которая делегирует запросы на IMarshal маршалеру свободных потоков.

HRESULT CA::Init() { HRESULT hr = CUnknown::Init();

if (FAILED(hr)) { return hr;

} // Создать мьютекс для защиты счетчика m_hCountMutex = CreateMutex(0, FALSE, 0);

if (m_hCountMutex == NULL) { return E_FAIL;

} // Создать мьютекс для защиты индикатора стороны m_hHandMutex = CreateMutex(0, FALSE, 0);

if (m_hHandMutex == NULL) { return E_FAIL;

} // Агрегировать маршален свободных потоков hr = ::CoCreateFreeThreadedMarshaler( GetOuterUnknown(), &m_pIUnknownFreeThreadedMarshaler);

if (FAILED(hr)) { return E_FAIL;

} return S_OK;

} HRESULT stdcall CA::NondelegatingQueryInterface(const IID& iid, void** ppv) { if (iid == IID_IX) { return FinishQI(static_cast(this), ppv);

} else if (iid == IID_IMarshal) { return m_pIUnknownFreeThreadedMarshaler->QueryInterface(iid, ppv);

} else { return CUnknown::NondelegatingQueryInterface(iid, ppv);

} } Замечание о терминологии Как я говорил в начале главы, терминология СОМ, связанная с потоками, существенно различается в разных документах. Авторы Win32 SDK используют слово «apartment» («подразделение») не совсем так, как это делаю я. То, что я называю подразделением, они называют «многопоточное подразделение» для обозначения всей совокупности свободных потоков. В их терминах, в процессе может быть произвольное число «однопоточных подразделений», но только одно «многопоточное подразделение». Я надеюсь, что это разъяснение поможет Вам избежать путаницы при чтении документации Win32 SDK.

Информация о потоковой модели в Реестре СОМ необходимо знать, какую потоковую модель поддерживают компоненты внутри процесса, чтобы обеспечить правильный маршалинг их интерфейсов и синхронизацию при вызовах между потоками. Чтобы зарегистрировать потоковую модель своего компонента внутри процесса, добавьте в раздел компонента InprocServer32 параметр с именем ThreadingModel. (ThreadingModel — это именованный параметр, а не подраздел!) Для ThreadingModel допускается одно из трех значений: Apartment, Free или Both.

Должно быть очевидно, что компоненты, которые можно использовать в разделенных потоках, устанавливают этот параметр в значение Apartment. Компоненты, которые можно использовать в свободных потоках, задают значение Free. Компоненты, которые могут использоваться как разделенными, так и свободными потоками, используют значение Both. Если компонент ничего не знает о потоках, то параметр не задан вообще. Если параметр не существует, то подразумевается, что компонент не поддерживает многопоточность. Все компоненты, обслуживаемые данным сервером внутри процесса, должны иметь одну и ту же потоковую модель.

Резюме В одной главе мы не только научились реализовывать разделенные и свободные потоки, но также узнали, что такое подразделение. Подразделение — это концептуальный конгломерат, состоящий из потока и цикла выборки сообщений. Поток подразделения похож на типичный процесс Win32 в том смысле, что оба имеют один поток и цикл выборки сообщений. В одном процессе может быть любое число подразделений и свободных потоков.

Разделенные потоки должны инициализировать СОМ, иметь цикл выборки сообщений и выполнять маршалинг указателей на интерфейсы в другие потоки. Компонент, созданный в разделенном потоке, должен вызываться только создавшим его потоком. Аналогичное правило существует и для оконной процедуры. Серверы внутри процесса должны иметь «потокобезопасные» точки входа, но компоненты могут не быть «потокобезопасными», так как синхронизацию обеспечивает СОМ.

Свободные потоки должны инициализировать СОМ при помощи CoInitializeEx. Они не обязаны иметь цикл выборки сообщений, но по-прежнему обязаны выполнять маршалинг интерфейсов в разделенные потоки и в другие процессы. Им не нужно выполнять маршалинг интерфейсов в другие свободные потоки в том же самом процессе.

Встает вопрос, потоки какого типа следует использовать Вам? Код пользовательского интерфейса обязан использовать разделенные потоки. Это гарантирует, что сообщения будут обработаны и у пользователя не возникнет впечатления, что программа «зависла». Если Вы хотите выполнять в фоновом режиме только простейшие операции, следует использовать разделенные потоки. Их гораздо легче реализовывать, так как не нужно заботиться о «потокобезопасности» используемых в них компонентах.

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

Итак, какие же потоки нужно использовать в программе моделирования нашего вертолета? Я оставлю Вам этот вопрос в качестве домашнего задания.

13 глава Сложим все вместе Известная более двухсот лет китайская головоломка танграм состоит из семи фрагментов, из которых нужно складывать разные фигуры (рис. 13-1). Я люблю танграм за то, что из простых фрагментов можно сложить бесчисленное множество сложных форм.

Фрагменты танграма включают пять равнобедренных треугольников: два маленьких, один средний и два больших. Еще два кусочка — квадрат и параллелепипед. Все семь фрагментов показаны на рис. 13-1. Из них можно сложить разнообразные фигуры, от геометрических форм до фигур людей, животных, деревьев, машин и даже всех букв алфавита (см. примеры на рис. 13-2). Многие из таких фигур запоминаются и весьма выразительны. Например, слегка поворачивая квадратик, изображающий голову танграмного гребца в танграмной лодке, Вы видимо увеличиваете прилагаемое им усилие.

Рис. 13-1 Семь фрагментов танграма — простые геометрические фигуры Рис. 13-2 Из фрагментов танграма можно создавать самые разные фигурки. Кролик, вертолет и кошка показаны на рисунке Как и танграм, СОМ очень проста — и в то же время приложения, которые можно с ее помощью построить, могут быть очень мощными. По-моему, вариация на тему танграма прекрасно подходит для примера программы, которая идеи этой книги объединяет в одно целое.

Программа Tangram Первоначально я планировал построить эту книгу вокруг одной программы, а не давать разные примеры в каждой главе. Однако отзывы рецензентов быстро сделали очевидной нежизнеспособность такого подхода. СОМ напоминает скелет слона, я имею в виду, приложения. Приложение — это плоть, кожа и мышцы, которые поддерживает скелет. Трудно разглядеть скелет, он скрыт под мышцами и кожей;

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

Тем не менее, я считаю полезным показать новые идеи в конкретном контексте. Для этого и написан Tangram, законченное приложение СОМ для Microsoft Windows. Tangram демонстрирует большинство технологий, представленных в книге, все вместе и в одном приложении. Кроме того, Tangram демонстрирует некоторые OLE, ActiveX и COM интерфейсы, которые я еще не рассматривал.

Tangram в работе Откомпилированная версия программы находится в каталоге \TANGRAM на прилагаемом к книге диске. Сначала запустите REGISTER.BAT, чтобы зарегистрировать компоненты. После этого можете запускать приложение, дважды щелкнув мышью его значок.

При запуске Tangram выводится диалоговое окно, предоставляющее возможность выбрать один из вариантов работы программы:

!" Окно списка позволяет выбрать «мировой» компонент, который Вы хотите использовать для рисования танграма на экране. Компонент TangramGdiWorld рисует двумерное отображение, а TangramGLWorld — трехмерное. Если библиотека OpenGL в Вашей системе не установлена, то доступен только вариант TangramGdiWorld.

!" Флажок позволяет выбрать, будет ли «модельный» компонент, представляющий фрагменты танграма, выполняться внутри или вне процесса. Компоненты вне процесса исполняются локально (если только Вы сами не настроите их для удаленного выполнения, как описано в гл. 10).

После запуска Tangram Вы увидите на экране семь фрагментов. С помощью мыши их можно двигать. Щелчок правой кнопки поворачивает фрагмент против часовой стрелки. Если при этом удерживать клавишу Shift, то фрагмент поворачивается в противоположном направлении. Попробуйте поработать с программой, и Вы увидите, какие образы можно с ее помощью сложить!

Детали и составные части Исходный текст программы Tangram находится на прилагаемом к книге диске в подкаталоге \TANGRAM\SOURCE, там же Вы найдете указания по компоновке и регистрации программы.

Tangram состоит из нескольких компонентов и множества интерфейсов. Названия компонентов и интерфейсов, специфичных для Tangram, имеют префиксы Tangram и ITangram соответственно. Наличие префикса позволяет легко определить, какие интерфейсы относятся к данному примеру.

Чтобы облегчить Вам поиск в Реестре связанных с Tangram интерфейсов и компонентов, все GUID Tangram имеют следующие общие цифры:

B53313xxx-20C4-11D0-9C6C-00A0C90A632C Основные составляющие Tangram компоненты и интерфейсы приведены в табл. 13-1.

Таблица 13-1 Основные компоненты и интерфейсы программы Tangram Компонент Интерфейсы Назначение TangramModel ITangramModel Содержит информацию о форме и положении отдельного фрагмента ITangramTransform ITangramPointContainer TangramGdiVisual ITangramVisual Отображает один фрагмент ITangramGdiVisual Компонент Интерфейсы Назначение ITangramModelEvent TangramGdiWorld ITangramWorld Управляет процессом отображения ITangramGdiWorld ITangramCanvas TangramCanvas ITangramCanvas Осуществляет поддержку отображения Интерфейсы и компоненты из предыдущей таблицы, имена которых содержат Gdi, имеют эквиваленты с именами, содержащими GL. Версия TangramGdiVisual для OpenGL называется TangramGLVisual. Версии интерфейсов и компонентов для GDI представляют двумерное отображение игрового поля танграма, тогда как версии для OpenGL обеспечивают трехмерное представление.

Следующие разделы кратко описывают основные компоненты, составляющие программу Tangram. Упрощенная схема архитектуры приложения показана на рис. 13-3.

(Подозреваю, Вы подумали, что если таково упрощенное представление, то полное лучше не видеть.) TangramModel Клиентский EXE-модуль Список указателей на ITangramModel модели ITangramTransform pITangramTransform Указатель на m_pSelectedVisual получателя событий m_pWorld pCanvas TangramGdiWorld TangramGdiVisual Список указателей на m_pModel фигуры ITangramVisual ITangramWorld ITangramGdiVisual ITangramGdiWorld ITangramModelEvent TangramCanvas m_pGdiWorld ITangramCanvas Рис. 13-3 Схема архитектуры программы Tangram Клиентский EXE-модуль Код клиентского EXE-модуля склеивает все компоненты в общее приложение. У клиентского EXE нет интерфейсов, это обычный код на С++ для Win32, хотя клиент и использует MFC, что несколько упростило его программирование. Модуль содержит указатели на управляемые им интерфейсы. Он взаимодействует с Tangram*World через ITangramWorld, с текущей выбранной фигуркой через ITangramVisual и с TangramModel через ITangramModel и ITangramTransform.

В программе присутствует семь экземпляров компонента TangramModel — по одному на каждый фрагмент танграма. Каждому TangramModel соответствует свой Tangram*Visual. Последний взаимодействует с TangramModel через интерфейс ITangramModel. Компонент Tangram*World содержит семь Tangram*Visual.

Каждый ITangram*World управляет каждым из Tangram*Visual при помощи интерфейса ITangram*Visual.

Tangram*World также агрегирует TangramCanvas, чтобы получить реализацию ITangramCanvas, который используется клиентским EXE.

Компонент TangramModel TangramModel — это основа программы Tangram. Компонент TangramModel, который я иногда называю просто «модель», — это многоугольник, представляющий один фрагмент танграма. Клиент управляет фрагментом танграма при помощи интерфейсов ITangramModel и ITangramTransform.

Интерфейс ITangramModel ITangramModel инкапсулирует координаты многоугольника, представляющего фрагмент танграма. Программа помещает все фрагменты танграма на виртуальное игровое поле 20x20 и манипулирует ими, используя только координаты в этом поле. Переход координат вершин в виртуальном игровом поле к фигуркам, отображенным на экране, возлагается на компоненты, реализующие ITangramWorld и ITangramVisual.

Интерфейс ITangramTransform Клиент использует интерфейс ITangramTransform для перемещения и вращения фрагмента танграма. В программе Tangram перемещение выражается через координаты виртуального игрового поля. Поворот задается в градусах.

Интерфейс IConnectionPointerContainer Это стандартный интерфейс COM/ActiveX. Более подробно он рассматривается далее в разделе «События и точки подключения». Данный интерфейс предоставляет TangramModel гибкий способ информировать соответствующий Tangram*Visual об изменении положения компонента.

Компоненты TangramGdiVisual и TangramGLVisual Каждый компонент TangramModel имеет соответствующий компонент Tangram*Visual, или просто образ (visual).

Компонент TangramGdiVisual использует GDI для вывода двумерного изображения танграмной фигурки.

Компонент TangramGLVisual при помощи OpenGL «оживляет» трехмерный образ фигурки. Оба компонента содержат указатель на интерфейс ITangramModel компонента TangramModel. С помощью этого интерфейса компонент Tangram*Visual может получить координаты вершин соответствующего TangramModel и преобразовать их в экранные координаты. Компоненты Tangram*Visual реализуют три интерфейса:

ITangramVisual, ITangram*Visual и ITangramModelEvent.

Интерфейс ITangramVisual Интерфейс ITangramVisual используется в программе для получения модели, соответствующей данному образу, и для выделения заданной фигурки. Выделение влияет на то, как отображается соответствующий фрагмент танграма.

Интерфейсы ITangramGdiVisual и ITangramGLVisual Компонент TangramGdiWorld использует ITangramGdiVisual для отображения на экране двумерного представления TangramModel. Компонент TangramGLWorld через ITangramGLVisual взаимодействует с компонентами TangramGLVisual для вывода на экран трехмерных версий TangramModel.

Использование нескольких интерфейсов изолирует клиент от деталей вывода изображений, зависящих от реализации. TangramGdiWorld и TangramGLWorld инкапсулируют детали, отличающие двумерное рисование от трехмерного, и полностью изолируют их от клиента. Клиент может спокойно работать с фрагментами танграма на воображаемом игровом поле 20x20, независимо от того, как на самом деле осуществляется отображение этого поля компонентами Tangram*World и Tangram*Visual.

Если клиент и TangramModel могут игнорировать способ вывода фигурок танграма на дисплей, то Tangram*World и Tangram*Visual должны учитывать роль друг друга в рисовании этих образов. Tangram*World подготавливает экран, на котором рисует каждый из Tangram*Visual. Эти компоненты писались одновременно с учетом взаимодействия в паре. Учитывая, как определены интерфейсы, практически невозможно написать один компонент, не написав другой. Здесь Вы можете рассматривать сочетание этих двух классов COM как один «компонент».

ITangramModelEvent Компоненту ITangram*Visual необходимо знать об изменениях координат вершин соответствующего ITangramModel. Для этого ITangramModel определяет интерфейс событий с именем ITangramModelEvent. Всякий раз, когда изменяется положение вершин, TangramModel вызывает ITangramModelEvent::OnChangeModel для всех компонентов, ожидающих этого события (в данном случае таким компонентом является только соответствующий образ). Мы рассмотрим события позже в разделе «События и точки подключения».

Компоненты TangramGdiWorld и TangramGLWorld Каждый компонент Tangram*Visual содержится в соответствующем компоненте Tangram*World. Tangram*World отвечает за подготовку дисплея, на котором будут рисовать TangramVisual. Он также отвечает за перерисовку экрана и палитру. Tangram*World поддерживает три интерфейса: ITangramWorld, ITangram*World и ITangramCanvas.

ITangramWorld Клиентский EXE-модуль управляет компонентом Tangram*World через универсальный интерфейс ITangramWorld. Клиентский EXE очень мало взаимодействует с Tangram*Visual, предпочитая общаться с Tangram*World и предоставляя ему работать с Tangram*Visual.

Интерфейсы ITangramGdiWorld и ITangramGLWorld Эти интерфейсы используются компонентами Tangram*Visual для взаимодействия с соответствующим Tangram*World. Обратные указатели от компонентов к клиентам — очень мощное средство, но они могут создать циклические ссылки;

в результате счетчик ссылок компонента может никогда не уменьшиться до 0 и, таким образом, компонент может никогда не удалиться из памяти. Мы рассмотрим этот вопрос далее в разделе «Циклический подсчет ссылок».

Интерфейс ITangramCanvas Клиентский EXE делегирует интерфейсу ITangramCanvas компонента Tangram*World решение всех вопросов, связанных с дисплеем, включая вывод и обновление изображение на экране, а также работу с палитрой. Но хотя этот интерфейс поддерживается и TangramGdiWorld, и TangramGLWorld, ни один из них его не реализует. Вместо этого они агрегируют компонент TangramCanvas, который и реализует интерфейс.

Что демонстрирует пример Как я говорил выше, Tangram демонстрирует большинство представленных в книге технологий. Я кратко поясню, что здесь наиболее интересно.

Агрегирование Tangram*World агрегирует TangramCanvas, чтоб предоставить клиентскому EXE реализацию ITangramCanvas.

Включение Включение широко используется в программе Tangram. Как видно на рис. 13-3, Tangram*World включает Tangram*Visual, каждый из которых включает TangramModel.

Категории компонентов Tangram определяет категорию компонентов Tangram World. Членом этой категории является компонент, реализующий ITangramWorld и ITangramCanvas. Клиентский EXE использует категории компонентов, чтобы найти зарегистрированные компоненты, которые реализуют ITangramWorld и ITangramCanvas. Затем он дает пользователю возможность выбрать компонент, который тот хочет использовать.

Взаимозаменяемые компоненты Одна из задач СОМ — обеспечить возможность замены компонента другим компонентом, поддерживающим те же самые интерфейсы. Пары TangramGLWorld — TangramGLVisual и TangramGdiWorld — TangramGdiVisual взаимозаменяемы. Обе пары отображают на экране фигурки танграма, но совершенно по-разному.

Компоненты внутри процесса, локальные и удаленные Компоненты TangramModel могут исполняться внутри процесса, локально или удаленно. Клиентский EXE запрашивает пользователя, как их выполнять.

Следующие три раздела посвящены некоторым особенностям (деталям) Tangram, которые не были рассмотрены в предыдущих главах книги.

Файлы IDL В нескольких последних главах мы использовали один файл IDL для описания всех интерфейсов и компонентов приложения. Хотя это прекрасно подходит для учебного примера, хотелось бы, чтобы компонент видел только те интерфейсы, которые использует. В связи с этим в каждый файл IDL программы Tangram помещен один интерфейс или группа взаимосвязанных интерфейсов. В именах таких файлов IDL имеется суффикс _I.

Например, MODEL_I.IDL содержит определения ITangramModel и ITangramTransform. Для построения библиотеки типа нам необходимы операторы coclass и library. Операторы coclass, описывающие компоненты, помещены в отдельные файлы IDL, каждый из которых помечен суффиксом _C. Эти файлы импортируют файлы _I для используемых ими интерфейсов.

Этот подход отличает большая гибкость. Однако из каждого файла IDL получается несколько других файлов, так что их размножение сбивает с толку. Следующее соображение поможет Вам не путаться. Считайте, что _C означает CLSID, а _I — IID. Если Ваш код использует IID, необходимо включить соответствующий заголовочный файл _I. Например, IID_ITangramModel определен в MODEL_I.IDL. Если я запрашиваю IID_ITangramModel, то должен включить MODEL_I.H и скомпоновать с MODEL_I.C.

Если я создаю компонент TangramModel, мне нужен CLSID_TangramModel. Этот компонент описан в MODEL_C.IDL. Следовательно, нужно включить MODEL_C.H и скомпоновать с MODEL_C.C. Если файл IDL импортирует другой файл IDL, то в код на С++ необходимо включить заголовок для импортированного файла.

Например, MODEL_I.IDL импортирует EVENTS_I.IDL. Поэтому, если Вы включаете MODEL_I.H, то нужно также включить и EVENTS_I.H.

Замечу, что суффиксы _I и _C — это мое личное соглашение. Вы можете называть эти файлы как угодно. Без суффиксов я всегда путал, что где находится. Теперь, если компилятор говорит, что не может найти CLSID, я уже знаю, что мне нужно включить и скомпоновать с файлом _C.

Файл DLLDATA.C Компилятор MIDL не всегда генерирует новую версию DLLDATA.C. Во многих случаях Вам может потребоваться одна DLL заместителя/заглушки, которая поддерживает несколько интерфейсов. Однако эти интерфейсы определены в разных файлах IDL. Если компилятор MIDL находит существующий файл DLLDATA.C, он добавляет новые интерфейсы, а не создает новый файл. Поэтому следует периодически проверять DLLDATA.C, чтобы убедиться, что там присутствуют только те интерфейсы, которые Вам нужны.

Циклический подсчет ссылок Когда компонент TangramGdiWorld создает компонент TangramGdiVisual, первый передает последнему указатель на интерфейс ITangramGdiWorld. TangramGdiVisual использует этот интерфейс для преобразования в экранные координаты. К сожалению, при этом создается циклическая ссылка (рис. 13-4). TangramGdiWorld указывает на TangramGdiVisual, который указывает обратно на TangramGdiWorld.

Циклические ссылки не очень подходят для подсчета ссылок, так как результатом циклических ссылок могут быть компоненты, которые никогда не освобождаются из памяти. Например, TangramGdiWorld создает TangramGdiVisual и получает интерфейс ITangramGdiVisual, для которого вызывает AddRef. Кроме того, TangramGdiWorld передает указатель на свой интерфейс ITangramGdiWorld компоненту TangramGdiVisual, который также вызывает для этого указателя AddRef. Теперь счетчик ссылок как компонента TangramGdiVisual, так и компонента TangramGdiWorld равен как миниму единице.

TangramGdiWorld TangramGdiVisual ITangramGdiVisual Список указателей на образы ITangramGdiWorld m_pGdiWorld Рис. 13-4 Циклические ссылки в программе Tangram Далее TangramGdiWorld освобождает ITangramGdiVisual в своем деструкторе, который вызывается, когда счетчик ссылок станет равным 0. Но TangramGdiVisual имеет указатель на интерфейс ITangramGdiWorld компонента TangramGdiWorld, и не освобождает этот интерфейс, пока его счетчик ссылок не станет равным 0.

Результатом является взаимный захват, или «клинч». TangramGdiWorld не освободит TangramGdiVisual до тех пор, пока TangramGdiVisual не освободит TangramGdiWorld. TangramGdiVisual не менее упрям и не желает освобождать указатель, пока это не сделает TangramGdiWorld. Это очень похоже на двух баранов на мосту, ни один из которых не желает посторониться и пропустить другого.

Вы можете выбрать одно из трех решений этой проблемы: не вызывать AddRef, явно удалять компонент или использовать другой компонент.

Не вызывайте AddRef Первое решение — самое простое. Не увеличивайте счетчик ссылок одного из интерфейсов в ссылочном цикле.

Так поступает компонент TangramGdiVisual. Он не вызывает AddRef для указателя на интерфейс ITangramGdiWorld, полученного от TangramGdiWorld. TangramGdiVisual известно, что его время существования находится внутри времени существования TangramGdiWorld, и поэтому, пока он существует, обратный указатель правилен.

Эта техника используется достаточно часто, чтобы получить собственное имя. Ссылка на интерфейс, счетчик ссылок которого не был увеличен, называется слабой ссылкой (weak reference). Наличие слабой ссылки не удерживает компонент в памяти. Сильная ссылка (string reference) — это ссылка, вызывающая увеличение счетчика ссылок. Такая ссылка удерживает компонент в памяти (рис. 13.5).

Хотя этот метод проще всего, не всегда его можно использовать. Компоненту, имеющему слабую ссылку на другой компонент, необходимо знать, когда такая ссылка становится недействительной. TangramGdiVisual это не волнует, так как его время существования вложено во время существования TangramGdiWorld. Но если времена существования компонентов не вложены, необходим другой способ определять, что ссылка стала недействительной.

TangramGdiWorld TangramGdiVisual Список указателей на образы ITangramGdiVisual ITangramGdiWorld m_pGdiWorld Рис. 13-5 TangramGdiVisual поддерживает слабую ссылку на TangramGdiWorld. Слабая ссылка изображена пунктирной линией.

Используйте явное удаление Другой способ избежать захвата — предоставить одному из компонентов (или обоим) способ явно удалять другой компонент. Вместо того, чтобы ждать пока счетчик компонента станет равным 0, один из компонентов должен уметь приказать другому освободить все имеющиеся у того указатели на интерфейсы. Для этого нужно просто создать новый интерфейс с функцией, которая удаляет компонент (рис. 13-6).

TangramGdiWorld TangramGdiVisual ITangramGdiVisual Список указателей на образы Явный разрыв ссылки Ликвидировать ILifeTime на этот компонент ITangramGdiWorld m_pGdiWorld Рис. 13-6 Ссылочный цикл можно разорвать с помощью отдельной функции, которая заставляет компонент освободить имеющиеся у него указатели, прежде чем его собственный счетчик ссылок достигнет Но здесь следует быть осторожным. В реальной программе компонент, который Вы явно удаляете, может быть все еще кому-то нужен. Поэтому хорошей идеей будет реализовать еще один счетчик ссылок для истинно сильных ссылок, помимо традиционного. Пример счетчика истинно сильных ссылок — IClassFactory::LockServer.

Подробнее об этом см. гл. 7 и 10. Другими примерами истинно сильных ссылок являются IOleContainer::LockContainer и IExternalConnection::AddConnection. Эти функции предоставляют клиентам способ явно управлять временем существования компонентов.

Используйте отдельный компонент Другой способ разорвать ссылочный цикл — использовать отдельный объект или подкомпонент, на который указывает один из компонентов в цикле. Этот подкомпонент поддерживает слабую ссылку на свой внешний объект. Схематически это показано на рис. 13-7. Здесь TangramGdiWorld управляет временем жизни TangramGdiVisual, который управляет временем жизни подкомпонента TangramGdiWorld.

Подкомпоненты — самый гибкий способ избежать циклического подсчета ссылок. Вам не нужен доступ к исходному тексту или дополнительные сведения о компонентах;

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

TangramGdiWorld и TangramGdiVisual не используют подкомпонент для устранения ссылочного цикла.

TangramGdiVisual сам выступает в качестве подкомпонента и поддерживает на TangramGdiWorld слабую ссылку.

В то же время TangramGdiVisual и TangramModel используют подкомпонент во избежание циклических ссылок при реализации точек подключения. Подробнее мы поговорим об этом в следующем разделе.

TangramGdiWorld TangramGdiVisual Список указателей на образы ITangramGdiVisual слабая ссылка ITangramGdiWorld m_pGdiWorld подкомпонент сильные ссылки Рис. 13-7 Ссылочный цикл можно разорвать, используя подкомпоненты, поддерживающие слабые ссылки на своих родителей События и точки подключения До этого момента мы использовали только однонаправленную связь, когда клиент управляет компонентом. Но и компонент может выступать в качестве клиента и управлять другим компонентом. За исключением агрегирования, компоненты в этой книге никогда не имели указателя на свой клиент. В Tangram ситуация иная.

В предыдущем разделе мы видели, что у TangramGdiVisual имеется обратный указатель на TangramGdiWorld.

Одно из самых распространенных применений обратного указателя — уведомление клиента о различных событиях. Как мы видели в предыдущем разделе, проще всего информировать клиент о событии при помощи слабой ссылки. Но более искусный метод состоит в том, чтобы использовать подкомпонент со слабой ссылкой на один из компонентов.

При разработке управляющих элементов ActiveX (OLE) потребовался универсальный и гибкий механизм обработки событий. В качестве решения были использованы точки подключения (connection points). Точка подключения похожа на электрический разъем. Клиент реализует интерфейс, который подключается к точке подключения. Затем компонент вызывает реализованный клиентом интерфейс. Такие интерфейсы называются исходящими (outgoing) интерфейсами, или интерфейсами источника (source). В IDL для обозначения исходящего интерфейса используется атрибут source. (Пример можно найти в файле \TANGRAM\SOURCE\MODEL\MODEL_C.IDL.) Интерфейс называется исходящим, потому что в данном случае компонент вызывает клиент. Название «интерфейс источника» обусловлено тем, что компонент служит источником вызовов этого интерфейса.

Давайте рассмотрим очень простую схему обратного вызова. На рис. 13-8 TangramModel вызывает интерфейс ITangramModelEvent, реализованный компонентом TangramGdiVisual, однако во избежание ссылочного цикла он реализуется подкомпонентом, который перенаправляет вызовы компоненту TangramGdiVisual (Подобную реализацию мы уже встречали в разделе «Циклический подсчет ссылок».) На рис. 13-8 TangramModel является источником вызовов интерфейса ITangramModelEvent. Подразумевается, что у ITangramModelEvent имеется функция, инициализирующая m_pEvents. Это простое решение, и оно будет работать. Но его нельзя назвать слишком гибким. Во-первых, у клиента нет стандартного способа определить, какие события поддерживаются компонентом. Во-вторых, поддерживается только один интерфейс событий. В третьих, TangramModel может посылать события только одному клиенту. Во многих случаях необходимо информировать о произошедших событиях несколько клиентов. Эта проблема решается с помощью точек подключения.

TangramGdiWorld TangramGdiVisual m_pITangramVisual ITangramGdiVisual ITangramModelEvent ITangramModelEvent m_pGdiWorld Источник событий Получатель событий Рис. 13-8 Простая схема обратного вызова, которая не используется в программе Tangram Первую проблему решает интерфейс IConnectionPointContainer. Он содержит две функции: FindConnectionPoint и EnumConnectionPoints. Первая принимает IID исходящего интерфейса и возвращает указатель точки поключения для этого интерфейса. EnumConnectionPoints возвращает объект, который перечисляет все точки подключения, поддерживаемые компонентом. Это удовлетворяет второму требованию, поддержке компонентом более одного исходящего интерфейса. Перечислители, кстати, мы рассмотрим в следующем разделе.

Точка подключения — это объект, который реализует интерфейс IConnectionPoint. Каждому исходящему интерфейсу соответствует одна точка подключения. У каждой точки подключения может быть несколько получателей. IConnectionPoint::EnumConnections возвращает указатель на IEnumConnections объекта, который перечисляет все подключения. Каждому получателю также может соответствовать несколько источников, но соответствующая реализация — уже задача получателя.

TangramGdiWorld TangramGdiVisual CEnumConnec tionPoints m_pITangramVisual IConnectionPointContainer IEnumConnec ITangramModelEvent tionPoints Список ConnectionPoint CEnumCon CTangramModelEventSink nections ITangramModelEvent IConnectionPointContainer IEnumCon nections Список ConnectionPoint Рис. 13-9 Архитектура точек подключения На рис. 13-9 изображена архитектура точек подключения TangramModel. TangramGdiVisual использует IConnectionPointContainer, чтобы найти IConnectionPoint, соответствующий IID_ITangramModelEvent. Компонент TangramGdiVisual передает свой интерфейс функции IConnectionPoint::Advise. Это сообщает точке подключения о том, что TangramGdiVisual хочет получать уведомления о событиях. Для реализации точки подключения TangramModel использует несколько простых объектов СОМ. Эти объекты создаются с помощью оператора new С++, у них нет CLSID, и они не зарегистрированы в Реестре Windows. На рис. 13-9 эти объекты изображены прерывистыми линиями. Они реализуют перечислители для набора точек подключения, набор подключений и сам компонент-точку подключения. У TangramModel есть только одна точка подключения, но тем не менее реализован объект, поддерживающий интерфейс IEnumConnectionPoints. Перечислители будут рассматриваться в следующем разделе.

Представленная архитектура точек подключения (рис. 13-9) довольно сложна. Каждая точка подключения — это отдельный объект;

кроме того, присутствуют два перечислителя. Но дополнительная сложность дает большую гибкость.

IEnumXXX Наборы указателей на интерфейсы и других данных весьма важны в компонентных архитектурах. Поэтому в СОМ определен стандартный шаблон для перечисления содержимого набора. Здесь нет стандартного интерфейса, так как все версии шаблона работают с данными разных типов. Шаблон перечислителя определен как IEnumXXX, который имеет функции Reset, Next, Skip и Clone. Два примера такого интерфейса мы видели в предыдущем разделе — IEnumConnectionPoints и IEnumConnections. В гл. 6 для перечисления доступных категорий мы фактически использовали интерфейс IEnumCATEGORYINFO.

Метод Next интерфейса перечислителя возвращает элементы набора. Одна из интересных особенностей этого метода — то, что Вы можете за один раз получить из набора любое число элементов. Вместо того, чтобы копировать по одному элементу за раз, метод Next позволяет задать количество копируемых элементов. Это значительно повышает производительность при работе с удаленными компонентами, так как сокращает число циклов обмена данными по сети.

Использовать и реализовывать перечислители легко. Самое сложное — не забыть вызывать AddRef и Release для всех перечисляемых указателей на интерфейсы. Если Вы реализуете метод Next, следует вызвать для указателей на интерфейсы AddRef, прежде чем возвращать их клиенту. Если же Вы используете метод Next, то необходимо вызывать для возвращенных им указателей Release.

Фиксируют ли перечислители мгновенное состояние, или они «живые»? Обычно фиксируют, как, например, IEnumConnections и IEnumConnectionPoints. Объект-перечислитель, который Вы получаете в результате вызова IConnectionPoint::EnumConnections, представляет собой «мгновенный снимок» текущих подключений к данной точке. Если набор подключений изменился (из-за того, что другой клиент вызвал Advise для добавления или Unadvise для удаления подключения), Ваш мгновенный снимок обновлен не будет.

Основа COM — стандартные интерфейсы Как я уже неоднократно повторял, СОМ основана на интерфейсах. Чем больше компонентов используют одни и те же интерфейсы, тем больше вероятность полиморфного использования компонентов. Многие интерфейсы уже определены СОМ, OLE, управляющими элементами ActiveX, документами ActiveX и Автоматизацией.

Разработчику компонента СОМ следует изучить эти уже существующие интерфейсы. Даже если Вы решите не использовать их в своем приложении, то многое узнаете о создании с их помощью гибких компонентных архитектур.

У-у-ф!

Итак, мы подошли к концу. Вы знаете, как создавать интерфейсы СОМ на С++, реализовывать IUnknown и IClassFactory и регистрировать свои компоненты в Реестре Windows. Вам также известно, как создавать приложения из компонентов, включающих и агрегирующих другие компоненты. Вы знаете, как упростить себе жизнь с помощью классов С++ и smart-указателей. Вы также умеете описывать свои интерфейсы в файлах IDL, чтобы автоматически генерировать библиотеки маршалинга и библиотеки типа. Реализация IDispatch — это простой процесс, состоящий в использовании ITypeInfo. Наконец, Вы мастерски умеете создавать компоненты, реализующие модель разделенных потоков.

Если Вы решите написать компонент СОМ, то, учитывая Ваши знания, единственным недостающим ингредиентом будут конкретные интерфейсы СОМ. Вы знаете, как реализовать интерфейс. Теперь Вам нужно либо разработать собственный интерфейс, либо найти стандартный и реализовать его. Microsoft уже разработаны сотни интерфейсов для технологий ActiveX, DirectX и OLE. Управляющий элемент ActiveX — это просто реализация набора интерфейсов. Документ ActiveX — также набор интерфейсов с их реализациями.

Управляющие элементы и документы ActiveX используют много общих стандартных интерфейсов.

Реализация интерфейсов ActiveX, DirectX и OLE — непростая задача. Однако все это, как говорится, детали реализации. Проблема заключается не в СОМ, ведь после этой книги Вы стали настоящим экспертом по СОМ.

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

Pages:     | 1 |   ...   | 3 | 4 ||



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.