
- •Оглавление
- •От автора
- •Введение
- •Преимущества использования компонентов
- •Адаптация приложений
- •Библиотеки компонентов
- •Распределенные компоненты
- •Требования к компонентам
- •Динамическая компоновка
- •Инкапсуляция
- •Заключительные замечания о компонентах
- •Повторное использование архитектур приложений
- •Соглашения о кодировании
- •Законченный пример
- •Взаимодействие в обход интерфейсов
- •Детали реализации
- •Теория интерфейсов, часть 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

183
Использование IDispatch
Рассмотрим следующую программу на Visual Basic:
Dim Cmpnt As Object
Set Cmpnt = CreateObject(“InsideCOM.Chap11.Cmpnt11”)
Cmpnt.Fx
Эта маленькая программа создает компонент СОМ и вызывает функцию Fx через интерфейс IDispatch, реализованный компонентом. Взглянем теперь на аналогичную программу на С++. Во-первых, необходимо создать компонент по его ProgID. (Эта процедура обсуждалась в гл. 6.) Приведенный ниже код, взятый из файла DCLIENT.CPP примера этой главы (который находится на прилагающемся к книге диске), создает компонент, используя ProgID. (Для ясности я убрал проверки ошибок.)
//Инициализировать библиотеку OLE. HRESULT hr = OleInitialize(NULL);
//Получить CLSID приложения.
wchar_t progid[] = L”InsideCOM.Chap11”; CLSID clsid;
hr = ::CLSIDFromProgID(progid, &clsid);
// Создать компонент. IDispatch* pIDispatch = NULL;
hr = ::CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IDispatch, (void**)&pIDispatch);
Чтобы не делать лишнего вызова QueryInterface, я запросил у CoCreateInstance указатель на IDispatch. Теперь, имея этот указатель, мы можем получить DISPID функции Fx. Функция IDispatch::GetIDsOfNames принимает имя функции и в виде строки возвращает соответствующий DISPID:
DISPID dispid; |
|
OLECHAR* name = L”Fx”; |
|
pIDispatch->GetIDsOfNames( |
// Должно быть IID_NULL |
IID_NULL, |
|
&name, |
// Имя функции |
1, |
// Число имен |
GetUserDefaultLCID(), |
// Информация локализации |
&dispid); |
// Диспетчерский идентификатор |
С точки зрения клиента DISPID — просто средство оптимизации, позволяющее избежать передачи строк. Для сервера же DISPID — идентификатор функции, которую хочет вызвать клиент.
Имея DISPID для Fx, мы можем вызвать эту функцию, передав DISPID IDispatch::Invoke, которая представляет собой сложную функцию. Ниже приведен один из простейших вариантов вызова Invoke. Здесь Fx вызывается без параметров:
// Подготовить аргументы для Fx DISPPARAMS dispparamsNoArgs = {
NULL, |
|
|
NULL, |
// Ноль аргументов |
|
0, |
|
|
0, |
// Ноль именованных аргументов |
|
}; |
|
|
// Простейший вызов Invoke |
|
|
pIDispatch->Invoke(dispid, |
// DISPID |
|
|
IID_NULL, |
// Должно быть IID_NULL |
|
GetUserDefultLCID(), |
// Информация локализации |
|
DISPATCH_METHOD, |
// Метод |
|
&dispparamsNoArgs, |
// Аргументы метода |
|
NULL, |
// Результаты |
|
NULL, |
// Исключение |
|
NULL); |
// Ошибка в аргументе |
Контроллер Автоматизации не обязан что-либо знать о сервере Автоматизации. Контроллеру не нужен заголовочный файл с определением функции Fx. Информация об этой функции не зашита в программу. Сравните это с самим интерфейсом IDispatch, который является интерфейсом СОМ. IDispatch определен в OAIDL.IDL. код вызова членов IDispatch генерируется во время компиляции и остается неизменным. Однако вызываемая функция определяется параметрами Invoke. Эти параметры, как и параметры всех функций могут меняться во время выполнения.

184
Преобразовать приведенный выше фрагмент кода в программу, которая будет вызывать любую функцию без параметров, легко. Просто запросите у пользователя две строки — ProgID и имя функции — и передайте их
CLSIDFromProgID и GetIDsOfNames. Код вызова Invoke останется неизменным.
Сила Invoke в том, что она может использоваться в полиморфно. Любой реализующий ее компонент можно вызывать при помощи одного и того же кода. Однако у этого есть своя цена. Одна из задач IDispatch::Invoke — передача параметров вызываемой функции. Число типов параметров, которые Invoke может передавать, ограничено. Более подробно об этом мы поговорим ниже, в разделе, где будет обсуждаться VARIANT. Но прежде чем поговорить о параметрах функций disp-интерфейсов, давайте рассмотрим параметры самой IDispatch::Invoke.
Параметры Invoke
Рассмотрим параметры функции Invoke более подробно. Первые три параметра объяснить нетрудно. Первый — это DISPID функции, которую хочет вызвать контроллер. Второй параметр зарезервирован и должен быть равен IID_NULL. Третий параметр содержит информацию локализации. Рассмотрим более детально оставшиеся параметры, начиная с четвертого.
Методы и свойства
Все члены интерфейса СОМ — функции. Интерфейсы СОМ, многие классы С++ и даже Win32 API моделируют доступ к переменной с помощью функций «Get» и «Set». Пусть, например, SetVisible делает окно видимым, а GetVisible возвращает текущее состояние видимости окна:
if (pIWindow->GetVisible() == FALSE)
{
pIWindow->SetVsible(TRUE);
}
Но для Visual Basic функций «Get» и «Set» недостаточно. Основная задача Visual Basic — сделать все максимально простым для разработчика. Visual Basic поддерживает понятие свойств (properties). Свойства — это функции «Get/Set», с которыми программист на Visual Basic работает как с переменными. Вместо синтаксиса вызова функции программист использует синтаксис обращения к переменной:
’ Код VB
If Window.Visible = False Then
Window.Visible = True
End If
Атрибуты IDL propget и propput указывают, что данная функция СОМ должна рассматриваться как свойство. Например:
[
object, uuid(D15B6E20-0978-11D0-A6BB-0080C7B2D682), pointer_default(unique),
dual
]
interface IWindow : IDispatch
{
...
[propput]
HRESULT Visible([in] VARIANT_BOOL bVisible); [propget]
HRESULT Visible([out, retval] VARIANT_BOOL* pbVisible);
...
}
Здесь определяется интерфейс со свойством Visible. Функция, помеченная как propput, принимает значение свойства в качестве параметра. Функция помеченная propget, возвращает значение свойства как выходной параметр. Имена свойства и функции совпадают. Когда MIDL генерирует для функций propget и propput заголовочный файл, он присоединяет к имени функции префикс get_ или put_. Следовательно, на С++ эти функции должны вызываться так:
VARIANT_BOOL vb; get_Visible(&vb);
{
put_Visible(VARIANT_TRUE);
}

185
Возможно, Вы уже начали понимать, почему я, программист на С++, не люблю disp-интерфейсы. Что хорошо на Visual Basic, плохо на С++. Я рекомендую Вам предоставлять интерфейс СОМ низкого уровня для пользователей Visual Basic и Java. Такой дуальный интерфейс можно реализовать с помощью интерфейсов СОМ низкого уровня. Писать хорошие интерфейсы достаточно сложно, даже если не пытаться удовлетворить два разных класса разработчиков. Кстати, VARIANT_TRUE — 0xFFFF.
Возможно, Вы удивлены, какое все это имеет отношение к четвертому параметру IDispatch::Invoke. Все просто — одно имя, например Visible, может быть связано с четырьмя разными функциями: нормальной функцией, функцией установки значения свойства, функцией установки значения свойства по ссылке и функцией, которая возвращает значение свойства. У всех этих одноименных функций будет один и тот же DISPID, но реализованы они могут быть совершенно по-разному. Таким образом, Invoke необходимо знать, какую функцию вызывать. Необходимая информация задается одним из следующих значений четвертого параметра:
DISPATCH_METHOD
DISPATCH_PROPERTGET
DISPATCH_PROPERTYPUT
DISPATCH_PROPERTYPUTREF
Параметры функций disp-интерфейсов
Довольно запутанно, не так ли? Пятый параметр IDispatch::Invoke содержит параметры вызываемой функции. Понятнее это будет на примере. Пусть мы вызываем Invoke для установки свойств Visible в True. Аргументы функции, к которой мы обращаемся, передаются в пятом параметре Invoke. Таким образом, нам нужно передать в этом пятом параметре True — новое значение свойства Visible.
Пятый параметр — это структура DISPPARAMS, определение которой таково:
typedef struct tagDISPPARAMS { |
// Массив аргументов |
VARIANTARG* rgvarg; |
|
DISPID* rgdispidNamedArgs; |
// DISPID для именованных аргументов |
unsigned int cArgs; |
// Число аргументов |
unsigned int cNamedArgs; |
// Число именованных аргументов |
} DISPPARAMS; |
|
Visual Basic и disp-интерфейсы поддерживают концепцию именованных аргументов. Именованные аргументы позволяют программисту задавать параметры функции в любом порядке, передавая вместе со значениями параметра его имя. Эта концепция малополезна для программиста на С++, и у нас есть гораздо более важные темы для обсуждения, поэтому я не собираюсь ее здесь рассматривать. В этой книге rgdispidNamedArgs всегда будет равен NULL, а cNamedArgs — 0.
Первый элемент (rgvarg) структуры DISPPARAMS — это массив аргументов. Поле cArgs задает число аргументов в данном массиве. Каждый аргумент имеет тип VARIANTARG, и именно поэтому число типов параметров, которые могут передаваться между контроллером и сервером Автоматизации, ограничено. Функциям disp-интерфейса или дуального интерфейса можно передавать только такие параметры, которые можно поместить в структуру VARIANTARG (поскольку функции в vtbl должны соответствовать функциям, доступным через Invoke).
VARIANTARG — это то же самое, что и VARIANT. Знакомый нам файл IDL Автоматизации OAIDL.IDL дает следующее определение VARIANT:
typedef struct tagVARIANT { VARTYPE vt;
unsigned short wReserved1; unsigned short wReserved2; unsigned short wReserved3;
union { |
|
|
Byte |
bVal; |
// VT_UI1. |
Short |
iVal; |
// VT_I2. |
long |
lVal; |
// VT_I4. |
float |
fltVal; |
// VT_R4. |
double |
dblVal; |
// VT_R8. |
VARIANT_BOOL |
boolVal; |
// VT_BOOL. |
SCODE |
scode; |
// VT_ERROR. |
CY |
cyVal; |
// VT_CY. |
DATE |
date; |
// VT_DATE. |
BSTR |
bstrVal; |
// VT_BSTR. |
DECIMAL |
FAR* pdecVal |
// VT_BYREF|VT_DECIMAL. |
IUnknown |
FAR* punkVal; |
// VT_UNKNOWN. |
IDispatch |
FAR* pdispVal; |
// VT_DISPATCH. |
SAFEARRAY |
FAR* parray; |
// VT_ARRAY|*. |

186
Byte |
FAR* pbVal; |
// VT_BYREF|VT_UI1. |
short |
FAR* piVal; |
// VT_BYREF|VT_I2. |
long |
FAR* plVal; |
// VT_BYREF|VT_I4. |
float |
FAR* pfltVal; |
// VT_BYREF|VT_R4. |
double |
FAR* pdblVal; |
// VT_BYREF|VT_R8. |
VARIANT_BOOL |
FAR* pboolVal; |
// VT_BYREF|VT_BOOL. |
SCODE |
FAR* pscode; |
// VT_BYREF|VT_ERROR. |
CY |
FAR* pcyVal; |
// VT_BYREF|VT_CY. |
DATE |
FAR* pdate; |
// VT_BYREF|VT_DATE. |
BSTR |
FAR* pbstrVal; |
// VT_BYREF|VT_BSTR. |
IUnknown |
FAR* FAR* ppunkVal; |
// VT_BYREF|VT_UNKNOWN. |
IDispatch |
FAR* FAR* ppdispVal; |
// VT_BYREF|VT_DISPATCH. |
SAFEARRAY |
FAR* FAR* pparray; |
// VT_ARRAY|*. |
VARIANT |
FAR* pvarVal; |
// VT_BYREF|VT_VARIANT. |
void |
FAR* byref; |
// Generic ByRef. |
char |
cVal; |
// VT_I1. |
unsigned short |
uiVal; |
// VT_UI2. |
unsigned long |
ulVal; |
// VT_UI4. |
int |
intVal; |
// VT_INT. |
unsigned int |
uintVal; |
// VT_UINT. |
char FAR * |
pcVal; |
// VT_BYREF|VT_I1. |
unsigned short FAR * |
puiVal; |
// VT_BYREF|VT_UI2. |
unsigned long FAR * |
pulVal; |
// VT_BYREF|VT_UI4. |
int FAR * |
pintVal; |
// VT_BYREF|VT_INT. |
unsigned int FAR * |
puintVal; |
//VT_BYREF|VT_UINT. |
};
};
Как видите, VARIANT — это просто большое объединение (union) разных типов. VARIANT всегда использовался в Visual Basic для унифицированного хранения переменных разных типов. Идея оказалась настолько хороша, что разработчики Visual Basic решили выпустить ее в свет. Скоро мы рассмотрим, как ее использовать. Для нас, однако, важно то, что disp-интерфейсы и дуальные интерфейсы могут передавать только те типы, которые можно выразить при помощи VARIANT. Теперь продолжим рассмотрение Invoke.
Возврат результатов
Шестой параметр, pVarResult — это указатель на VARIANT, который будет содержать результат выполнения метода (или propget), исполняемого Invoke. Этот параметр может быть равен NULL для методов, не возвращающих значение, а также для propput и propputref.
Исключения
Следующий параметр IDispatch::Invoke — указатель на структуру EXCEPINFO. Если в процессе работы метода или свойства, вызванного с помощью Invoke, возникнет исключение (исключительная ситуация), структура будет заполнена информацией об этой ситуации. Структуры EXCEPINFO используются в тех же случаях, что и исключения в C++.
Ниже приведено определение EXCEPINFO. BSTR — это строка специального формата, о которой мы поговорим далее в этой главе.
typedef struct tagEXCEPINFO { |
// Код ошибки |
WORD wCode; |
|
WORD wReserved; |
// Источник исключительной ситуации |
BSTR bstrSource; |
|
BSTR bstrDescription; |
// Описание ошибки |
BSTR bstrHelpFile; |
// Полное имя файла справки |
DWORD dwHelpContext; |
// Контекст внутри файла справки |
ULONG pvReserved; |
// Функция для заполнения этой структуры |
ULONG pfnDefferedFillIn; |
|
SCODE scode; |
// Код возврата |
} EXCEPINFO; |
|
Значение, идентифицирующее ошибку, должно содержаться либо в коде ошибки (wCode), либо в коде возврата (scode), при этом другое поле должно быть нулем. Ниже приведен простой пример использования структуры
EXCEPINFO:
EXCEPINFO excepinfo;
HRESULT hr = pIDispatch->Invoke(..., &excepinfo); if (FAILED(hr))
{