- •Оглавление
- •От автора
- •Введение
- •Преимущества использования компонентов
- •Адаптация приложений
- •Библиотеки компонентов
- •Распределенные компоненты
- •Требования к компонентам
- •Динамическая компоновка
- •Инкапсуляция
- •Заключительные замечания о компонентах
- •Повторное использование архитектур приложений
- •Соглашения о кодировании
- •Законченный пример
- •Взаимодействие в обход интерфейсов
- •Детали реализации
- •Теория интерфейсов, часть II
- •Интерфейсы не изменяются
- •Полиморфизм
- •Что за интерфейсом
- •Таблица виртуальных функций
- •Указатели vtbl и данные экземпляра
- •Множественные экземпляры
- •Разные классы, одинаковые vtbl
- •Запрос интерфейса
- •IUnknown
- •Получение указателя на IUnknown
- •Знакомство с QueryInterface
- •Использование QueryInterface
- •Реализация QueryInterface
- •А теперь все вместе
- •Правила и соглашения QueryInterface
- •Вы всегда получаете один и тот же IUnknown
- •Вы можете получить интерфейс снова, если смогли получить его раньше
- •Вы можете снова получить интерфейс, который у Вас уже есть
- •Вы всегда можете вернуться туда, откуда начали
- •Если Вы смогли попасть куда-то хоть откуда-нибудь, Вы можете попасть туда откуда угодно
- •QueryInterface определяет компонент
- •Вы не можете воспользоваться всеми знаниями сразу
- •Работа с новыми версиями компонентов
- •Когда нужно создавать новую версию
- •Имена версий интерфейсов
- •Неявные соглашения
- •Управление временем жизни
- •Подсчет ссылок
- •Подсчет ссылок на отдельные интерфейсы
- •Реализация AddRef и Release
- •Когда подсчитывать ссылки
- •Оптимизация подсчета ссылок
- •Правила подсчета ссылок
- •Амуниция пожарного, резюме
- •Создание компонента
- •Экспорт функции из DLL
- •Загрузка DLL
- •Разбиваем монолит
- •Тексты программ
- •Связки объектов
- •Негибкое связывание, резюме
- •HRESULT
- •Поиск HRESULT
- •Использование HRESULT
- •Определение собственных кодов ошибки
- •GUID
- •Зачем нужен GUID?
- •Объявление и определение GUID
- •Сравнение GUID
- •Передача GUID по ссылке
- •Реестр Windows
- •Организация Реестра
- •Редактор Реестра
- •Необходимый минимум
- •Другие детали Реестра
- •ProgID
- •Саморегистрация
- •Категории компонентов
- •OleView
- •Некоторые функции библиотеки COM
- •Инициализация библиотеки COM
- •Управление памятью
- •Преобразование строк в GUID
- •Резюме
- •CoCreateInstance
- •Прототип CoCreateInstance
- •Использование CoCreateInstance
- •Контекст класса
- •Листинг кода клиента
- •Но CoCreateInstance недостаточно гибка
- •Фабрики класса
- •Использование CoGetClassObject
- •IClassFactory
- •CoCreateInstance vs. CoGetClassObject
- •Фабрики класса инкапсулируют создание компонентов
- •Реализация фабрики класса
- •Использование DllGetClassObject
- •Общая картина
- •Листинг кода компонента
- •Последовательность выполнения
- •Регистрация компонента
- •Несколько компонентов в одной DLL
- •Повторное применение реализации фабрики класса
- •Выгрузка DLL
- •Использование DllCanUnloadNow
- •LockServer
- •Резюме
- •Включение и агрегирование
- •Включение
- •Агрегирование
- •Сравнение включения и агрегирования
- •Реализация включения
- •Расширение интерфейсов
- •Реализация агрегирования
- •Магия QueryInterface
- •Неверный IUnknown
- •Интерфейсы IUnknown для агрегирования
- •Создание внутреннего компонента
- •Законченный пример
- •Слепое агрегирование
- •Агрегирование и включение в реальном мире
- •Предоставление информации о внутреннем состоянии
- •Моделирование виртуальных функций
- •Резюме
- •Упрощения на клиентской стороне
- •Smart-указатели на интерфейсы
- •Классы-оболочки C++
- •Упрощения на серверной стороне
- •Базовый класс CUnknown
- •Базовый класс CFactory
- •Использование CUnknown и CFactory
- •Резюме
- •Разные процессы
- •Локальный вызов процедуры
- •Маршалинг
- •DLL заместителя/заглушки
- •Введение в IDL/MIDL
- •Примеры описаний интерфейсов на IDL
- •Компилятор MIDL
- •Реализация локального сервера
- •Работа примера программы
- •Нет точек входа
- •Запуск фабрик класса
- •Изменения в LockServer
- •Удаленный сервер
- •Что делает DCOMCNFG.EXE?
- •Но как это работает?
- •Другая информация DCOM
- •Резюме
- •Новый способ общения
- •Старый способ общения
- •Использование IDispatch
- •Параметры Invoke
- •Примеры
- •Тип VARIANT
- •Тип данных BSTR
- •Тип данных SAFEARRAY
- •Библиотеки типа
- •Создание библиотеки типа
- •Библиотеки типа в Реестре
- •Реализация IDispatch
- •Генерация исключений
- •Маршалинг
- •Что Вы хотите сделать сегодня?
- •Потоковые модели COM
- •Потоки Win32
- •Подразделение
- •Разделенные потоки
- •Свободные потоки
- •Маршалинг и синхронизация
- •Реализация модели разделенных потоков
- •Автоматический маршалинг
- •Ручной маршалинг
- •Настало время написать программу
- •Пример с разделенным потоком
- •Реализация модели свободных потоков
- •Пример со свободным потоком
- •Оптимизация маршалинга для свободных потоков
- •Информация о потоковой модели в Реестре
- •Резюме
- •Программа Tangram
- •Tangram в работе
- •Детали и составные части
- •Клиентский EXE-модуль
- •Компонент TangramModel
- •Компоненты TangramGdiVisual и TangramGLVisual
- •Компоненты TangramGdiWorld и TangramGLWorld
- •Что демонстрирует пример
- •Файлы IDL
- •Файл DLLDATA.C
- •Циклический подсчет ссылок
- •Не вызывайте AddRef
- •Используйте явное удаление
- •Используйте отдельный компонент
- •События и точки подключения
- •IEnumXXX
187
// Ошибка при вызове Invoke if (hr == DISP_E_EXCEPTION)
{
//Метод сгенерировал исключение.
//Сервер может отложить заполнение EXCEPTINFO.
if (excepinfo.pfnDefferedFillIn != NULL)
{
// Заполнить структуру EXCEPTINFO (*(excepinfo.pfnDefferedFillIn)(&excepinfo);
}
strstream sout;
sout << “Информация об исключительной ситуации в компоненте:”
<<endl
<<“ Источник: ” << excepinfo.bstrSource << endl
<<“ Описание: ” << excepinfo.bstrDescription
<<ends;
trace(sout.str());
}
}
Ошибки в аргументах
Если возвращаемое значение IDispatch::Invoke равно либо DISP_E_PARAMNOTFOUND, либо DISP_E_TYPEMISMATCH, то индекс аргумента, вызвавшего ошибку, возвращается в последнем параметре — puArgErr.
Теперь, познакомившись со всеми параметрами Invoke, давайте рассмотрим еще один пример вызова функции disp-интерфейса. Затем более подробно поговорим о VARIANT, а также рассмотрим два типа, которые могут содержаться в VARIANT: BSTR и SAFEARRAY.
Примеры
Код примера этой главы содержит компонент, который реализует дуальный интерфейс IX. Весь код можно найти на прилагающемся к книге диске. Для компиляции при помощи Microsoft Visual C++ воспользуйтесь командой:
nmake –f makefile
По этой команде будут построены версии компонента внутри и вне процесса. Интерфейс IX описан в SERVER.IDL так:
// Interface 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();
};
С этим компонентом могут работать два клиента. Клиент, содержащийся в файле CLIENT.CPP, подключается к компоненту с помощью vtbl, как мы делали в предыдущих главах. Клиент же из файла DCLIENT.CPP работает через disp-интерфейс. Ранее в этой главе мы уже видели, как он вызывает функцию Fx. Теперь посмотрим на вызов функции FxStringIn. Для большей ясности я убрал из кода обработку ошибок:
trace("Получить DispID метода \"FxStringIn\"."); name = L"FxStringIn";
hr = pIDispatch->GetIDsOfNames(IID_NULL, &name, 1,
GetUserDefaultLCID(),
188
&dispid);
//Передать компоненту следующую строку wchar_t wszIn[] = L"Это тестовая строка";
//Преобразовать строку Unicode в BSTR BSTR bstrIn;
bstrIn = ::SysAllocString(wszIn);
//Подготовить параметры и осуществить вызов
//Выделить и инициализировать аргумент VARIANT
VARIANTARG varg;
::VariantInit(&varg);// Инициализировать VARIANT.
varg.vt = VT_BSTR; |
// Тип данных VARIANT |
varg.bstrVal = bstrIn; |
// Данные для VARIANT |
// Заполнить структуру 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”)
189
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
то эта функция должна преобразовать число 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);
190
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 Win32
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. Возможны следующие значения:
* «Безопасный массив». — Прим.перев.