1. Основы архитектуры COM и определение интерфейсов на языке IDL
Основы архитектуры COM и определение интерфейсов на языке IDL
Добро пожаловать в курс по разработке In-Process COM сервера на C++. Это первая статья, в которой мы заложим теоретический фундамент. Прежде чем писать код и компилировать DLL, необходимо понять, что такое COM, почему он выглядит именно так, и как мы описываем контракты взаимодействия между компонентами.
Что такое COM и зачем он нужен?
COM (Component Object Model) — это бинарный стандарт взаимодействия компонентов, разработанный Microsoft. Ключевое слово здесь — бинарный. Это означает, что COM определяет, как объекты выглядят в памяти и как вызывать их методы на уровне машинного кода, а не на уровне исходного текста конкретного языка программирования.
Благодаря этому стандарту, COM решает проблему совместимости языков. Вы можете написать сервер на C++, а использовать его в клиенте, написанном на C#, Python, Delphi или VBA. Клиенту не нужно знать, на чем написан сервер; ему нужно лишь знать, как обратиться к его интерфейсу.
In-Process vs Out-of-Process
В рамках этого курса мы фокусируемся на In-Process серверах. Давайте разберем разницу:
* In-Process (In-Proc): Сервер реализован как динамическая библиотека (DLL). Он загружается непосредственно в адресное пространство процесса-клиента. Это обеспечивает максимальную скорость вызова методов, так как не требуется переключение контекста процесса. * Out-of-Process (Local Server): Сервер реализован как исполняемый файл (EXE). Он работает в отдельном процессе. Взаимодействие происходит через механизмы межпроцессного взаимодействия (IPC), что медленнее, но безопаснее (падение сервера не обрушит клиента).
Интерфейсы: Контракт взаимодействия
В мире COM клиент никогда не имеет доступа к самому объекту (структуре данных C++). Вместо этого он владеет указателем на интерфейс.
Интерфейс в COM — это группа логически связанных функций. Технически, интерфейс — это указатель на таблицу виртуальных функций (vtable). Если вы знакомы с C++, то COM-интерфейс для вас — это абстрактный базовый класс, содержащий только чисто виртуальные методы (pure virtual functions) и не имеющий полей данных.
IUnknown: Мать всех интерфейсов
Каждый COM-интерфейс обязан наследоваться от базового интерфейса IUnknown. Это закон. IUnknown определяет три фундаментальных метода, обеспечивающих управление жизнью объекта и навигацию по его возможностям:
QueryInterface — позволяет клиенту спросить у объекта: «А поддерживаешь ли ты другой интерфейс?». Если да, объект возвращает указатель на него.AddRef — увеличивает счетчик ссылок на объект.Release — уменьшает счетчик ссылок. Когда счетчик достигает нуля, объект удаляет сам себя из памяти.Управление памятью и подсчет ссылок
COM использует идиому Reference Counting (подсчет ссылок) для управления временем жизни объектов. Поскольку одним объектом могут пользоваться разные части программы (или даже разные программы), мы не можем просто удалить его оператором delete. Объект должен жить до тех пор, пока он кому-то нужен.
Логику изменения счетчика ссылок можно выразить простой формулой:
где — новое значение счетчика ссылок, — текущее значение, а — операция изменения ( для AddRef и для Release).
Когда выполняется условие:
где — текущее количество активных ссылок, объект обязан освободить занимаемую память.
Определение интерфейсов на языке IDL
Как объяснить компилятору C++ и другим языкам, как выглядит наш интерфейс? Для этого используется IDL (Interface Definition Language). Это декларативный язык, который не содержит логики выполнения, а только описывает типы данных и сигнатуры методов.
Файлы с расширением .idl компилируются специальным инструментом MIDL (Microsoft Interface Definition Language compiler). На выходе MIDL генерирует:
.h), которые мы будем наследовать._i.c), содержащий GUID-ы..tlb), которую могут читать высокоуровневые языки (C#, VB).Структура IDL файла
Рассмотрим пример простого IDL файла для нашего будущего сервера:
Разберем ключевые элементы:
* Атрибуты: В квадратных скобках [...] указываются метаданные.
* object: указывает, что это COM-интерфейс.
* uuid: (Universally Unique Identifier) — уникальный 128-битный идентификатор интерфейса. В COM все идентифицируется через UUID (или GUID).
* Наследование: interface IMathOperations : IUnknown — мы явно указываем наследование от IUnknown.
* Методы: Обратите внимание на возвращаемый тип HRESULT. В COM почти все методы возвращают код ошибки или успеха. Реальный результат возвращается через указатель, помеченный атрибутами [out, retval].
* Направленные атрибуты:
* [in]: данные передаются от клиента к серверу.
* [out]: данные передаются от сервера к клиенту.
* [retval]: параметр является возвращаемым значением функции для языков высокого уровня.
HRESULT
Тип HRESULT — это 32-битное целое число. Это стандартный способ сообщения о статусе операции. Наиболее частые значения:
* S_OK (0): Успех.
* E_POINTER: Передан неверный указатель.
* E_FAIL: Общая ошибка.
* E_NOINTERFACE: Запрошенный интерфейс не поддерживается.
Виртуальная таблица (vtable) и бинарная структура
Понимание того, как интерфейс выглядит в памяти — критически важно для C++ разработчика. Указатель на COM-интерфейс — это указатель на указатель на массив функций.
!Структура виртуальной таблицы (vtable), связывающая вызов клиента с реализацией метода.
В C++ это реализуется автоматически, если вы используете абстрактные классы. Компилятор C++ формирует vtable точно так же, как того требует стандарт COM. Именно поэтому C++ является "родным" языком для COM.
Когда клиент вызывает pMath->Add(10, 20, &res), на уровне ассемблера происходит следующее:
pMath, чтобы найти адрес vtable.Add (например, 4-й метод, так как первые 3 — это методы IUnknown).this) передается сам указатель pMath.Заключение
Мы рассмотрели фундаментальные принципы COM:
* COM — это бинарный стандарт, основанный на интерфейсах.
* Все интерфейсы наследуются от IUnknown, который управляет жизнью объекта (AddRef/Release) и навигацией (QueryInterface).
* IDL используется для описания интерфейсов независимо от языка реализации.
* In-Process сервер — это DLL, загружаемая в память клиента.
В следующей статье мы перейдем от теории к практике и начнем создавать структуру нашего проекта в Visual Studio, настраивать компиляцию MIDL и реализовывать наш первый C++ класс, поддерживающий IUnknown.