Основные понятия

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). Рассмотрим небольшой пример:

// первый интерфейс
struct IFoo
{
   virtual void method_one(void) = 0;
   virtual void method_two(void) = 0;
};

// второй интерфейс
struct IGoo
{
   virtual void method_three(void) = 0;
};

// COM–класс
class CoMyClass : IFoo, IGoo
{
   // методы первого интерфейса
   virtual void method_one(void);
   virtual void method_two(void);
   // методы второго интерфейса
   virtual void method_three(void);
};

// реализация
void CoMyClass::method_one(void)
{
   // делаем что-нибудь полезное
}

Каждый интерфейс должен порождаться от интерфейса с именем IUnknown; следовательно, каждый COM-класс будет содержать, как минимум, реализацию методов этого интерфейса. При использовании множественного наследования интерфейсы класса могут совместно использовать методы IUnknown, а при использовании вложенных классов эти методы необходимо реализовывать для каждого интерфейса.

Интерфейс IUnknown

Заголовочный файл #include <unknwn.h>
Непосредственный предок

Интерфейс 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:

IFoo* pFoo = NULL;
...   // попытались получить указатель на интерфейс
if (pFoo != NULL)
{
   ...   // повызывали методы интерфейса
   pFoo->Release();
}

Если приложение-клиент создает копию ранее полученного указателя на интерфейс, то оно должно самостоятельно вызвать для этой копии метод AddRef:

IFoo* pFoo = NULL;
...   // попытались получить указатель на интерфейс
if (pFoo != NULL)
{
   IFoo* pFooCopy = pFoo;
   pFooCopy->AddRef();
   ...   // повызывали методы копии
   pFooCopy->Release();
   pFoo->Release();
}

Заметим, что то же самое необходимо делать и при передаче указателя на интерфейс как параметра в функцию, поскольку в этом случае на стеке неявно создается копия этого указателя:

void use_foo(IFoo* pFoo)
{
   pFoo->AddRef();
   ...   // повызывали методы интерфейса
   pFoo->Release();
}

Разработка приложения-клиента

Рассмотрим основные шаги, которые необходимо предпринять при разработке приложения, использующего технологию COM.

1. Заголовочный файл

Заголовочный файл #include <objbase.h>

Указанный заголовочный файл содержит основные объявления, необходимые при работе с COM-библиотекой, и является для COM-приложений тем же, чем <windows.h> — для обычных. Кроме того, он включает другие заголовки, в частности, <unknwn.h> и <objidl.h>.

2. Библиотека импорта

В список библиотек, используемых при компоновке приложения, необходимо добавить библиотеку импорта ole32.lib; для этого в IDE Visual C++ 6.0 необходимо в меню Project выбрать команду Settings, в появившемся диалоговом окне перейти на вкладку Link, выбрать категорию General и вписать указанное имя в поле ввода Object/library modules. Альтернативным способом является использование в исходном тексте приложения директивы

#pragma comment(lib, "ole32.lib")

которая распознается всеми последними версиями компиляторов C/C++ от Microsoft и Borland.

3. Выбор модели параллелизма (concurrency model)

Архитектура COM предусматривает использование COM-объектов в двух моделях параллелизма (автору не известен менее шершавый перевод термина concurrency model):

single-threaded apartment (STA),
иначе называемая apartment-threaded. В этом случае приложение может быть многопоточным, но методы каждого интерфейса должны вызываться только из того потока, в котором был получен указатель на этот интерфейс.
multithreaded apartment (MTA),
иначе называемая free-threaded. Методы интерфейса могут вызываться из разных потоков, но соответствующий COM-класс обязан при этом заботиться о синхронизации доступа к ресурсам, используя для этого критические секции, семафоры, etc.

Требуемая модель задается при инициализации COM-библиотеки (так часто называют ole32.dll, являющуюся ядром COM).

4. Инициализация COM-библиотеки

Перед какими-либо обращениями к COM-библиотеке приложение-клиент должно ее инициализировать. Это можно сделать с помощью одной из двух следующих функций:

HRESULT CoInitialize(
   void* pvReserved
);

Данная функция инициализирует COM-библиотеку для вызывающего потока в модели STA. Параметр pvReserved необходимо задавать равным NULL. Возвращается одно из следующих значений:

S_OK
инициализация прошла успешно;
S_FALSE
COM-библиотека уже инициализирована;
RPC_E_CHANGED_MODE
COM-библиотека уже инициализирована в модели MTA, изменение модели невозможно.
HRESULT CoInitializeEx(
   void* pvReserved,
   DWORD fdwFlags
);

Как и в предыдущем случае, параметр pvReserved необходимо задавать равным NULL. Через параметр fdwFlags передается комбинация флагов, определяющих модель совместимости и дополнительные параметры инициализации:

COINIT_APARTMENTTHREADED
инициализация в модели STA;
COINIT_MULTITHREADED
инициализация в модели MTA;
COINIT_DISABLE_OLE1DDE
не использовать DDE для поддержки протокола OLE1;
COINIT_SPEED_OVER_MEMORY
расходовать больше памяти ради увеличения скорости работы.

Естественно, два первых флага являются взаимоисключающими. Возможные возвращаемые значения такие же, как и у функции 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-серверов необходимо задействовать, и может быть комбинацией следующих флагов:

CLSCTX_INPROC_SERVER
внутрипроцессный сервер (реализуется как DLL-библиотека, загружающаяся в адресное пространство приложения-клиента);
CLSCTX_LOCAL_SERVER
автономный сервер (реализуется как Windows-приложение), находящийся на том же компьютере, что и приложение-клиент;
CLSCTX_REMOTE_SERVER
удаленный сервер (автономный сервер, находящийся на другом компьютере).

Через параметр iid передается идентификатор интерфейса, реализуемого COM-классом, экземпляр которого создается. Функция записывает указатель на заданный интерфейс в переменную по адресу ppvDest и возвращает одно из следующих значений:

S_OK
успешное выполнение;
REGDB_E_CLASSNOTREG
класс с идентификатором clsid не зарегистрирован в системном реестре;
E_NOINTERFACE
класс с идентификатором clsid не реализует интерфейс с идентификатором iid;
CLASS_E_NOAGGREGATION
класс с идентификатором clsid не поддерживает агрегацию.

6. Завершение работы с COM-библиотекой

После завершения работы с COM-библиотекой приложение-клиент должно освободить занятые ей системные ресурсы с помощью функции

void CoUninitialize(void);

Кроме этого, перед завершением работы с COM-библиотекой (и вызовом CoUninitialize), для гарантированного удаления из памяти всех COM-серверов, использовавшихся приложением-клиентом, можно вызвать функцию

void CoFreeUnusedLibraries(void);

Для сокращения расхода оперативной памяти эту функцию можно периодически вызывать в процессе работы приложения-клиента с COM-библиотекой.

Интерфейс IMalloc

Заголовочный файл #include <objidl.h>
Непосредственный предок интерфейс IUnknown

Система предоставляет некоторое количество «просто интерфейсов», не связанных явно ни с какими 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 должен содержать адрес переменной, в которую будет записан указатель на полученный интерфейс. Данная функция может вернуть одно из следующих значений:

S_OK
указатель успешно получен;
E_OUTOFMEMORY
для выполнения требуемой операции недостаточно памяти;
E_INVALIDARG
при вызове задано недопустимое значение аргумента.

Прототип этой функции находится в файле <objbase.h>. Кроме того, существует аналогичная по назначению функция

HRESULT SHGetMalloc(
   IMalloc** ppDest
);

Она записывает полученный указатель по адресу ppDest и возвращает NOERROR при успешном выполнении или E_FAIL в случае ошибки. Для использования этой функции необходимо включить в исходный текст программы заголовок <shlobj.h>, а в список библиотек, используемых при компоновке приложения, добавить файл shell32.lib.

Ниже приведен простой пример использования интерфейса IMalloc:

// стандартные заголовочные файлы
#include <windows.h>
#include <objbase.h>
#include <shlobj.h>

// библиотеки импорта
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "shell32.lib")

IMalloc* pMalloc = NULL;

// инициализируем COM-библиотеку
::CoInitialize(NULL);

// пытаемся получить указатель
if (::SHGetMalloc(&pMalloc) == NOERROR)
{
   // выделяем память для массива из четырех переменных типа int
   int* pn = (int*)pMalloc->Alloc(sizeof(int) * 4);

   // присваиваем элементам массива одинаковые значения
   pn[0] = pn[1] = pn[2] = pn[3] = 1972;

   // освобождаем память, занятую массивом
   pMalloc->Free(pn);

   // интерфейс больше не нужен
   pMalloc->Release();
}

// уходим
::CoFreeUnusedLibraries();
::CoUninitialize();

Заметим, что официальная документация утверждает, что для использования функции CoGetMalloc инициализировать COM-библиотеку не нужно, однако автор данного текста все-таки предпочитает это делать, поскольку «даже у параноиков могут быть враги».

Работа со строками

При работе с COM-библиотекой используются два вида строк.

Строки первого вида, называемые OLE strings, объявляются как завершающиеся двоичным нулем массивы символов типа OLECHAR; в Win32 они являются строками в кодировке Unicode. Определены следующие псевдонимы типов: LPOLESTR соответствует типу OLECHAR*, а LPCOLESTR — типу const OLECHAR*. Присваивание строковых литералов переменным таких типов должно выполняться с помощью макроса

OLESTR(строковый_литерал)

следующим образом:

OLECHAR* pstrHello = OLESTR("Hello!");

Для операций с OLE-строками необходимо явным образом использовать «широкие» функции Win32 API:

OLECHAR* pstrCopy = new OLECHAR[16];
::lstrcpyW(pstrCopy, OLESTR("Hello!"));

Строки второго вида называются 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 и возвращает ненулевое значение при успешном выполнении. Пример использования:

BSTR bstrTest = ::SysAllocString(OLESTR("Hello!"));
::SysReAllocString(&bstrTest, OLESTR("Good bye..."));
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.

обновлено
29.03.2006
 
Проверка PR и ТИЦ