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

25
public:
virtual void Fy1() = 0; virtual void Fy2() = 0;
}; |
|
class CA : public IX, public IY |
// Компонент |
{ |
|
public: |
|
// Реализация абстрактного базового класса IX virtual void Fx1() { cout << “Fx1” << endl; } virtual void Fx2() { cout << “Fx2” << endl; }
// Реализация абстрактного базового класса IY virtual void Fy1() { cout << “Fy1” << endl; } virtual void Fy2() { cout << “Fy2” << endl; }
};
IX и IY — это чисто абстрактные базовые классы, которые используются для реализации интерфейсов. Чисто абстрактный базовый класс (pure abstract base class) — это базовый класс, который содержит только чисто виртуальные функции (pure virtual functions). Чисто виртуальная функция — это виртуальная функция, «помеченная =0 — знаком спецификатора чистоты (pure specifier). Чисто виртуальные функции не реализуются в классах, в которых объявлены. Как видно из приведенного выше примера, функции IX::Fx1, IX::Fx2, IY::Fy1 и IY::Fy2 только декларируются. Реализуются же они в производном классе. В приведенном фрагменте кода компонент CA наследует два чисто абстрактных базовых класса — IX и IY — и реализует их чисто виртуальные функции.
Для того, чтобы реализовать функции-члены IX и IY, CA использует множественное наследование. Последнее означает, что класс является производным более чем от одного базового класса. Класс С++ чаще всего использует единичное наследование, т.е. имеет только один базовый класс. Далее в этой главе мы более подробно поговорим о множественных интерфейсах и множественном наследовании.
Абстрактный базовый класс напоминает канцелярский бланк, а производный класс заполняет этот бланк. Абстрактный базовый класс определяет функции, которые будет предоставлять производный класс, а производные классы реализуют их. Открытое (public) наследование от чисто абстрактного базового класса называется наследованием интерфейса (interface inheritance), так как производный класс наследует лишь описания функций. Абстрактный базовый класс не содержит никакой реализации, которую можно было бы унаследовать.
В этой книге все интерфейсы будут реализованы при помощи чисто абстрактных базовых классов. Поскольку СОМ не зависит от языка программирования, имеется двоичный стандарт того, что в этой модели считается интерфейсом. В последнем разделе данной главы — «Что за интерфейсом» — представлена эта структура. По счастью, многие компиляторы С++ автоматически генерируют в памяти верную структуру, если использовать чисто абстрактные базовые классы.
IX и IY не совсем интерфейсы в смысле СОМ. Чтобы стать настоящими интерфейсами, IX и IY должны наследовать специальный интерфейс IUnknown. Однако IUnknown — это предмет следующей главы, поэтому я не буду обсуждать его сейчас. До конца данной главы мы будем считать, что IX и IY — это интерфейсы СОМ.
Соглашения о кодировании
В своих программах я использую некоторые соглашения, чтобы отличать интерфейсы от других классов. Все имена интерфейсов начинаются с буквы «I». Так, «IX» следует читать «интерфейс X». Имена классов имеют префикс «C», и «CA» читается как «класс A».
Другое соглашение состоит в том, что вместо определения интерфейса как класса я использую следующее определение из заголовочного файла OBJBASE.H Microsoft Win32 Software Development Kit (SDK):
#define interface struct
Определение использует ключевое слово struct, а не class, поскольку члены структуры автоматически объявляются имеющими общий доступ, так что не требуется ключевое слово public. Это меньше загромождает код. Ниже повторно приводятся примеры интерфейсов, записанные теперь в рамках новых соглашений.
#include <objbase.h> interface IX
{
virtual void __stdcall Fx1() = 0; virtual void __stdcall Fx2() = 0;
};

26
interface IY
{
virtual void __stdcall Fy1() = 0; virtual void __stdcall Fy2() = 0;
}
Чтобы показать интерфейс на картинке, я использую прямоугольник с «разъемом для подключения» на одной из сторон. Пример дан на рис. 2-2.
Компонент
IX
IY
Рис. 2-2 Компонент с двумя интерфейсами
На такой основе мы и будем рассматривать и реализовывать интерфейсы СОМ на С++. Согласитесь, что это не сложнее обычной азбуки (A, B, C++, …).
Законченный пример
Давайте рассмотрим несложную, но законченную реализацию интерфейсов IX и IY. Для реализации компонентов мы используем простую программу на С++ без динамической компоновки. Динамическую компоновку мы добавим в гл. 5, а пока гораздо проще обойтись без нее. В листинге 2-1 класс CA реализует компонент, который поддерживает интерфейсы IX и IY. В качестве клиента в этом примере выступает процедура main.
Копия приведенного в книге кода содержится в файле IFACE.CPP на прилагаемом к книге диске. Чтобы скомпилировать его с помощью Microsoft Visual C++, введите команду
cl iface.cpp
Соглашение о вызове __stdcall (или Pascal)
Возможно, Вы заметили в приведенном выше примере ключевое слово __stdcall. Это расширение языка, специфичное для компилятора Microsoft. (Вряд ли Вы сомневались, что какое-то расширение должно присутствовать.) Любой компилятор, поддерживающий разработку для Win32, поддерживает и это ключевое слово или его синоним. Это верно для компиляторов Borland, Symantec и Watcom. Функция, помеченная как __stdcall, использует соглашение о вызове языка Pascal. Такая функция выбирает параметры из стека перед возвратом в вызывающую процедуру. В соответствии же с обычным соглашением о вызове С/С++ стек очищает вызывающая процедура, а не вызываемая. В большинстве других языков, в том числе в Visual Basic, по умолчанию используется это же стандартное соглашение о вызове. Название «стандартное» применяется потому, что оно используется для всех функций Win32 API, за исключением имеющих переменное число аргументов. Для функций с переменным числом аргументов по-прежнему используется соглашение языка С, или __cdecl. Стандартное соглашение о вызовах применяется в Windows потому, что уменьшает размер кода, а первые версии Windows должны были работать на системах с 640 КБ памяти.*
Практически для всех функций, предоставляемых интерфейсами СОМ на платформах Microsoft, используется стандартное соглашение о вызовах. Только для функций с переменным числом аргументов применяется соглашение о вызовах С. Предполагается, что и Вы будете следовать этим правилам. Однако это требование не абсолютно. Вы можете использовать и другие соглашения о вызове, но должны их ясно документировать и учитывать, что клиенты, написанные на некоторых языках, могут оказаться не в состоянии использовать Ваши интерфейсы.
Если Вы предпочитаете слово, которое легче запомнить, используйте pascal. Оно определено в WINDEF.H как
#define pascal __stdcall
Если же Вы полагаете, что наличие в Вашем коде слова pascal сделает Вас жалким Pascal’истом, можете воспользоваться следующим определением из OBJBASE.H:
#define STDMETHODCALLTYPE __stdcall
* В языке Pascal параметры передаются в вызываемую процедуру слева на право (сначала первый, потом второй и т.д.), а в языке С — наоборот, справа налево (сначала последний, потом предпоследний и т.д.). Стандартное соглашение о вызове является компромиссом: порядок передачи параметров взят из С, а порядок очистки стека — из Pascal. Прим. ред.

27
IFACE.CPP
//
//Iface.cpp
//Копиляция: cl Iface.cpp
// |
|
#include <iostream.h> |
// Определить интерфейс |
#include <objbase.h> |
void trace(const char* pMsg) { cout << pMsg << endl; }
// Абстрактные интерфейсы interface IX
{
virtual void __stdcall Fx1() = 0; virtual void __stdcall Fx2() = 0;
};
interface IY
{
virtual void __stdcall Fy1() = 0; virtual void __stdcall Fy2() = 0;
};
// Реализация интерфейса class CA : public IX,
public IY
{
public:
// Реализация интерфейса IX
virtual void __stdcall Fx1() { cout << "CA::Fx1" << endl; } virtual void __stdcall Fx2() { cout << "CA::Fx2" << endl; }
// Реализация интерфейса IY
virtual void __stdcall Fy1() { cout << "CA::Fy1" << endl; } virtual void __stdcall Fy2() { cout << "CA::Fy2" << endl; }
};
// Клиент int main()
{
trace("Клиент: Создание экземпляра компонента");
CA* pA = new CA;
// Получить указатель IX IX* pIX = pA;
trace("Клиент: Использование интерфейса IX"); pIX->Fx1();
pIX->Fx2();
// Получить указатель IY IY* pIY = pA;
trace("Клиент: Использование интерфейса IY"); pIY->Fy1();
pIY->Fy2();
trace("Клиент: Удаление компонента"); delete pA;
return 0;
}
Листинг 2-1 Полный пример использования интерфейсов
Результаты работы этой программы таковы:
Клиент: Создание экземпляра компонента

28
Клиент: Использование интерфейса IX CA::Fx1
CA::Fx2
Клиент: Использование интерфейса IY CA::Fy1
CA::Fy2
Клиент: Удаление компонента
Как видно из текста, клиент и компонент взаимодействуют через два интерфейса. Последние реализованы с помощью двух чисто абстрактных базовых классов IX и IY. Компонент реализуется классом CA, который наследует как IX, так и IY. Класс CA реализует функции-члены обоих интерфейсов.
Клиент создает экземпляр компонента. Далее он получает указатели на интерфейсы, поддерживаемые компонентом. Потом клиент использует эти указатели совершенно аналогично указателям на классы С++, поскольку интерфейсы реализованы как чисто абстрактные базовые классы. Ключевыми в этом примере являются следующие моменты:
!" Интерфейсы СОМ реализуются как чисто абстрактные базовые классы С++ !" Один компонент СОМ может поддерживать несколько интерфейсов
!" Класс С++ может использовать множественное наследование для реализации компонента, поддерживающего несколько интерфейсов.
В этом примере я оставил в инкапсуляции некоторые «прорехи», которые мы заделаем в нескольких последующих главах. Но мне хотелось бы обсудить кое-какие проблемы сразу, поскольку они очевидны из листинга 2-1.
Взаимодействие в обход интерфейсов
Помните, как я говорил, что клиент и компонент взаимодействуют только через интерфейс? Клиент из листинга 2-1 не следует этому правилу. Он взаимодействует с компонентом посредством pA — указателя на класс CA, а не на интерфейс. Это может показаться несущественным, но на самом деле очень важно. Использование указателя на CA требует, чтобы клиент знал, как объявлен (обычно в заголовочном файле) класс CA. Объявление класса содержит множестве деталей реализации. Изменение этих деталей потребует перекомпиляции клиента. Компоненты (как я уже говорил) должны уметь добавлять и удалять интерфейсы без нарушения работы старых клиентов. Это одна из причин, по которым мы настаиваем, что клиент и компонент должны взаимодействовать только через интерфейсы. Вспомните, что интерфейсы основаны на чисто абстрактных базовых классах, с которыми не связана какая-либо реализация.
Конечно, не обязательно изолировать клиент от компонента, если они находятся в одном файле. Однако подобная изоляция необходима, если клиент и компонент подключаются друг к другу динамически, особенно когда у Вас нет исходных текстов. В гл. 3 мы исправим наш пример так, чтобы в нем не использовался указатель на CA. Клиенту более не потребуется знать как объявлен класс CA.
Использование указателя на CA — не единственное место, где клиент из предыдущего примера в обход интерфейса взаимодействует с компонентом. Для управления существованием компонента клиент применяет операторы new и delete. Эти операторы не только не входят ни в один из интерфейсов, но и специфичны для языка С++. В гл. 4 мы рассмотрим, как удалить компонент через интерфейс без помощи специфичного для языка оператора. В гл. 6 и 7 мы рассмотрим гораздо более мощный способ создания компонентов.
Теперь давайте обсудим некоторые более тонкие детали реализации клиента и компонента из предыдущего примера.
Детали реализации
Листинг 2-1 — это стандартная программа на С++. В ней нет ничего необычного, за исключением того, что она стала нашим первым шагом в создании компонента и клиента СОМ. Очень легко спутать требования СОМ к компоненту и конкретный способ реализации. В этом разделе я проясню некоторые места, где часто возникает путаница.
Класс — это не компонент
В листинге 2-1 класс CA реализует один компонент. СОМ не требует, чтобы один класс С++ соответствовал одному компоненту. Вы можете реализовать один компонент при помощи нескольких классов. На самом деле компонент можно реализовать вообще без классов. Классы С++ не используются при реализации компонентов СОМ на С, а потому они не обязательно должны использоваться и на С++. Просто компоненты СОМ гораздо легче реализовать через классы, чем строить вручную.