Глава 6
HRESULL GUID, Реестр и другие детали

Дух братьев Райт все еще жив. Каждый год сотни людей в своих гаражах строят самолеты из наборов «Сделай сам». Они делают не пластиковые игрушки, радиоуправляемые модели или легковесные матерчатые конструкции. Они строят современные двухместные самолеты с полностью закрытой кабиной из современнейших композитных материалов. По правилам Федерального авиационного агентства достаточно, чтобы производитель набора выполнил только 49% всей работы по постройке самолета. Оставшийся 51% конструктор-любитель делает сам.

Постройка 51% самолета занимает, в зависимости от модели, примерно от 250 до 5000 часов. Большинство производителей предлагают наборы «для быстрого приготовления», в которых многие части уже собраны, например, сварены рамы и пропитаны детали из композитных материалов. Используя такие заготовки, можно быстро сделать нечто, похожее на самолет. Однако это будет еще далеко не настоящий самолет. Куча времени уходит на разные мелочи — установку панели управления и приборов, сидений, ремней безопасности, огнетушителей, покрытия, сигнализации, табличек, кабелей управления, электропроводки, лампочек, батарей, брандмауэров, вентиляционных люков, крыши'пилотской кабины, отопителя, окон, замков и ручек на дверях и еще многого другого.

Многие энтузиасты самолетостроения приходят в уныние, разочаровываются и даже бросают это занятие, потратив на сборку многие часы. Точно так же многие начинают изучать сложный предмет, например СОМ, лишь затем, чтобы захлебнуться в деталях и все бросить. В первых пяти главах книги я пытался максимально избегать подробностей, чтобы Вы сосредочились на общей картине. В этой главе я собираюсь обсудить некоторые из тех деталей, которые раньше пропускал или скрывал. Я хочу рассмотреть и другие детали, которые понадобятся нам в следующих главах.

Сначала мы обсудим HRESULT — тему, впервые возникшую в гл. 3 в связи с QueryInterface. Затем мы рассмотрим GUID. Один из примеров GUID - структура IID, передаваемая QueryInterface. После обсуждения этих типов мы познакомимся с тем, как компоненты публикуют в Реестре данные о своем местонахождении (это позволяет клиентам находить и создавать компоненты). В заключение мы рассмотрим некоторые полезные функции и утилиты библиотеки СОМ.

HRESULT

Во всех самолетах есть приборы, и самодельные самолеты — не исключение. Хотя в некоторых таких самолетах роль приборов играет компьютер с цветным графическим дисплеем (иногда даже «под» Windows NT), обычно ставят что-нибудь подешевле. Металлическая полоса, например, - простейший индикатор скорости. Чем быстрее Вы летите, тем сильнее она изгибается.

Хотя приборы и могут сообщить в деталях, что происходит с самолетом и отдельными системами, их основное назначение — предупреждать об опасности. На индикаторе скорости есть красная полоска, отмечающая слишком высокую (или низкую) скорость. Часто приборы снабжают аварийными лампочками и зуммерами.

У компонентов СОМ нет приборов. Вместо шкал или лампочек для сообщений о текущем состоянии дел они используют HRESULT. QueryInterface вращает HRESULT. И, как мы увидим в оставшейся части книги, большинство функций интерфейсов СОМ также возвращает HRESULT. Xoтя из названия HRESULT можно было бы заключить, что это описатель (handle) результата, на самом деле это не так. HRESULT — это 32-разрядное значение, разделенное на три поля. Значения полей, составляющих HRESULT поясняет рис. 6-1. Название возникло по историческим причинам; расшифровывайте его как «вот результат» (here's the, result), а не «описатель результата» (handle of result).

Определенные системой значения HRESULT содержатся в заголовочном файле Win32 WINERROR.H. В начале файла расположены коды ошибок Win32, так что его нужно пролистать, чтобы добраться до HRESULT похож на код ошибки Win32, но это не одно и то же, и смешивать их не следует.

Старший бит HRESULT, как показано на рис. 6-1, отмечает, успешно или нет выполнена функция. Это позволяет определить много кодов возврата и для успеха, и для неудачи. Последние 16 битов содержат собственно код возврата. Остальные 15 битов содержат дополнительную информацию о типе и источнике кода ошибки.


РИС. 6-1. Формат HRESULT.

В табл. 6-1 приведены наиболее часто используемые коды. По соглашению в названиях успешных кодов возврата содержится S_, а в названиях кодов ошибок — Е_.

Таблица 6-1. Распространенные значения HRESULT


НазваниеЗначение
S_OKФункция отработала успешно. В некоторых случаях этот код также означает, что функция возвращает логическую истину. Значение S_OK равно 0.
NOERRORТоже,что S_ОК.
S_FALSEФункция отработала успешно и возвращает логическую ложь. Значение S_FALSE равно 1.
E_UNEXPECTEDНеожиданная ошибка.
E_NOTIMPLМетод не реализован.
E_NOINTERFACEКомпонент не поддерживает запрашиваемый интерфейс. Возвращается QueryInterface.
E_OUTOFMEMORYКомпонент не может выделить требуемый объем памяти.
E_FAILОшибка по неуказанной причине.

Обратите внимание, что значение S_FALSE равно 1, а значение S_OK — 0. Это противоречит обычной практике программирования на C/C++, где 0 - это ложь, а не 0 — истина. Поэтому при использовании HRESULT обязательно явно сравнивайте коды возврата с S_FALSE или S_OK.

Пятнадцать битов — с 30-го по 16-й — содержат идентификатор средства (facility). Он указывает, какая часть операционной системы выдает данный код возврата. Поскольку операционную систему разрабатывает Microsoft, она зарезервировала право определения идентификаторов средств за собой. Идентификаторы средств, определенные в настоящее время, приведены в табл. 6-2.

Таблица 6-2. Идентификаторы средств, определенные в настоящее время


FACILITY_WINDOWS

8

FACILITY_STORAGE

3

FACILITY_SSPI

9

FACILITY_RPC

1

FACILITY_Win32

7

FACILITY_CONTROL

10

FACILITY_NULL

0

FACILITY_ITF

4

EACILITY_DISPATCH

2

FACILITY_CERT

11


Идентификатор средства освобождает, например, разработчиков Microsoft, занятых RPC (FACILITY_RPC), от необходимости согласовывать значения кодов возврата с теми, кто работает над управляющими элементами ActiveX (FACILITY_CONTROL). Поскольку группы разработчиков используют разные идентификаторы средств, коды возврата разных средств не будут конфликтовать. Разработчикам специализированных интерфейсов повезло меньше.

Все идентификаторы средств, кроме FACILITY_ITF, задают определенные СОМ универсальные коды возврата. Эти коды всегда и везде одни и те же. FACILITY_ITF — исключение; ему отвечают коды, специфичные для данного интерфейса. Чтобы определить средство для данного HRESULT, используйте макрос HRESULT_FACILITY, определенный в WINERROR.H. Как Вы увидите в разделе «Определение собственных кодов возврата», коды FACILITY_ITF не уникальны и могут иметь разные значения в зависимости от интерфейса, возвратившего код. Но прежде чем определять собственные коды, давайте рассмотрим использование HRESULT.

Поиск HRESULT

Как уже отмечалось, определение всех кодов состояния СОМ (и OLE - точнее, уже ActiveX), генерируемых системой в настоящее время, содержится в WINERROR.H. Обычно коды заданы как шестнадцатеричные числа; запись для E_NOINTERFACE выглядит так:

// MessageId: E_NOTINTERFACE
//
// MessageText:
// Данный интерфейс не поддерживается*
//
#define E_NOINTERFACE          0х800040021L
Однако если идентификатор средства HRESULT равен FACILITY_WIN32, Вы можете не найти его среди других. Часто это будет код ошибки Win32, преобразованный в HRESULT. Чтобы найти его значение, отыщите код ошибки Win32, совпадающий с последними 16 битами. Пусть, например, интерфейс возвращает код ошибки 0х80070103. Число 7 в середине — это идентификатор средства FACILITY_WIN32. В файле WINERROR.H Вы не найдете этот код там, где перечислены другие HRESULT. Поэтому переведите последние 16 битов из шестнадцатеричного представления в двоичное; получится число 259, которое уже можно найти в списке кодов Win32.
// MessageId: ERROR_NO_MORE_ITEMS
//
// Больше элементов нет.
//
#define ERROR_NO_MORE_ITEMS           259L
//

Искать HRESULT в WINERROR.H вполне допустимо, когда мы пишем код. Однако нашим программам необходим способ получить сообщение об ошибке, соответствующее данному HRESULT, и отобразить его пользователю. Для отображения сообщений о стандартных ошибках СОМ (а также ActiveX, ранее OLE, и Win32) можно использовать API Win32 FormatMessage:

void ErrorMessage(LPCTSTR str, HRESULT hr)
{
    void* pMsgBuf ;
    ::FormatMessage(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
		NULL,
		hr,
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPTSTR)&pMsgBuf,
		0,
		NULL);
    // Отобразить строку.
    cout « str « "\r\n"
    cout « "Error (" « hex « hr « "): "
      « (LPTSTR)pMsgBuf « endl ;

    // Освободить буфер.
    LocalFree(pMsgBuf);
}

Использование HRESULT

Как видите, использовать HRESULT несколько сложнее, чем обычные булевы коды возврата. Среди потенциально чреватых осложнениями ocoбенностей можно назвать:
Множественность кодов завершения
Как правило, в зависимости от обстоятельств функции возвращают различные коды как успешного, так и ошибочного завершения. Именно поэтому мы использовали SUCCEEDED и FAILED. Для проверки успешного завершения нельзя сравнивать HRESULT с каким-либо одним кодом, например S_OK; равно как и для проверки неудачного завершения HRESULT нельзя сравнивать с каким-то одним кодом, например E_FAIL. Иными словами, нельзя писать:
HRESULT hr = CreateInstance(...);
if (hr == E_FAIL) // He делайте так!
    return;
hr = pl->QueryInterface(...);
if (hr == S_OK) // He делайте так!
{
    pIX->Fx();
    pIX->Release();
}
pI->Release();

Вместо этого надо использовать макросы SUCCEEDED и FAILED.

HRESULT hr = CreateInstance(...);
if (FAILED(hr))
    return;
if (SUCCEEDED(hr))
{
    pIX->Fx();
    pIX->Release();
}
pI->Release();
Коды ошибок могут изменяться
После того, как Ваш клиент закончен, другие разработчики могут определить новые коды ошибок для HRESULT, с которыми столкнется клиент. Поскольку компоненты, используемые клиентом, могут меняться, могут изменяться и возвращаемые ими коды ошибок. Предположим, мы пишем компонент как сервер внутри процесса. Некоторое время спустя мы решили модернизировать его, сделав удаленным сервером на другой машине. Первая версия компонента не возвращала никаких кодов ошибок сети, тогда как вторая версия может это делать. Клиент не может заранее знать обо всех возможных ошибках, поэтому он должен быть готов к обработке неожиданных ошибок. Обрабатывайте все неожиданные ошибки так же, как E_UNEXPECTED.

С кодами успешного окончания этой проблемы нет. Набор этих кодов для Вашей функции должен быть статичным. Коды успешного завершения — часть интерфейса и поэтому не могут изменяться. Клиент, использующий интерфейс, должен быть способен понять, предсказать и обработать все возможные случаи успешного завершения, поскольку ему надо будет продолжать работу. Клиенту нет необходимости обрабатывать все возможные коды ошибок — он не обязан продолжать работу, если ему встретился неожиданный код.
HRESULT и сеть
Часто связь с удаленной машиной по сети неожиданно прерывается. Если клиент работает с удаленным компонентом, он должен уметь элегантно обрабатывать разрыв сетевого соединения. Это означает, что у каждого вызова функции, который может выполняться по сети, должен быть некий способ индикации разрыва связи. По этой причине все методы, которые могут выполняться на удаленной машине, должны возвращать HRESULT. Избегайте других типов возвращаемого значения,например:
double GetCordLenght(double BladeSection);
Вместо этого возвращайте из функции HRESULT, а все результаты передавайте через выходные параметры:
HRESULT GetCordLength(/* in */ double BladeSection, /* out */ double* pLength);
HRESULT передает клиенту информацию, необходимую для обнаружения сетевых ошибок. Вызовы функций в Автоматизации (ранее OLE Автоматизация) удовлетворяют этому требованию. Более подробно удаленные компоненты будут рассмотрены в гл. 10.

Определение собственных кодов ошибки

СОМ определяет универсальные коды возврата, такие как S_OK и E_UNEXPECTED. Разработчики интерфейсов ответственны за коды возврата, специфичные для их интерфейсов. HRESULT, содержащий специфичный для интерфейса код возврата, должен также содержать идентификатор средства FACILITY_ITF. Он указывает клиенту, что код специфичен для данного интерфейса.

Хотя смысл кода возврата, отмеченного с помощью FACILITY_ITF, специфичен для возвращающего его интерфейса, само по себе соответствующее число не уникально — возможны только 216 разных значений. Тысячи разработчиков пишут свои компоненты СОМ со своими кодами возврата. Все такие коды помечены с помощью FACILITY_ITF. Кроме того, много интерфейсов определено ActiveX (OLE) — и все такие интерфейсы определяют коды возврата, тоже отмеченные FACILITY_ITE Поэтому не просто с очень большой вероятностью, но и с гарантией разные интерфейсы придадут разный смысл одним и тем же кодам возврата. Тридцати двух разрядов недостаточно, чтобы дать каждому разработчику ввести свой собственный идентификатор средства, а большая длина HRESULT снизила бы эффективность. В качестве кодов возврата GUID не являются разумной альтернативой длинным целым значениям, поскольку размер GUID слишком велик. Однако, поскольку FACILITY_ITF отмечает каждый код возврата как специфичный для интерфейса, постольку такой код возврата связан с идентификатором интерфейса (IID).

Для клиента, вызывающего функции интерфейса, возможность конфликта кодов возврата — не проблема. Клиент знает, к кому он обращается, и, таким образом, знает все коды успеха для данного интерфейса. Клиенту также известна большая часть кодов ошибок. Клиент должен рассматривать любой неизвестный ему код ошибки как E_UNEXPECTED. Однако проблемы начинаются, когда клиент интерфейса сам является компонентом, который пытается без изменения передать возвращаемый код успеха или ошибки своему клиенту. Последний не поймет этот код, поскольку не знает, к какому первоначальному интерфейсу тот относится.

Например, предположим, что первый клиент вызывает функцию IX::Fx, которая затем вызывает IY::Fy. Если IY::Fy возвращает HRESULT с FAGILITY_ITF, то IX::Fx не может передать этот код первому клиенту. Данный клиент знает только о IX - и будет полагать, что HRESULT относится к IX, а не IY. Следовательно, IX::Fx должна транслировать возвращаемые IУ значения HRESULT с FACILITY_ITF в такие значения, которые понятны первому клиенту. Для неизвестных ошибок у IХ нет иного выбора, кроме как возвращать E_UNEXPECTED. Для кодов успешного завершения IX должен возвращать свои собственные, документированные коды возврата.

Вот некоторые основные правила определения собственных HRESULT:

Теперь, когда Вы получили некоторое представление о HRESULT, создадим полный код при помощи макроса MAKE_HRESULT. По заданному признаку критичности, идентификатору средства и коду завершения MAKE_HRESULT создает HRESULT. Вот два примера:
    MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 512);
    MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_ITF, 513);
По соглашению нестандартным кодам завершения дается к качестве префикса имя компонента или интерфейса. Например, двум приведенным выше кодам можно было бы дать имена
AIRPLANE_E_LANDINGWITHGEARUP
HELICOPTER_S_ROTORRPMGREEN
Сказанного о HRESULT более чем достаточно. Теперь пора снять завесу таинственности с GUID.

GUID

В США всем обычным летательным аппаратам ФАА присваивает N-номер (N-number), который идентифицирует самолет, — как номерной знак идентифицирует Вашу машину. Этот номер уникален для каждого самолета и пользуется пилотом в переговорах с авиадиспетчерами. В этом разделе мы обсудим GUID, которые являются такими «опознавательными знаками» компонентов и интерфейсов.

В гл. 3 я предложил Вам представлять себе IID как константу, идентифицирующую данный интерфейс. Однако, как Вы могли видеть из определения IID_IX, IID — это константа особого рода:

static const IID IID_IX = 
{0x32bb8320, 0xb41b, 0x11cf,
{0ха6, Oxbb; 0х0, 0х80, Oxc7, Oxb2, Oxd6, 0х82}} ;
На самом деле IID представляет собой тип, определенный как структура длиной 128 битов (16 байтов) под названием GUID. GUID - аббревиатура и Globally Unique IDentifier (глобально уникальный идентификатор; произносится как «гуид» - как первая часть в слове geoduck' и последняя - в druid).

Зачем нужен GUID?

Почему мы используем GUID, a не длинное целое (long integer)? С помощью длинных целых можно однозначно задать 232 интерфейсов. Я сильно сомневаюсь, что большее их число когда-либо понадобится. Однако настоящая проблема не в том, сколь много интерфейсов мы сможем однозначно задать, но в том, как гарантировать уникальность идентификатора интерфейса. Если два идентификатора совпадают, клиент легко может получить от QueryInterface неверный указатель на интерфейс. Проблема усложняется тем, что компоненты создаются разработчиками по всему земному шару. Если Сара в Ако и Линн в Таксоне разрабатывают новые интерфейсы СОМ, то как им удостовериться, что идентификаторы интерфейсов не будут конфликтовать? Можно было бы договориться о чем-нибудь вроде N-номеров летательных аппаратов и учредить некое центральное агентство, наподобие FAA, для выделения идентификаторов. Централизованная организация подходит для относительно ограниченного числа летательных аппаратов; но я сомневаюсь, что какое-нибудь агентство смогло бы столь же успешно, как FAA, работать с тем количеством интерфейсов, которое необходимо для средней программы.

Для GUID есть более удачное решение. Уникальный GUID можно сгенерировать программно, без какой-либо координирующей организации. Microsoft Visual C++ предоставляет для генерации GUID две программы - утилиту командной строки UUIDGEN.EXE и диалоговую программу на VC++, GUIDGEN.EXE. Если я сейчас запущу UUIDGEN.EXE, то получу строку, представляющую некоторый GUID:

{166769E1-88E8-11CF-A6BB-0080C7B2D692}
При всяком новом запуске UUIDGEN получается иной GUID. Если Вы запустите UUIDGEN на своей машине, то получите GUID, отличный от моего. Если миллионы (я надеюсь) людей, читающих эту книгу, сейчас запустят UUIDGEN, они получат миллион разных GUID.

Исходный текст GUIDGEN.EXE можно найти в примерах программ Microsoft Visual C++. Но я и так могу сказать Вам, как работает эта программа: она просто вызывает функцию библиотеки СОМ Microsoft CoCreateGuid, которая вызывает функцию RPC UuidCreate.
Теория GUID
GUID по определению уникален «в пространстве и во времени». Для обеспечения «географической» уникальности каждый GUID использует 48-битовое значение, уникальное для компьютера, на котором он генерируется. Обычно в качестве такого значения берется адрес сетевой платы. Такой подход гарантирует, что любой GUID, полученный на моем компьютере, будет отличаться от любого, сгенерированного на Вашем компьютере. Для тех компьютеров, в которых не установлен сетевой адаптер, используется другой алгоритм генерации уникальных значений. В каждом GUID 60 битов отведено для указания времени. Туда заносится число 100-наносекундных интервалов, прошедших с 00:00:00:00 15 октября 1582 года. Используемый в настоящее время алгоритм генерации GUID начнет выдавать повторяющиеся значения примерно в 3400 году. (Я подозреваю, что очень немногие из нынешних программ, за исключением некоторых на Фортране, еще будут использоваться в 3400 году; но я верю, что к этому времени уже выйдет Windows 2000.)

GUID придумали толковые ребята из Open Software Foundation. (OSF); правда, они использовали термин UUID (Universally Unique IDentifiers — вселенски уникальные идентификаторы). UUID разработали для использования в среде распределенных вычислений (DCE, Distributed Computing Environment). Вызовы удаленных процедур (RPC) DCE используют UUID для идентификации вызываемого, т. е. практически затем же, зачем и мы.

Дополнительно о генерации UUID или GUID можно прочитать в САЕ Specification X/Open DCE: Remote Procedure Call.

Объявление и определение GUID

Поскольку размер GUID велик (128 битов), не хотелось бы, чтобы они повторялись в нашем коде повсюду. В гл. 5 GUID определялись в файле GUIDS.CPP примерно так:
// {32bb8320-b41b-11cf-a6bb-0080c7b2d682}
extern "С" const IID IID_IX =
    {0x32bb8320, 0xb41b, Ox11cf,
    {0xa6, 0xbb, 0х0, 0х80, Oxc7, Oxb2, Oxd6, 0х82}} ;
Объявлены они были в файле IFACE.H так:
extern "С" const IID IID_IX;
Вести для GUID два файла, один с определениями, а другой с объявлениями — изрядная морока. Чтобы определить и объявить GUID одним оператором, используйте макрос DEFINE_GUID, который определен в OBJBASE.H. Для использования DEFINE_GUID генерируйте GUID с помощью GUIDGEN.EXE. Эта программа генерирует GUID в различных форматах — выберите второй из них. Этот формат используется в следующем примере.
// {32bb8320-b41b-11cf-a6bb-0080c7b2d682}
DEFINE_GUID(«name»,
{0x32bb8320, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0х0, 0х80, 0xc7, 0xb2, 0xd6, 0х82}} ;
Вставьте сгенерированный GUID в заголовочный файл. Замените «name» идентификатором, используемым в Вашем коде, — например, IID_IX:
{32bb8320-b41b-11cf-a6bb-0080c7b2d682}
DEFINE_GUID(IIO_IX,
    {0x32bb8320, 0xb41b, 0x11cf,
    {0xa6, 0xbb, 0х0, 0х80, 0xc7, 0xb2, 0xd6, 0х82}};
В соответствии с определением в OBJBASE, DEFINE_GUID генерирует что-то вроде:
extern "С" const GUID IID_IX;
Однако, если после OBJBASE.H включить заголовочный файл INITGUID.H, макрос DEFINE_GUID будет раскрываться так:
extern "С" const IID IID_IX =
    {0x32bb8320, 0xb41b, 0x11cf,
    {0xa6, 0xbb, 0х0, 0х80, 0xc7, 0xb2, 0xd6, 0х82}} ;
Механизм работы этих заголовочных файлов представлен на рис. 6-2. Заголовок IFACE.H использует макрос DEFINE_GUID для объявления IID_IX. Идентификатор IID_IX определен в файле GUIDS.H. Он определен там потому, что заголовочный файл INITGUID.H включен после OBJBASE.H и перед IFACE.H. С другой стороны, в файле CMPNT.CPP IID_IX объявлен, но не определен, поскольку заголовочный файл INITGUID.H здесь не включен.

Так как я старался сделать примеры в книге максимально ясными, то DEFINE_GUID я в них не использовал, но явно определял и объявлял используемые GUID.


РИС. 6-2. Включение INITGUID.H заставляет макрос DEFINE_GUID определить GUID.

Сравнение GUID

Для сравнения GUID в OBJBASE.H .определен operator ==:
inline BOOL operator ==(const GUID& guid1, const GUID& guid2)
{
    return !memcnip(&guid1, &guid2, sizeof(GUID));
}
Нам уже приходилось использовать эту операцию в QueryInterface. Если Вы не любите упрятывать истинный код во внешне простенькие операторы, OBJBASE.H дает определения эквивалентных по смыслу функций IsEqualGUID, IsEqualIID и IsEqualCLSID.

Использование GUID в качестве идентификаторов компонентов

Помимо уникальной идентификации интерфейсов, GUID используются и для уникальной идентификации компонентов. В гл. 5 мы определили для создания компонентов функцию CallCreateInstance. Параметром этой функции служит строка с именем DLL, в которой содержится компонент:
    IUnknown* CallCreateInstance(char* name) ;
В следующей главе мы заменим эту функцию на функцию библиотеки СОМ CoCreateInstance. Последняя использует для идентификации компонента не строку, a GUID. Такой GUID в СОМ называется идентификатором класса. Чтобы отличать идентификаторы классов от IID, для них используют тип CLSID.

Подобно интерфейсам, все компоненты имеют различные идентификаторы. Два компонента могут реализовывать в точности одинаковые наборы интерфейсов, но у них по-прежнему должны быть свои CLSID. Компоненты могут добавлять новые интерфейсы, не изменяя CLSID. Однако если изменение компонента отражается на работе какого-нибудь приложения, компоненту надо дать новый CLSID.

Передача GUID по ссылке

Поскольку размер GUID 16 байтов, мы будем передавать их не по значению, а по ссылке. Именно поэтому параметром QueryInterface является ссылка на константу. Если для Вас утомительно все время писать
const IID&
можете использовать эквивалентное выражение REFIID. Точно так же для передачи идентификаторов классов можно использовать REFCLSID, а для передачи GUID - REFGUID.

Теперь давайте рассмотрим, как компоненты регистрируются в системе (чтобы клиенты смогли их найти и использовать),

Реестр Windows

FAA ведет реестр всех летательных аппаратов, включая самодельные. По этому реестру можно определить, кто хозяин самолета. В этой главе мы рассмотрим чем-то похожий реестр, позволяющий определить, какой DLL принадлежит данный компонент.

В гл. 5 при создании компонента мы передавали функции CallCreateInstance имя файла соответствующей DLL. В следующей главе мы собираемся заменить CallCreateInstance функцией библиотеки СОМ CoCreateInstance. Для идентификации компонента CoCreateInstance вместо имени файла использует CLSID (по нему определяется имя файла DLL). Компоненты помещают имена своих файлов, индексированные CLSID, в Реестр Windows. CoCreateInstance отыскивает имя файла, используя CLSID как ключ.

В реальной жизни реестр — это учетная книга для записи предметов, имен или действий. В Windows Реестр — это общедоступная база данных операционной системы. Реестр содержит информацию об аппаратном и программном обеспечении, о конфигурации компьютера и о пользователях. Любая программа для Windows может добавлять и считывать информацию из Реестра; клиенты могут искать там нужные компоненты. Но прежде чем поместить свою информацию в Реестр, надо узнать, как он устроен.

Организация Реестра

Рестр имеет иерархическую структуру. Каждый ее элемент называется разделом, (key)- Раздел может включать в себя набор подразделов, набор именопанных параметров и/или один безымянный параметр — параметр по умолчанию (default value). Подразделы, но не параметры, могут содержать другие подразделы и параметры. Параметры могут быть разного типа, но чаще всего мы будем записывать в Реестр строки. Структура Реестра показана на рис. 6-3.


РИС. 6-3. Структура Реестра Windows.

Редактор Реестра

Реестр содержит очень много информации. По счастью, нас интересует лишь малое подмножество. Лучше всего изучать Реестр, запустив Редактор Реестра — Windows-программу, позволяющую просматривать и редактировать записи. Эта программа называется REGEDT32.EXE в Windows NT и REGEDITEXE в Windows 95*. Одно предостережение: редактируя Реестр, чрезвычайно легко повредить систему, так что будьте осторожны.

Необходимый минимум

СОМ использует только одну ветвь дерева данных Реестра: HKEY_CLASSES_ROOT. Ниже HKEY_CLASSES_ROOT отыщите раздел CLSID. В этом разделе перечислены CLSID всех компонентов, установленных в системе. CLSID хранится в Реестре как строка формата {хххххххх-хххх-хххх-хххх-хххххххххххх}. Искать CLSID в Реестре — занятие не слишком привлекательное. Поэтому в каждом разделе CLSID параметр по умолчанию задает «дружественное» имя компонента.

Пока в разделе каждого CLSID нас интересует только один подраздел - InprocServer32. Его параметр по умолчанию — имя файла DLL. Название InprocServer32 используется потому, что DLL — это сервер в процессе (in-proc); она загружается в процесс клиента и предоставляет ему сервисы. На рис. 6-4 показан пример ветви CLSID Реестра.

Как видно из рисунка, в разделе Реестра HKEY_CLASSES_ROOT\CLSID xpaнится CLSID компонента Tail Rotor Simulator. Дружественное имя зарегистрировано как параметр по умолчанию для CLSID компонента. Подраздел InprocServer32 содержит имя файла DLL — C:\Helicopter\TailRotor.dll.

Имя файла и CLSID — две наиболее важные составляющие данных Реестра. Для многих компонентов СОМ ничего больше и не потребуется. Однако в некоторых случаях нужна дополнительная информация.


РИС. 6-4. Структура подраздела CLSID Реестра.

Другие детали Реестра

Давайте совершим краткую экскурсию по подразделам HKEY_CLASSES_ROOT. Мы уже знакомы с CLSID, и далее мы рассмотрим, какая дополнительная информация для классов хранится в этом подразделе. В начале HKEY_CLASSES_ROOT Вы можете видеть группу расширений имен файлов, зарегистрированных разными программами. После расширений следует множество других имен. По большей части это так называемые ProgID — что расшифровывается как программный идентификатор (programmatic identifier). Мы поговорим о ProgID немного позже. Некоторые из имен — не ProgID, а специальные разделы реестра, похожие на CLSID. Эти разделы связывают GUID с некоторыми другими данными, например, именами файлов. Такие разделы перечислены ниже.

ProgID

Теперь рассмотрим ProgID более подробно. Большая часть подразделов в ветви Реестра HKEY_CLASSES_ROOT - это ProgID. ProgID отображает «дружественную», понятную программисту строку в CLSID. Некоторые языки программирования, такие как Visual Basic, идентифицируют компоненты по ProgID, а не по CLSID. Уникальность ProgID не гарантируется, поэтому существует потенциальная опасность конфликта имен. Однако с ProgID легче работать. (Кроме того, некоторые языки не поддерживают структур, и в них пришлось бы использовать строковое представление CLSID.)
Соглашение об именовании ProgID
По соглашению ProgID имеет следующий формат:
<Программа>.<Компонент>.<Версия>
Вот несколько примеров из Реестра:
Visio.Application.3
Visio.Drawing.4
RealAudio.ReadAudio ActiveX Control (32-bit).1
Qffice.Binder.95
MSDEV.APPLICATION
JuiceComponent.RareCat.1
Но этот формат — лишь соглашение, а не жесткое правило, и в Реестре на моей машине полно компонентов, которые ему не следуют.

Во многих случаях клиента не интересует версия компонента, к которому он подключается. Таким образом, у компонента часто имеется ProgID, не зависящий от версии. Этот ProgID связывается с самой последней версией компонента из установленных в системе. Соглашение об именовании не зависящих от версии ProgID сводится к отбрасыванию номера версии. Пример такого ProgID, следующего соглашению, - MSDEV.APPLICATION.

ProgID в Реестре
ProgID и не зависящий от версии ProgID компонента приводятся в разделе CLSID. Однако основное назначение ProgID — обеспечить получение соответствующего CLSID. Просматривать все разделы CLSID для поиска ProgID было бы неэффективно. В связи с этим ProgID указывается непосредственно и в разделе HKEY_CLASSES_ROOT. ProgID не предназначены для представления конечным пользователям, поэтому по умолчанию значение любого раздела ProgID — дружественное для пользователя имя. В разделе ProgID имеется подраздел с именем CLSID, который содержит CLSID компонента в качестве значения по умолчанию. Не зависящий от версии ProgID также приводится непосредственно в разделе HKEY_CLASSES_ROOT. У него есть дополнительный подраздел CurVer, содержащий ProgID текущей версии компонента.

На рис. 6-5 представлен расширенный пример с рис. 6-4, включающий ProgID. В раздел CLSID компонента добавлен раздел с именем ProgID, и в него помещено значение Helicopter. TailRotor. I — ProgID компонента. Не зависящий от версии ProgID сохранен в разделе VersionIndependentProsID. В данном примере не зависящий от версии ProgID — Helicopter.TailRotor.

На рисунке также показаны отдельные разделы Helicopter. TailRotor и Helicopter. TailRotor.1, расположенные непосредственно в HKEY_CLASSES_ROOT. В разделе Helicopter. TailRotor.1 имеется единственный подраздел — CLSID, который содержит CLSID компонента. Не зависящий от версии ProgID Helicopter.TailRotor содержит подразделы CLSIDи CurVer. Значение по умолчанию подраздела CurVer—ProgID текущей версии компонента, Helicopter.TailRotor.1.


РИС. 6-5. Организация разделов Реестра, в которых содержится информация, имеющая отношение к ProgID.
От ProgID к CLSID
После того, как Вы поместили в Реестр нужную информацию, получить CLSID по ProgID и наоборот легко. Библиотека СОМ предоставляет две функции — CLSIDFromProgID и ProgIDFromCLSID, — которые производят все необходимые манипуляции с Реестром:
    CLSID clsid;
    CLSIDFromProgID("Helicopter.TailRotor", &clsid);

Саморегистрация

Каким образом информация о компоненте попадает в Реестр Windows? Так как DLL знает о содержащемся в ней компоненте, она может поместить эту информацию в Реестр. Но, поскольку DLL ничего не делает сама по себе, Вам следует экспортировать следующие две функции:
    STDAPI DllRegisterServer();
    STDAPI DllUnregisterServer();
STDAPI определен в OBJBASE.H как
#define STDAPI EXTERN_C HRESULT STDAPICALLTYPE
что раскрывается в
extern "С" HRESULT __stdcall
С помощью программы REGSVR32.EXE эти функции можно вызывать для регистрации компонента. Эта вездесущая утилита, вероятно, уже есть на Вашем компьютере; если же нет, ее копию можно найти на прилагающемся к книге компакт-диске. В примерах программ ряда последующих глав этой книги make-файлы будут вызывать REGSVR32.EXE для регистрации соответствующих компонентов.

Большинство программ установки вызывают DllRegisterServer в процессе своей работы. Для этого нужно просто загрузить DLL с помощью LoadLibrary, получить адрес функции с помощью GetProcAddress и потом, наконец, вызвать функцию.

Реализация DllRegisterServer
Реализация DllRegisterServer — простой код обновления Реестра. Win32 содержит множество функций, добавляющих и удаляющих разделы Реестра. Для регистрации наших компонентов и удаления их из Реестра понадобятся только шесть функций:
RegOpenKeyEx
RegCreateKeyEx
RegSetValueEx
RegEnumKeyEx
RegDeleteKey
RegCloseKey
Об этих функциях много написано в других книгах, поэтому я не собираюсь детально рассматривать их здесь. Чтобы использовать эти функции, включите в Ваш исходный файл WINREG.H и WINDOWS.H и скомпонуйте программу с ADVAPI32.LIB. Увидеть эти функции в действии Вы сможете в файлах REGISTRY.H и REGISTRY.CPP из примера следующей, седьмой главы.

Категории компонентов

Минималистский взгляд на Реестр Windows состоит в том, что это длинный список CLSID, с каждым из которых связано имя файла. Клиент может просмотреть эти CLSID и выбрать подходящий компонент. Но как клиент определяет, какой именно компонент следует использовать? Один из вариантов — вывести список дружественных имен компонентов и предоставить пользователю выбирать. Однако пользователю вряд ли понравится, если, выбрав компонент из длинного списка, он в итоге узнает, что тот не работает. Загружать каждый из компонентов, упомянутых в Реестре, и запрашивать у них необходимые нам интерфейсы — слишком затяжное мероприятие. Надо как-то уметь определять, поддерживает ли компонент нужные интерфейсы, до создания экземпляра этого компонента.

Решение этой проблемы дают категории компонентов (component categories). Категория компонентов — это набор интерфейсов, которым присвоен CLSID, называемый в данном случае CATID. Компоненты, реализующие все интерфейсы некоторой категории, могут зарегистрироваться как члены данной категории. Это позволяет клиентам более осмысленно выбирать компоненты из реестра, рассматривая только те, которые принадлежат к некоторой категории.

Категория компонентов — тоже своего рода договор между компонентом и клиентом. Регистрируя себя в некоторой категории, компонент тем самым гарантирует, что поддерживает все входящие в категорию интерфейсы. Категории могут использоваться для типизации компонентов. Использование категорий аналогично использованию абстрактных базовых классов в C++. Абстрактный базовый класс — это набор функций, которые производный класс обязан реализовать; поэтому можно сказать, что производный класс — конкретная реализация данного абстрактного базового класса. Категория компонентов — набор интерфейсов, которые должны быть реализованы компонентом, чтобы тот относился к данной категории. Компонент, принадлежащий категории, — конкретная реализация этой категории.

Компонент может входить в произвольное число категорий. Компонент не обязан поддерживать исключительно те интерфейсы, которые определены категорией; он может поддерживать любой интерфейс в дополнение к ним.

Одно из применений категорий — задание набора интерфейсов, которые компонент обязан поддерживать. Альтернативой служит задание набора интерфейсов, которые компонент требует от своего клиента. Компоненту для нормальной работы могут потребоваться от клиента некоторые сервисы. Например, трехмерному графическому объекту для работы может требоваться определенная графическая библиотека (graphic engine).

Реализация категорий компонентов
Самое приятное в категориях компонентов то, что для их использования Вам не нужно возиться с Реестром самостоятельно. В системах Windows имеется стандартный Диспетчер категорий компонентов (Component Cajtegory Manager), который проделает за Вас всю работу Этот Диспетчер (CLSID_StdComponentCategoriesMgr) — стандартный компонент СОМ, реализующий два интерфейса, ICatRegister и ICatInformation. ICatRegister используется для регистрации и удаления категорий. Он также может использоваться для добавления и удаления компонентов к категории. ICatInformation применяется для получения информации о категориях в системе. С помощью этого интерфейса Вы можете найти: Более полная документация Диспетчера категорий компонентов содержится на прилагающемся к книге компакт-диске. Поищите ICatRegister и ICatInformation в файле ACTIVEX.MVB.

С помощью Диспетчера категорий легко добавлять и удалять категории. Использование Диспетчера показано в примере программы из этой главы, который можно найти на компакт-диске. Программа выдает список зарегистрированных в системе категорий компонентов, добавляет новую категорию, снова составляет список категорий, добавляет компонент в новую категорию и, наконец, удаляет эту категорию. Если у Вас эта программа не работает, то возможно, что на Вашем компьютере не установлены некоторые файлы. В прилагаемом к, примеру файле README указаны файлы, которые могут отсутствовать, и поясняется, как их установить с компакт-диска.

Даже если Вам не нужны категории компонентов, данный пример все равно представляет интерес, так как это первый случай использования нами компонента СОМ, реализованного кем-то другим.

OleView

Редактор Реестра показывает Реестр «в чистом виде», что полезно для изучения. Вы должны знать организацию Реестра, чтобы реализовать самостоятельно регистрирующиеся компоненты или клиентов, которые будут опрашивать Реестр. Однако если Вам нужна дополнительная информация об установленных на компьютере компонентах, использование Редактора Реестра может потребовать слишком много времени. Ведь он, по существу, показывает все данные в виде списка CLSID.

Другая программа из Win32 SDK — OleView — представляет информацию на более высоком уровне. Копия OleView (OLEVIEW.EXE) имеется на прилагающемся к книге компакт-диске. Вместо длинного списка CLSID и других GUID OleView отображает деревья, "содержащие элементы с дружественными именами. Кроме того, OleView позволяет просматривать категории компонентов, установленных в системе. Для изучения лучше всего запустить OleView и поработать. Я использовал эту программу для провеки моего кода саморегистрации. Если OleView может найти информацию, скорее всего, эта информация помещена в правильное место.

Некоторые функции библиотеки СОМ

Всем клиентам и компонентам СОМ приходится выполнять много типовых операций. Чтобы сделать выполнение этих операций стандартным и совместимым, СОМ предоставляет библиотеку функций. Библиотека реализована в OLE32.DLL. Для статической компоновки с ней Вы можете использовать OLE32.LIB. В этом разделе мы рассмотрим некоторые из важных типовых операций.

Инициализация библиотеки СОМ

Во-первых, рассмотрим инициализацию самой библиотеки СОМ. Процесс должен вызвать CoInitialize для инициализации библиотеки, прежде чем использовать ее функции (за исключением функции CoBuildVersion, возвращающей номер версии библиотеки). Когда процесс завершает работу с библиотекой СОМ, он должен вызвать CoUninitialize. Прототипы этих функций приведены ниже:
HRESULT CoInitialize(void* reserved) ; // Значением параметра должно быть NULL.
void CoUninitialize();
Библиотека СОМ требует инициализации только один раз для каждого процесса. Многократные вызовы процессом CoInitialize допустимы, но каждому из них должен соответствовать отдельный вызов CoUninitialize. Если CoInitialize уже была вызвана данным процессом, то она возвращает не S_OK, a S_FALSE.

Поскольку в данном процессе библиотеку СОМ достаточно инициализировать лишь один раз, и поскольку эта библиотека используется для создания компонентов, компонентам в процессе не требуется инициализировать библиотеку. По общему соглашению СОМ инициализируется в ЕХЕ, а не в DLL.

Использование OleInitlalize
OLE, построенная «поверх» СОМ, добавляет поддержку библиотек типов, буфера обмена, перетаскивания мышью, документов ActiveX, Автоматизации и управляющих элементов ActiveX. Библиотека OLE содержит дополнительную поддержку этих возможностей. Если Вы хотите использовать все это, следует вызывать OleInitialize и OleUninitialize вместо CoInitialize и CoUninitialize. Обычно проще всего вызвать функции Ole* и забыть о них. Функции Ole* вызывают соответствующие функции Com*. Однако использование Ole* вместо Com* приводит к излишним расходам ресурсов и времени, если расширенные возможности не используются.
CoInitializeEx
В операционных системах Windows, поддерживающих DCOM, Вы можете использовать CoInitializeEx, чтобы пометить компонент как использующий модель свободных потоков (free-threaded). Более подробная информация о CoInitializeEx содержится в гл. 12.

Управление памятью

Очень часто внутренняя функция компонента выделяет блок памяти, который возвращается клиенту через выходной параметр. Но кто и каким образом будет эту память освобождать? Наибольшая проблема связана с тем, кто освободит память, — ведь клиент и компонент могут быть реализованы разными людьми, написаны на разных языках и даже выполняться в разных процессах. Необходим стандартный способ выделения и освобождения такой памяти.

Решение предоставляет менеджер памяти задачи (task memory allocator) СОМ. С его помощью компонент может передать клиенту блок памяти, который тот будет в состоянии освободить. Кроме того, менеджер «гладко» работает с потоками, поэтому его можно применять в многопоточных приложениях.

Как обычно, менеджер используется через интерфейс. В данном случае интерфейс называется IMalloc и возвращается функцией CoGetMalloc. IMalloc::Alloc выделяет блок памяти, a IMalloc::Free освобождает память, выделенную с помощью IMalloc: :Alloc. Однако обычно вызывать CoGetMalloc для получения указателя на интерфейс, вызывать с помощью этого указателя, функции и затем освобождать указатель — значит делать слишком много работы. Поэтому библиотека СОМ предоставляет удобные вспомогательные функции — CoTaskMemAlloc и CoTaskMemFree:

void* CoTaskMemAlloc(
    ULONG cb // Размер выделяемого блока в байтах
    );

void CoTaskMemFree(
    void* pv // Указатель на освобождаемый блок памяти
    );
Память, выделенную и переданную при помощи выходного параметра, всегда освобождает вызывающая процедура (пользующаяся CoTaskMemFree).

Преобразование строк в GUID

В Реестре содержатся строковые представления CLSID. Поэтому нам нужны специальные функции для преобразования CLSID в строку и обратно. В библиотеке СОМ имеется несколько удобных функций такого рода.StringFromGUID2 конвертирует GUID в строку:
wchar_t szCLSID[39];
int r = ::StringFromGUID2(CLSID_Component1, szCLSID, 39);
StringFromGUID2 генерирует строку символов Unicode, т. е. строку двухбайтовых символов типа wchar_t, а не char. В системах, не использующих Unicode, Вам придется преобразовать результат в char. Для этого можно прибегнуть к функции ANSI wcstombs, как показано ниже.
#ifndef _UNICODE
    // Преобразование из строки Unicode в обычную
    char szCLSID_single[39];
    wcstombs(szCLSID_single,szCLSID,39);
#endif
Есть еще несколько функций, выполняющих аналогичные операции:

Функция

Назначение


StringFromCLSID


Безопасное с точки зрения приведения типов преобразование CLSID в строку

StringFromIID

Безопасное с точки зрения приведения типов преобразование IID в строку

StringFromGUID2

Преобразование GUID в текстовую строку; строка возвращается в буфер, выделенный вызывающей программой

CLSIDFromString

Безопасное с точки зрения приведения типов преобразование строки в CLSID

IIDFromString

Безопасное с точки зрения приведения типов преобразование строки в IID


Некоторые из этих функций требуют использовать менеджер памяти задачи из предыдущего раздела:
    wchar_t* string;
    //Получить строку из CLSID
    ::StringFromCLSID(CLSID_Component1, &string);
    // Использовать строку
    :
    :
    // Освободить строку
    ::CoTaskMemFree(string);

Резюме

Строим ли мы дома (например, в гостиной) самолет, пишем ли ночами книгу или разрабатываем компоненты, большая часть нашего времени и энергии уходит на тысячи деталей. Внимание к деталям и правильное обращение с ними определяет успех. В этой главе Вы узнали, что СОМ использует HRESULT для возвращения кодов успеха или ошибки. Вы узнали о GUID — удивительной структуре данных, которая основана на алгоритме, позволяющем кому угодно, где угодно и когда угодно получать уникальный идентификатор. Вы также видели, что СОМ использует GUID для идентификации практически всех объектов, в том числе компонентов (CLSID) и интерфейсов (IID).

Вы узнали и о том, как CLSID транслируется в имя файла компонента с помощью Реестра Windows. Для регистрации компонента программа установки или REGSRV32.EXE вызывают функцию DllRegisterServer, экспортированную DLL компонента. В минимальном варианте компонент помещает в Реестр свой CLSID и имя файла.

В следующей главе мы увидим, как СОМ создает компонент при помощи CLSID. Это гораздо проще, чем построить самолет в гостиной.
Замечание о макросах определения интерфейсов
Существуют макросы, облегчающие программистам переход с С на C++; они помогают добиться того, чтобы одно и то же определение интерфейса работало в программах на обоих языках. Эти макросы есть как в OBJBASE.H, так и в BASETYPS.H. Ранее в примерах я использовал следующий простой интерфейс:
interface IX : IUnknown
{
    virtual void __stdcall Fx() = 0;
};
При использовании упомянутых макросов этот интерфейс выглядит так:
DECLARE_INTERFACE(IX, IUnknown)
{
    //IUnknown
    STDMETHOD(QueryInterface) (THIS_ REFIID, PPVOID)
PURE ;
    STDMETHOD_(ULONG, AddRef) (THIS) PURE ;
    STDMETHOD_(ULONG, Release) (THIS) PURE ;
    //IX
    STDMETHOD_(void, Fx) (THIS) PURE ;
Однако сам я не использую эти макросы, предпочитая писать код так, чтобы он выглядел и работал, как код на C++. Если бы я собирался публиковать свои компоненты, чтобы их использовали другие люди, то писал бы интерфейс на специальном языке. Этот язык описания интерфейсов, называемый IDL, рассматривается в гл. 10 и 11.

[Назад] [Содержание] [Вперед]


Смотрите также:



Hosted by uCoz