Введение в COM |
Основные понятияCOM (Component Object Model) — это не зависящий от платформы и языка программирования объектно-ориентированный стандарт создания и использования двоичных программных компонентов. В рамках каждого такого программного компонента, называемого COM-сервером, реализуется один или несколько COM-классов, доступ к экземплярам которых осуществляется через предоставляемые ими интерфейсы. Под интерфейсом понимают группу логически связанных между собой функций, прототипы и порядок использования которых жестко определены, а реализация — нет. Эти функции принято называть методами интерфейса. Интерфейсы строго типизованы и не могут изменяться. Каждый COM-класс и каждый интерфейс однозначно идентифицируются в системе так называемым глобально-уникальным идентификатором (GUID), представляющим собой 128-битную величину, которая должна быть назначена на этапе разработки COM-сервера. Эта величина строится на основании текущего системного времени, взятого с точностью до 100 нс и при наличии сетевой карты — на основании ее номера (MAC-адреса). Применительно к COM-классам используется термин class identifier (идентификатор класса, сокращенно CLSID), а применительно к интерфейсам — interface identifier (идентификатор интерфейса, сокращенно IID). С точки зрения C/C++ GUID является структурой, определенной в заголовочном файле <winnt.h>; для генерации таких идентификаторов можно воспользоваться утилитой guidgen.exe, находящейся в папке Visual Studio\Common\Tools. Каждый COM-объект должен хранить внутренний счетчик ссылок, то есть количество используемых в данный момент интерфейсов. Когда это значение станет равным нулю, COM-объект должен выгрузить себя из памяти. Когда COM-сервер выгрузит из памяти все созданные им объекты, он будет автоматически выгружен системой. В рамках C++ интерфейс обычно определяют как структуру, содержащую один или несколько чисто-виртуальных методов и не содержащую никаких полей. COM-класс при этом реализуют либо как класс-потомок одного или нескольких интерфейсов, либо как класс, содержащий вложенные реализации соответствующих интерфейсов (такой подход используется в библиотеке MFC). Рассмотрим небольшой пример:
Каждый интерфейс должен порождаться от интерфейса с именем IUnknown; следовательно, каждый COM-класс будет содержать, как минимум, реализацию методов этого интерфейса. При использовании множественного наследования интерфейсы класса могут совместно использовать методы IUnknown, а при использовании вложенных классов эти методы необходимо реализовывать для каждого интерфейса. Интерфейс IUnknown
Интерфейс IUnknown содержит методы, предназначенные для реализации счетчика ссылок и получения указателя на один интерфейс через указатель на другой. ULONG AddRef(void);
Реализация метода AddRef должна увеличивать на 1 величину счетчика ссылок соответствующего COM-объекта и возвращать это значение. Заметим, что документация рекомендует использовать возвращаемое значение только в диагностических и отладочных целях. ULONG Release(void);
Реализация метода Release должна уменьшать на 1 величину счетчика ссылок соответствующего COM-объекта и возвращать это значение. Использовать возвращаемое этим методом значение также рекомендуется только в диагностических и отладочных целях. HRESULT QueryInterface( REFIID iid, void** ppvDest ); Реализация метода QueryInterface должна записывать указатель на интерфейс с идентификатором iid по адресу ppvDest. При успешном выполнении метод должен возвращать значение S_OK, а в случае ошибок — E_NOINTERFACE (при этом можно также присвоить указателю по адресу ppvDest значение NULL). Заметим, что для C тип REFIID является псевдонимом типа IID*, а для C++ — типа IID&. COM-объект должен сам вызывать метод AddRef при успешной передаче указателя на свой интерфейс клиенту; как правило это делается в реализации метода QueryInterface. По окончании работы с интерфейсом приложение-клиент должно вызвать для него метод Release:
Если приложение-клиент создает копию ранее полученного указателя на интерфейс, то оно должно самостоятельно вызвать для этой копии метод AddRef:
Заметим, что то же самое необходимо делать и при передаче указателя на интерфейс как параметра в функцию, поскольку в этом случае на стеке неявно создается копия этого указателя:
Разработка приложения-клиентаРассмотрим основные шаги, которые необходимо предпринять при разработке приложения, использующего технологию COM. 1. Заголовочный файл
Указанный заголовочный файл содержит основные объявления, необходимые при работе с COM-библиотекой, и является для COM-приложений тем же, чем <windows.h> — для обычных. Кроме того, он включает другие заголовки, в частности, <unknwn.h> и <objidl.h>. 2. Библиотека импортаВ список библиотек, используемых при компоновке приложения, необходимо добавить библиотеку импорта ole32.lib; для этого в IDE Visual C++ 6.0 необходимо в меню Project выбрать команду Settings, в появившемся диалоговом окне перейти на вкладку Link, выбрать категорию General и вписать указанное имя в поле ввода Object/library modules. Альтернативным способом является использование в исходном тексте приложения директивы
которая распознается всеми последними версиями компиляторов C/C++ от Microsoft и Borland. 3. Выбор модели параллелизма (concurrency model)Архитектура COM предусматривает использование COM-объектов в двух моделях параллелизма (автору не известен менее шершавый перевод термина concurrency model):
Требуемая модель задается при инициализации COM-библиотеки (так часто называют ole32.dll, являющуюся ядром COM). 4. Инициализация COM-библиотекиПеред какими-либо обращениями к COM-библиотеке приложение-клиент должно ее инициализировать. Это можно сделать с помощью одной из двух следующих функций: HRESULT CoInitialize( void* pvReserved ); Данная функция инициализирует COM-библиотеку для вызывающего потока в модели STA. Параметр pvReserved необходимо задавать равным NULL. Возвращается одно из следующих значений:
HRESULT CoInitializeEx( void* pvReserved, DWORD fdwFlags ); Как и в предыдущем случае, параметр pvReserved необходимо задавать равным NULL. Через параметр fdwFlags передается комбинация флагов, определяющих модель совместимости и дополнительные параметры инициализации:
Естественно, два первых флага являются взаимоисключающими. Возможные возвращаемые значения такие же, как и у функции CoInitialize. 5. Создание COM-объектаДля создания COM-объекта и получения указателя на требуемый его интерфейс проще всего воспользоваться функцией HRESULT CoCreateInstance( REFCLSID clsid, IUnknown* pUnk, DWORD fdwContext, REFIID iid, void** ppvDest ); Через параметр clsid передается идентификатор COM-класса, объект-экземпляр которого необходимо создать. Заметим, что для C тип REFCLSID является псевдонимом типа CLSID*, а для C++ — типа CLSID&. Параметр pUnk должен содержать указатель на интерфейс IUnknown объекта-агрегата; если агрегация не используется, этот параметр задается равным NULL. Параметр fdwContext определяет, какие виды COM-серверов необходимо задействовать, и может быть комбинацией следующих флагов:
Через параметр iid передается идентификатор интерфейса, реализуемого COM-классом, экземпляр которого создается. Функция записывает указатель на заданный интерфейс в переменную по адресу ppvDest и возвращает одно из следующих значений:
6. Завершение работы с COM-библиотекойПосле завершения работы с COM-библиотекой приложение-клиент должно освободить занятые ей системные ресурсы с помощью функции void CoUninitialize(void);
Кроме этого, перед завершением работы с COM-библиотекой (и вызовом CoUninitialize), для гарантированного удаления из памяти всех COM-серверов, использовавшихся приложением-клиентом, можно вызвать функцию void CoFreeUnusedLibraries(void);
Для сокращения расхода оперативной памяти эту функцию можно периодически вызывать в процессе работы приложения-клиента с COM-библиотекой. Интерфейс IMalloc
Система предоставляет некоторое количество «просто интерфейсов», не связанных явно ни с какими COM-классами. Одним из таких интерфейсов является IMalloc, представляющий собой системный распределитель памяти. Очень многие функции COM-библиотеки используют этот интерфейс для выделения памяти, освобождение которой возлагается на приложение-клиент. Ниже перечислены методы интерфейса IMalloc. void* Alloc( ULONG cbSize ); Выделяет cbSize байт памяти и возвращает указатель на выделенную область. void* Realloc( void* pvMem, ULONG cbNewSize ); Устанавливает размер блока памяти по адресу pvMem равным cbNewSize и возвращает указатель на выделенную область. Этот указатель может не совпадать с исходным. void Free( void* pvMem ); Освобождает блок памяти по адресу pvMem. ULONG GetSize( void* pvMem ); Возвращает размер блока памяти, расположенного по адресу pvMem (в байтах). int DidAlloc( void* pvMem ); Возвращает 1, если блок памяти по адресу pvMem был выделен с помощью IMalloc; 0, если этот блок памяти был выделен иным образом; или -1, если не распознает указанный адрес. void HeapMinimize(void);
Дефрагментирует «кучу» приложения. Получить указатель на интерфейс IMalloc можно с помощью функции HRESULT CoGetMalloc( DWORD dwContext, IMalloc** ppDest ); Параметр dwContext необходимо задавать равным 1, а параметр ppDest должен содержать адрес переменной, в которую будет записан указатель на полученный интерфейс. Данная функция может вернуть одно из следующих значений:
Прототип этой функции находится в файле <objbase.h>. Кроме того, существует аналогичная по назначению функция HRESULT SHGetMalloc( IMalloc** ppDest ); Она записывает полученный указатель по адресу ppDest и возвращает NOERROR при успешном выполнении или E_FAIL в случае ошибки. Для использования этой функции необходимо включить в исходный текст программы заголовок <shlobj.h>, а в список библиотек, используемых при компоновке приложения, добавить файл shell32.lib. Ниже приведен простой пример использования интерфейса IMalloc:
Заметим, что официальная документация утверждает, что для использования функции CoGetMalloc инициализировать COM-библиотеку не нужно, однако автор данного текста все-таки предпочитает это делать, поскольку «даже у параноиков могут быть враги». Работа со строкамиПри работе с COM-библиотекой используются два вида строк. Строки первого вида, называемые OLE strings, объявляются как завершающиеся двоичным нулем массивы символов типа OLECHAR; в Win32 они являются строками в кодировке Unicode. Определены следующие псевдонимы типов: LPOLESTR соответствует типу OLECHAR*, а LPCOLESTR — типу const OLECHAR*. Присваивание строковых литералов переменным таких типов должно выполняться с помощью макроса OLESTR(строковый_литерал) следующим образом:
Для операций с OLE-строками необходимо явным образом использовать «широкие» функции Win32 API:
Строки второго вида называются basic strings (также используется термин binary strings); вместе с символами такой строки в памяти хранится также ее длина, поэтому она может содержать любое количество двоичных нулей. Формальный тип таких строк для C/C++ называется BSTR; в Win32 они, как и OLE-строки, состоят из символов в кодировке Unicode. Ниже приведены функции, работающие с basic strings. BSTR SysAllocString( LPCOLESTR pstrSrc ); Создает и возвращает копию строки pstrSrc. BSTR SysAllocStringLen( LPCOLESTR pstrSrc, unsigned int cch ); Создает и возвращает строку, являющуюся копией первых cch символов строки pstrSrc. BOOL SysReAllocString( BSTR* pbstrDest, LPCOLESTR pstrSrc ); Замещает строку по адресу pbstrDest копией строки pstrSrc и возвращает ненулевое значение при успешном выполнении. Пример использования:
BOOL SysReAllocStringLen( BSTR* pbstrDest, LPCOLESTR pstrSrc, unsigned int cch ); Замещает строку по адресу pbstrDest копией первых cch символов строки pstrSrc и возвращает ненулевое значение при успешном выполнении. void SysFreeString( BSTR bstrSrc ); Удаляет строку bstrSrc, освобождая занимаемую ей память. UINT SysStringLen( BSTR bstrSrc ); Возвращает длину строки bstrSrc. UINT SysStringByteLen( BSTR bstrSrc ); Возвращает размер строки bstrSrc в байтах. Для применения перечисленных функций необходимо использовать заголовочный файл <oleauto.h> и библиотеку импорта oleaut32.lib. | ||||||||||