Дух братьев Райт все еще жив. Каждый год сотни людей в своих гаражах строят самолеты из наборов «Сделай сам». Они делают не пластиковые игрушки, радиоуправляемые модели или легковесные матерчатые конструкции. Они строят современные двухместные самолеты с полностью закрытой кабиной из современнейших композитных материалов. По правилам Федерального авиационного агентства достаточно, чтобы производитель набора выполнил только 49% всей работы по постройке самолета. Оставшийся 51% конструктор-любитель делает сам.
Постройка 51% самолета занимает, в зависимости от модели, примерно от 250 до 5000 часов. Большинство производителей предлагают наборы «для быстрого приготовления», в которых многие части уже собраны, например, сварены рамы и пропитаны детали из композитных материалов. Используя такие заготовки, можно быстро сделать нечто, похожее на самолет. Однако это будет еще далеко не настоящий самолет. Куча времени уходит на разные мелочи — установку панели управления и приборов, сидений, ремней безопасности, огнетушителей, покрытия, сигнализации, табличек, кабелей управления, электропроводки, лампочек, батарей, брандмауэров, вентиляционных люков, крыши'пилотской кабины, отопителя, окон, замков и ручек на дверях и еще многого другого.
Многие энтузиасты самолетостроения приходят в уныние, разочаровываются и даже бросают это занятие, потратив на сборку многие часы. Точно так же многие начинают изучать сложный предмет, например СОМ, лишь затем, чтобы захлебнуться в деталях и все бросить. В первых пяти главах книги я пытался максимально избегать подробностей, чтобы Вы сосредочились на общей картине. В этой главе я собираюсь обсудить некоторые из тех деталей, которые раньше пропускал или скрывал. Я хочу рассмотреть и другие детали, которые понадобятся нам в следующих главах.
Сначала мы обсудим HRESULT — тему, впервые возникшую в гл. 3 в связи с QueryInterface. Затем мы рассмотрим GUID. Один из примеров GUID - структура IID, передаваемая QueryInterface. После обсуждения этих типов мы познакомимся с тем, как компоненты публикуют в Реестре данные о своем местонахождении (это позволяет клиентам находить и создавать компоненты). В заключение мы рассмотрим некоторые полезные функции и утилиты библиотеки СОМ.
Во всех самолетах есть приборы, и самодельные самолеты — не исключение. Хотя в некоторых таких самолетах роль приборов играет компьютер с цветным графическим дисплеем (иногда даже «под» 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 приведены наиболее часто используемые коды. По соглашению в названиях успешных кодов возврата содержится 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.
Как уже отмечалось, определение всех кодов состояния СОМ (и 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 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 и сеть |
Часто связь с удаленной машиной по сети неожиданно прерывается. Если клиент работает с удаленным компонентом, он должен уметь элегантно обрабатывать разрыв сетевого соединения. Это означает, что у каждого вызова функции, который может выполняться по сети, должен быть некий способ индикации разрыва связи. По этой причине все методы, которые могут выполняться на удаленной машине, должны возвращать HRESULT. Избегайте других типов возвращаемого значения,например:
double GetCordLenght(double BladeSection);Вместо этого возвращайте из функции HRESULT, а все результаты передавайте через выходные параметры: HRESULT GetCordLength(/* in */ double BladeSection, /* out */ double* pLength);HRESULT передает клиенту информацию, необходимую для обнаружения сетевых ошибок. Вызовы функций в Автоматизации (ранее OLE Автоматизация) удовлетворяют этому требованию. Более подробно удаленные компоненты будут рассмотрены в гл. 10. |
Хотя смысл кода возврата, отмеченного с помощью 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:
MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 512); MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_ITF, 513);По соглашению нестандартным кодам завершения дается к качестве префикса имя компонента или интерфейса. Например, двум приведенным выше кодам можно было бы дать имена
AIRPLANE_E_LANDINGWITHGEARUP HELICOPTER_S_ROTORRPMGREENСказанного о HRESULT более чем достаточно. Теперь пора снять завесу таинственности с 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 можно сгенерировать программно, без какой-либо координирующей организации. 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. |
// {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.
inline BOOL operator ==(const GUID& guid1, const GUID& guid2) { return !memcnip(&guid1, &guid2, sizeof(GUID)); }Нам уже приходилось использовать эту операцию в QueryInterface. Если Вы не любите упрятывать истинный код во внешне простенькие операторы, OBJBASE.H дает определения эквивалентных по смыслу функций IsEqualGUID, IsEqualIID и IsEqualCLSID.
IUnknown* CallCreateInstance(char* name) ;В следующей главе мы заменим эту функцию на функцию библиотеки СОМ CoCreateInstance. Последняя использует для идентификации компонента не строку, a GUID. Такой GUID в СОМ называется идентификатором класса. Чтобы отличать идентификаторы классов от IID, для них используют тип CLSID.
Подобно интерфейсам, все компоненты имеют различные идентификаторы. Два компонента могут реализовывать в точности одинаковые наборы интерфейсов, но у них по-прежнему должны быть свои CLSID. Компоненты могут добавлять новые интерфейсы, не изменяя CLSID. Однако если изменение компонента отражается на работе какого-нибудь приложения, компоненту надо дать новый CLSID.
const IID&можете использовать эквивалентное выражение REFIID. Точно так же для передачи идентификаторов классов можно использовать REFCLSID, а для передачи GUID - REFGUID.
Теперь давайте рассмотрим, как компоненты регистрируются в системе (чтобы клиенты смогли их найти и использовать),
FAA ведет реестр всех летательных аппаратов, включая самодельные. По этому реестру можно определить, кто хозяин самолета. В этой главе мы рассмотрим чем-то похожий реестр, позволяющий определить, какой DLL принадлежит данный компонент.
В гл. 5 при создании компонента мы передавали функции CallCreateInstance имя файла соответствующей DLL. В следующей главе мы собираемся заменить CallCreateInstance функцией библиотеки СОМ CoCreateInstance. Для идентификации компонента CoCreateInstance вместо имени файла использует CLSID (по нему определяется имя файла DLL). Компоненты помещают имена своих файлов, индексированные CLSID, в Реестр Windows. CoCreateInstance отыскивает имя файла, используя CLSID как ключ.
В реальной жизни реестр — это учетная книга для записи предметов, имен или действий. В Windows Реестр — это общедоступная база данных операционной системы. Реестр содержит информацию об аппаратном и программном обеспечении, о конфигурации компьютера и о пользователях. Любая программа для Windows может добавлять и считывать информацию из Реестра; клиенты могут искать там нужные компоненты. Но прежде чем поместить свою информацию в Реестр, надо узнать, как он устроен.
Пока в разделе каждого 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 — две наиболее важные составляющие данных Реестра. Для многих компонентов СОМ ничего больше и не потребуется. Однако в некоторых случаях нужна дополнительная информация.
<Программа>.<Компонент>.<Версия>Вот несколько примеров из Реестра:
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.
На рис. 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.
CLSID clsid; CLSIDFromProgID("Helicopter.TailRotor", &clsid);
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 и потом, наконец, вызвать функцию.
RegOpenKeyEx RegCreateKeyEx RegSetValueEx RegEnumKeyEx RegDeleteKey RegCloseKeyОб этих функциях много написано в других книгах, поэтому я не собираюсь детально рассматривать их здесь. Чтобы использовать эти функции, включите в Ваш исходный файл WINREG.H и WINDOWS.H и скомпонуйте программу с ADVAPI32.LIB. Увидеть эти функции в действии Вы сможете в файлах REGISTRY.H и REGISTRY.CPP из примера следующей, седьмой главы.
Решение этой проблемы дают категории компонентов (component categories). Категория компонентов — это набор интерфейсов, которым присвоен CLSID, называемый в данном случае CATID. Компоненты, реализующие все интерфейсы некоторой категории, могут зарегистрироваться как члены данной категории. Это позволяет клиентам более осмысленно выбирать компоненты из реестра, рассматривая только те, которые принадлежат к некоторой категории.
Категория компонентов — тоже своего рода договор между компонентом и клиентом. Регистрируя себя в некоторой категории, компонент тем самым гарантирует, что поддерживает все входящие в категорию интерфейсы. Категории могут использоваться для типизации компонентов. Использование категорий аналогично использованию абстрактных базовых классов в C++. Абстрактный базовый класс — это набор функций, которые производный класс обязан реализовать; поэтому можно сказать, что производный класс — конкретная реализация данного абстрактного базового класса. Категория компонентов — набор интерфейсов, которые должны быть реализованы компонентом, чтобы тот относился к данной категории. Компонент, принадлежащий категории, — конкретная реализация этой категории.
Компонент может входить в произвольное число категорий. Компонент не обязан поддерживать исключительно те интерфейсы, которые определены категорией; он может поддерживать любой интерфейс в дополнение к ним.
Одно из применений категорий — задание набора интерфейсов, которые компонент обязан поддерживать. Альтернативой служит задание набора интерфейсов, которые компонент требует от своего клиента. Компоненту для нормальной работы могут потребоваться от клиента некоторые сервисы. Например, трехмерному графическому объекту для работы может требоваться определенная графическая библиотека (graphic engine).
С помощью Диспетчера категорий легко добавлять и удалять категории. Использование Диспетчера показано в примере программы из этой главы, который можно найти на компакт-диске. Программа выдает список зарегистрированных в системе категорий компонентов, добавляет новую категорию, снова составляет список категорий, добавляет компонент в новую категорию и, наконец, удаляет эту категорию. Если у Вас эта программа не работает, то возможно, что на Вашем компьютере не установлены некоторые файлы. В прилагаемом к, примеру файле README указаны файлы, которые могут отсутствовать, и поясняется, как их установить с компакт-диска.
Даже если Вам не нужны категории компонентов, данный пример все равно представляет интерес, так как это первый случай использования нами компонента СОМ, реализованного кем-то другим.
Другая программа из Win32 SDK — OleView — представляет информацию на более высоком уровне. Копия OleView (OLEVIEW.EXE) имеется на прилагающемся к книге компакт-диске. Вместо длинного списка CLSID и других GUID OleView отображает деревья, "содержащие элементы с дружественными именами. Кроме того, OleView позволяет просматривать категории компонентов, установленных в системе. Для изучения лучше всего запустить OleView и поработать. Я использовал эту программу для провеки моего кода саморегистрации. Если OleView может найти информацию, скорее всего, эта информация помещена в правильное место.
Всем клиентам и компонентам СОМ приходится выполнять много типовых операций. Чтобы сделать выполнение этих операций стандартным и совместимым, СОМ предоставляет библиотеку функций. Библиотека реализована в OLE32.DLL. Для статической компоновки с ней Вы можете использовать OLE32.LIB. В этом разделе мы рассмотрим некоторые из важных типовых операций.
HRESULT CoInitialize(void* reserved) ; // Значением параметра должно быть NULL. void CoUninitialize();Библиотека СОМ требует инициализации только один раз для каждого процесса. Многократные вызовы процессом CoInitialize допустимы, но каждому из них должен соответствовать отдельный вызов CoUninitialize. Если CoInitialize уже была вызвана данным процессом, то она возвращает не S_OK, a S_FALSE.
Поскольку в данном процессе библиотеку СОМ достаточно инициализировать лишь один раз, и поскольку эта библиотека используется для создания компонентов, компонентам в процессе не требуется инициализировать библиотеку. По общему соглашению СОМ инициализируется в ЕХЕ, а не в DLL.
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).
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. |