Создание автономного COM-сервера |
Общие замечанияАвтономный COM-сервер представляет собой обычное Windows-приложение (исполняемый файл в формате PE). Поскольку сервер и приложение-клиент в данном случае выполняются в различных адресных пространствах, для обмена данными между ними должен использоваться специальный механизм, называемый маршалингом (marshaling). Под обменом данными здесь понимается передача параметров методам реализуемых интерфейсов и получение от них результатов. Существуют следующие три варианта реализации маршалинга. 1. Custom (пользовательский). При использовании этого варианта предоставляемые сервером COM-классы должны реализовывать интерфейс IMarshal. 2. Standard (стандартный), который строится на основании протокола ORPC (Object Remote Procedure Call, вызов удаленной объектной процедуры). Этот вариант применяется, как правило, при реализации удаленных серверов). В обоих описанных случаях необходимо помимо собственно COM-сервера реализовать также DLL-библиотеку, являющуюся «переходником» (заглушкой, stub) между сервером и приложением-клиентом. По сути, эта DLL представляет собой внутрипроцессный сервер. 3. Universal (универсальный), при котором используется стандартная системная DLL-библиотека oleaut32.dll, выполняющая построение переходника «на лету» на основании предоставляемой COM-сервером библиотеки типов (type library). Для построения такой библиотеки необходимо использовать специальный файл на языке ODL (Object Definition Language) или IDL (Interface Definition Language, является расширением первого), который обрабатывается либо компилятором midl.exe, либо утилитой mktyplib.exe, входящими в состав Microsoft Visual Studio. Этот вариант наиболее удобен, поскольку не требует создания дополнительной DLL-библиотеки. В качестве «расплаты» за удобство, использование универсального маршалинга предъявляет к серверу следующее требование: все методы реализуемых интерфейсов (за исключением, конечно, «стандартных» — в частности, IUnknown и IClassFactory) должны возвращать значение типа HRESULT и принимать в качестве параметров величины OLE-automation-совместимых типов. К основным таким типам относятся char (для отдельных символов), BSTR (для строк), short и long (для целых чисел), float и double (для дробных). Кроме этого, совместимым типом будет считаться указатель на совместимый тип, однако, в отличие от C/C++, при передаче по указателю будет доступен только один элемент соответствующего типа, то есть передать таким образом массив невозможно — необходимо использовать специально предназначенный для этого тип SAFEARRAY, также являющийся OLE-automation-совместимым. Кроме перечисленных, компилятор MIDL допускает также использование типа int для целочисленных параметров, но при этом их размер является системно-зависимым и может на различных платформах составлять 2, 4 или 8 байт. Заметим, что внутрипроцессные сервера тоже имеет смысл реализовывать как OLE-automation-совместимые, поскольку это существенно облегчает обращение к ним из приложений-клиентов, создаваемых с использованием других языков программирования. Естественно, эта рекомендация не носит ультимативного характера, поскольку все определяется назначением и сферой применения конкретного COM-сервера. Синтаксис описания библиотеки типовФайл на ODL, описывающий библиотеку типов, должен иметь следующую структуру: [атрибуты] library имя_библиотеки { // объявления интерфейсов и COM-классов }; Здесь атрибуты — это задаваемые с помощью ключевых слов ODL характеристики данной библиотеки типов (если задается несколько атрибутов, они должны быть перечислены через запятую); имя_библиотеки может быть произвольным (как правило, оно совпадает с именем COM-сервера). Обязательным является указание атрибута uuid, назначающего уникальный идентификатор данной библиотеки типов (library identifier, LIBID) и имеющего синтаксис uuid(идентификатор_библиотеки)
где идентификатор должен быть указан в формате Registry Format утилиты guidgen.exe, но без фигурных скобок. Ниже перечислены остальные допустимые атрибуты. version(основной_номер.дополнительный_номер)
Этот атрибут позволяет задать номер версии библиотеки типов. helpstring("строка")
Указанная строка будет отображаться в качестве подсказки различными браузерами COM-компонентов (один из них доступен, например, в среде Visual Basic). Таким образом, пример описания библиотеки типов может выглядеть так: [uuid(72FE438D-7EF8-4D1B-A694-6295CFC24EF8), version(1.0)] library SuperLib { // объявления интерфейсов и COM-классов }; В теле описания библиотеки (внутри фигурных скобок) может быть использована директива importlib("имя_библиотеки_типов");
предоставляющая доступ с объявлениям, сделанным в библиотеке типов с заданным именем. Это бывает необходимо при обращении к типам и интерфейсам, реализуемым другим COM-сервером. Например, почти каждая библиотека типов содержит директиву importlib("stdole32.tlb");
позволяющую использовать «стандартные» интерфейсы OLE/COM. Заметим, что по своему назначению директива ODL importlib аналогична директиве препроцессора #include в языках C/C++. Объявления интерфейсов должны содержаться в теле описания библиотеки типов и иметь следующий вид: [атрибуты] interface имя_интерфейса: имя_предка { // объявления методов интерфейса }; Обязательными атрибутами являются object, говорящий о том, что данный элемент относится к COM, и uuid, назначающий уникальный идентификатор объявляемого интерфейса. При использовании универсального маршалинга обязательным является также атрибут oleautomation. Помимо перечисленных, при объявлении интерфейса могут использоваться атрибуты version и helpstring, синтаксис и назначение которых аналогичны одноименным атрибутам самой библиотеки типов. Объявления методов интерфейса должны иметь вид тип_возвр_знач имя_метода ( [атрибут_парам_1] тип_парам_1 имя_парам_1, [атрибут_парам_2] тип_парам_2 имя_парам_2, ... ); В качестве атрибута параметра указывается ключевое слово in для входных или out — для выходных параметров. Заметим, что выходные параметры должны быть указателями и их может быть несколько (а может и не быть совсем). Если метод не принимает параметров, то вместо их списка необходимо, как и в C/C++, использовать ключевое слово void. Объявления COM-классов должны содержаться в теле описания библиотеки типов и включать в себя имена всех реализуемых данным классом интерфейсов. Они оформляются следующим образом: [атрибуты] coclass имя_класса { interface имя_интерфейса_1; interface имя_интерфейса_2; ... }; Обязательным является атрибут uuid, назначающий уникальный идентификатор COM-класса; кроме него допустимы атрибуты version и helpstring. Ниже приведен законченный пример файла на ODL, описывающего библиотеку типов гипотетического COM-сервера. [uuid(72FE438D-7EF8-4D1B-A694-6295CFC24EF8), version(1.0)] library SuperLib { // включаемая библиотека типов importlib("stdole32.tlb"); // реализуемый интерфейс [object, uuid(E9C494F5-6109-4518-9C3C-02ED3921E665), oleautomation] interface IWorker: IUnknown { HRESULT DoSomethig([in] BSTR bstrParam); HRESULT UndoSomething(void); HRESULT GetSomething([in] long nParam, [out] double* pDest); }; // предоставляемый COM-класс [uuid(84FA653D-CBCF-41A9-8E87-689C01EE3E6D), version(1.0)] coclass CoMyClass { interface IWorker; }; }; Заметим, что именно библиотека типов, полученная в результате компиляции файла, написанного на ODL, является унифицированным источником информации об интерфейсах и классах, предоставляемых соответствующим COM-сервером, независимо от того, каков его тип — внутрипроцессный или автономный. Библиотеки типов распознаются любым «COM-совместимым» средством разработки, к числу которых относятся Delphi, C++Builder и Visual Basic; в силу этого, можно рекомендовать их создание не только для автономных, но и для внутрипроцессных COM-серверов, если предполагается написание приложений-клиентов на языке, отличном от C/C++. Использование типа данных SAFEARRAY
Тип SAFEARRAY, формально являющийся структурой, объявлен в заголовочном файле <oleauto.h>; при описании соответствующего параметра интерфейса в .odl файле используется конструкция вида SAFEARRAY(тип_элементов) имя_параметра причем для использования универсального маршалинга указанный тип_элементов должен быть OLE-automation-совместимым. В тексте на C/C++ такой параметр должен иметь тип SAFEARRAY*. Ниже приведены основные функции, предназначенные для работы с «безопасными массивами». Создание массива выполняется с помощью функции SAFEARRAY* SafeArrayCreate( VARTYPE vt, unsigned int uNumDims, SAFEARRAYBOUND* psab ); Значение параметра vt определяется требуемым типом элементов массива:
Параметр uNumDims задает количество измерений создаваемого массива, а через параметр psab передается адрес массива структур, содержащих информацию о границах каждого измерения. Структура SAFEARRAYBOUND имеет следующие поля:
При успешном выполнении функция возвращает указатель, идентифицирующий созданный массив, а в случае ошибки — значение NULL. Таким образом, для создания «таблицы» целых чисел, состоящей из 4-х строк и 3-х столбцов, необходимо выполнить следующие действия:
Для создания одномерного массива можно воспользоваться функцией SAFEARRAY* SafeArrayCreateVector( VARTYPE vt, long lLbound, unsigned int cElements ); создающей вектор элементов типа vt с индексом первого элемента равным lLbound и количеством элементов cElements. При успешном выполнении функция возвращает указатель, идентифицирующий созданный массив, а в случае ошибки — значение NULL. Уничтожение массива производится с помощью функции HRESULT SafeArrayDestroy( SAFEARRAY* psa ); которая освобождает память, занятую массивом psa, и возвращает одно из следующих значений:
Запись элемента в массив выполняется с помощью функции HRESULT SafeArrayPutElement( SAFEARRAY* psa, long* pnIndices, void* pvSrc ); Параметр psa должен идентифицировать массив, а через параметр pnIndices необходимо передать адрес массива, содержащего все «координаты» элемента, в который будет скопировано значение переменной, находящейся по адресу pvSrc. Заметим, что в случае использования типа BSTR, который уже является указателем, соответствующую переменную необходимо передавать по значению, без дополнительной операции взятия адреса:
Функция SafeArrayPutElement может вернуть одно из следущих значений:
Чтение элемента массива осуществляется с помощью функции HRESULT SafeArrayGetElement( SAFEARRAY* psa, long* pnIndices, void* pvDest ); Параметр psa должен идентифицировать массив, а через параметр pnIndices необходимо передать адрес массива, содержащего все «координаты» элемента, значение которого должно быть скопировано в переменную по адресу pvDest. Функция может вернуть одно из следующих значений:
Получение границ массива. Существует функция HRESULT SafeArrayGetLBound( SAFEARRAY* psa, unsigned int uDim, long* pnDest ); которая записывает в переменную по адресу pnDest индекс первого элемента для измерения с индексом uDim (причем индекс первого измерения равен 1) в массиве psa и возвращает одно из следующих значений:
Получить индекс последнего элемента для заданного измерения можно с помощью функции HRESULT SafeArrayGetUBound( SAFEARRAY* psa, unsigned int uDim, long* pnDest ); параметры и возможные возвращаемые значения которой такие же, как и у функции SafeArrayGetLBound. Копирование массива может быть выполнено с помощью одной из следующих функций. HRESULT SafeArrayCopy( SAFEARRAY* psaSrc, SAFEARRAY** ppsaDest ); Эта функция создает массив, являющийся копией psaSrc, и записывает его идентификатор в переменную по адресу ppsaDest. Созданный массив должен быть впоследствии удален с помощью функции SafeArrayDestroy. В свою очередь, функция HRESULT SafeArrayCopyData( SAFEARRAY* psaSrc, SAFEARRAY* psaDest ); копирует данные из массива psaSrc на место данных массива psaDest. Обе эти функции могут возвращать одно из следующих значений:
Получение типа элементов массива выполняется с помощью функции HRESULT SafeArrayGetVartype( SAFEARRAY* psa, VARTYPE* pvtDest ); которая записывает тип элементов массива psa в переменную по адресу pvtDest и возвращает одно из следующих значений:
Заметим, что для корректной работы этой функции необходимо сразу же после создания массива установить для него флаг FADF_HAVEVARTYPE. Это делается следующим образом:
Блокирование и разблокирование массива. Для получения прямого доступа к области памяти, в которой находятся элементы массива, его можно временно блокировать с помощью функции HRESULT SafeArrayLock( SAFEARRAY* psa ); возвращающей одно из следующих значений:
В случае успешного блокирования массива, в поле pvData структуры SAFEARRAY, адресуемой параметром psa, будет записан адрес области памяти, в которой располагаются элементы массива. Полученный таким образом указатель можно использовать, например, для «группового» чтения/записи элементов с помощью функции memmove.
Разблокирование массива выполняется с помощью функции HRESULT SafeArrayUnlock( SAFEARRAY* psa ); которая может вернуть одно из следующих значений:
Еще раз подчеркнем, что массив может быть удален только после того, как он будет разблокирован. Информация в системном реестреВводная информации на эту тему приведена в одноименной части темы Создание внутрипроцессного COM-сервера, поэтому здесь мы остановимся только на ключевых моментах регистрации автономного сервера. Итак, для обеспечения корректной работы COM-библиотеки необходимо, чтобы в системном реестре каждому COM-классу соответствовала информация приведенной ниже структуры. 1. Должен существовать ключ вида HKEY_CLASSES_ROOT\CLSID\{идентификатор_класса} содержащий в качестве значения по умолчанию (безымянного значения) строку вида программный_идентификатор 2. Должен существовать ключ вида HKEY_CLASSES_ROOT\CLSID\{идентификатор_класса}\LocalServer32 содержащий в качестве значения по умолчанию строку с полным именем исполняемого файла сервера. 3. Должен существовать ключ вида HKEY_CLASSES_ROOT\программный_идентификатор\CLSID содержащий в качестве значения по умолчанию строку вида {идентификатор_класса} Нетрудно видеть, что единственное отличие от внутрипроцессного сервера состоит в имени второго ключа: для записи имени исполняемого файла сервера следует использовать ключ LocalServer32 вместо InprocServer32. Помимо создания перечисленных ключей и величин, необходимо зарегистрировать в системном реестре библиотеку типов. Для этого следует вначале загрузить библиотеку типов в память и получить указатель на соответствующий ей экземпляр интерфейса ITypeLib с помощью функции HRESULT LoadTypeLibEx( LPCOLESTR pszName, REGKIND regKind, ITypeLib** ppTypeLib ); Через параметр pszName передается полное имя библиотеки типов. В качестве параметра regKind можно указать одно из следующих значений:
Функция записывает полученный указатель на интерфейс ITypeLib в переменную по адресу ppTypeLib и возвращает одно из следующих значений:
После загрузки библиотеки типов для ее регистрации используется функция HRESULT RegisterTypeLib( ITypeLib* pTypeLib, OLECHAR* pszName, OLECHAR* pszHelpDir ); которой передается полученный ранее указатель на интерфейс ITypeLib pTypeLib, полное имя библиотеки типов pszName и имя папки pszHelpDir, в которой находится справочный файл для регистрируемой библиотеки (если он отсутствует, данный параметр следует задать равным NULL). Эта функция может вернуть одно из следующих значений:
Заметим, что, несмотря на возможность регистрации библиотеки типов при ее загрузке (посредством задания флагов REGKIND_REGISTER или REGKIND_DEFAULT), официальная документация рекомендует выполнять эти операции раздельно:
При деинсталляции сервера необходимо удалить информацию о библиотеке типов из системного реестра с помощью функции HRESULT UnRegisterTypeLib( REFGUID libid, unsigned short wVerMajor, unsigned short wVerMinor, LCID idLocale, SYSKIND sysKind ); Через параметр libid передается идентификатор библиотеки, через параметры wVerMajor и wVerMinor — номер ее версии. Параметр idLocale задает так называемый идентификатор территории (locale identifier); как показывает опыт, наименее «рискованным» является значение
И наконец через параметр sysKind передается идентификатор платформы; под Win32 его необходимо задавать равным SYS_WIN32. Функция возвращает те же значения, что и RegisterTypeLib. Откомпилированная библиотека типов может быть включена в ресурсы исполняемого файла сервера; в этом случае мы избавляемся от необходимости распространения вместе с ним отдельного .tlb файла. Для этого в среде Microsoft Visual C++ необходимо выбрать команду Resource… меню Insert и в появившемся диалоговом окне Insert Resource нажать на кнопочку Import…; откроется диалоговое окно Import Resource, где необходимо в поле ввода File name указать имя .tlb файла, в списке Open as выбрать пункт Custom и нажать на кнопочку Import; в появившемся после этого диалоговом окне Custom Resource Type нужно ввести строку «TYPELIB» (без кавычек) в поле ввода Resource type и нажать кнопку OK. После этого, в свойствах добавленного ресурса необходимо назначить ему идентификатор, равный 1. При «ручном» редактировании ресурсов процедура существенно упрощается — достаточно вписать в .rc файл строку вида 1 TYPELIB "имя_файла.tlb" При загрузке и регистрации библиотеки типов, находящейся в ресурсах исполняемого файла сервера, в качестве ее имени указывается имя файла самого сервера; если значение идентификатора отличается от 1, то его также необходимо указывать в имени сервера, добавляя дополнительный символ «\»:
Поскольку автономный сервер, в отличие от внутрипроцессного, не экспортирует функций DllRegisterServer и DllUnregisterServer, внесение информации в системный реестр и удаление ее из него должно выполняться либо в процессе инсталляции, либо в функции WinMain (по ключам командной строки). Особенности реализации фабрики классаВ автономном COM-сервере фабрика каждого класса должна создаваться в единственном экземпляре в начале его работы (в функции WinMain). После создания фабрики ее необходимо зарегистрировать в COM-библиотеке вызовом функции STDAPI CoRegisterClassObject( REFCLSID clsid, IUnknown* pUnk, DWORD fdwContext, DWORD fdwFlags, DWORD* pdwFactID ); Через параметр clsid ей передается идентификатор COM-класса, для создания экземпляров которого предназначена регистрируемая фабрика. Параметр pUnk должен содержать указатель на интерфейс IClassFactory регистрируемой фабрики. Параметр fdwContext используется для указания типа COM-сервера и формируется как комбинация флагов CLSCTX_xxx; заметим, что при указании флага CLSCTX_LOCAL_SERVER COM-библиотека автоматически добавляет флаг CLSCTX_INPROC_SERVER, позволяющий создавать COM-объекты не только приложениям-клиентам, но и самому серверу. Параметр fdwFlags определяет порядок использования сервера и может быть комбинацией следующих флагов:
Естественно, первые два флага являются взаимоисключающими. Функция записывает в переменную по адресу pdwFactID числовой идентификатор зарегистрированной фабрики класса, который используется для отмены регистрации, и возвращает одно из следующих значений:
Перед завершением работы сервера необходимо отменить регистрацию фабрики класса в COM-библиотеке с помощью функции HRESULT CoRevokeClassObject( DWORD dwFactID ); передав ей через параметр dwFactID идентификатор фабрики, полученный при вызове CoRegisterClassObject. Данная функция возвращает одно из следующих значений:
Процесс разработкиВ качестве примера мы рассмотрим разработку простейшего автономного сервера, предоставляющего клиентам COM-класс CoFile, останавливаясь на отличиях реализации этого сервера от его внутрипроцессного «предшественника», процесс разработки которого описан в заключительной части предыдущей темы — Создание внутрипроцессного COM-сервера. Напомню, что задачей класса CoFile является инкапсуляция основных операции файлового ввода/вывода, которые разделены на три группы (инициализация, чтение/запись данных, получение информации), образующие соответственно интерфейсы IFileInit, IFileOperate и IFileStatus. Шаг 0: объявление и реализация вспомогательного класса, инкапсулирующего операции с «безопасными массивами».
Конструктор класса CSafeArray (строки 44…48) обнуляет идентификатор связанного с объектом «безопасного массива», хранящийся в поле m_psa; деструктор (строки 50…54) вызывает метод Destroy для уничтожения соответствующего массива. Методы Create (строки 56…84) и CreateVector (строки 86…114), предназначенные для создания соответственно произвольного и одномерного массива, оформлены как специализации для поддерживаемых классом типов данных — char и BSTR. Аналогичным образом можно реализовать специализированные варианты для остальных OLE-automation-совместимых типов. Метод Destroy (строки 116…133) вначале разблокирует массив, если это необходимо (строки 121…124), после чего уничтожает его (строка 125) и обнуляет идентификатор (строка 126). Методы Attach (строки 135…147) и Detach (строки 149…155) позволяют соответственно связывать экземпляр класса CSafeArray с уже существующим «безопасным массивом» и разрывать эту связь. Пример их использования рассматривается на шаге 4. Метод PutElement (строки 175…197) предназначен для записи в массив элемента; отметим что для типа данных BSTR реализован специализированный вариант (строки 181…185), поскольку в данном случае не требуется взятие адреса записываемого элемента. Определены также перегруженные варианты этого метода для записи элемента в одномерный массив (строки 187…197). Метод GetElement позволяет получить значение элемента как в массиве с произвольным количеством измерений (строки 199…203), так и в одномерном (строки 205…212). Методы PutData (строки 214…228) и GetData (строки 230…244) предназначены соответственно для записи и чтения данных в область памяти, где находятся элементы массива. Оставшиеся методы носят информационный характер. Метод GetDim (строки 246…250) позволяет узнать количество измерений массива, а методы GetLBound (строки 252…265) и GetUBound (строки 267…280) — соответственно нижнюю и верхнюю границы заданного измерения. Метод GetVartype (строки 282…295) возвращает идентификатор типа элементов массива. Шаг 1: генерация идентификаторов и связывание их с предоставляемыми интерфейсами.
Этот файл является практически точной копией своего «тезки», использовавшегося при разработке внутрипроцессного COM-сервера. Добавился уникальный идентификатор библиотеки типов (строки 7 и 21…22); директивы условной компиляции (строки 9 и 24) позволяют включать данный заголовок в исходные файлы, написанные как на C++, так и на ODL/IDL. Шаг 2: объявление предоставляемых интерфейсов.
Основное отличие объявленных интерфейсов от их предыдущего варианта заключается в том, что все они являются OLE-automation-совместимыми, обеспечивая, тем самым, возможность использования универсального маршалинга. Второе, на что следует обратить внимание — это применение макросов STDMETHOD и PURE при объявлении методов интерфейсов (строки 5…6, 11…15 и 20). Оба этих макроса определены в заголовочных файлах <basetyps.h> и <objbase.h> и предназначены для повышения переносимости COM-программ на уровне исходного текста и некоторого сокращения объема этого текста. Макрос STDMETHOD имеет «прототип» STDMETHOD(имя_метода)
и после обработки препроцессором C++ раскрывается в virtual HRESULT __stdcall имя_метода а макрос PURE заменяется препроцессором на строку «= 0», делающую объявляемый метод чисто-виртуальным. Шаг 3: объявление класса, реализующего предоставляемые интерфейсы.
Как и в предыдущем, «внутрипроцессном» варианте, используется множественное наследование COM-класса от всех реализуемых им интерфейсов (строка 3); класс CoFileFactory объявлен дружественным (строка 5) для предоставления ему доступа к закрытому полю m_nNumObjs (строка 27), в котором хранится количество созданных сервером экземпляров класса CoFile. Отметим, что в объявлениях методов класса, помимо STDMETHOD, используется еще один «стандартный» макрос COM, определение которого также содержится в заголовочных файлах <basetyps.h> и <objbase.h>. Макрос STDMETHOD_ (строки 11…12) имеет «прототип» STDMETHOD_(тип_возвр_знач, имя_метода)
и предназначен для объявления методов, тип возвращаемого значения которых отличается от HRESULT; после обработки препроцессором C++ он раскрывается в virtual тип_возвр_знач __stdcall имя_метода Шаг 4: реализация методов COM-класса.
Конструктор класса CoFile (строки 3…8) не претерпел никаких изменений, а вот реализация деструктора (строки 10…16) имеет существенные отличия. Поскольку автономный COM-сервер является обычным Windows-приложением и должен оставаться в памяти до тех пор, пока существует хотя бы один созданный им COM-объект, в нем организуется стандартный цикл обработки сообщений. После уничтожения всех объектов и полного разблокирования сервера (строка 12) необходимо вызвать функцию PostQuitMessage (строка 14) для прерывания цикла обработки сообщений, завершения функции WinMain и выгрузки сервера из памяти. Методы интерфейса IUnknown (строки 18…57) реализованы стандартным образом; большинство методов, выполняющих «содержательную» часть работы класса CoFile, отличаются от предыдущего варианта только использованием параметров, имеющих OLE-automation-совместимые типы. Исключение составляют только методы Read (строки 79…96) и Write (строки 98…115), использующие для работы с передаваемым им «безопасным массивом» шаблонный класс CSafeArray, разработанный нами на шаге 0. В методе Read, выполняющем чтение данных из файла в заданный буфер, создается экземпляр класса CSafeArray (строка 83), после чего созданный объект связывается с полученным указателем на буфер при помощи метода Attach (строка 84). Далее мы определяем емкость буфера как разницу между его верхней и нижней границами (строка 85) и блокируем массив, получая указатель на область памяти, в которую следует скопировать прочитанные данные (строка 86). После чтения данных из файла (строка 87) выполняется разблокирование массива (строка 88) и «отвязывание» от него объекта CSafeArray вызовом метода Detach (строка 89). Аналогичным образом построен и метод Write. Отметим использование в заголовках всех реализованных методов еще двух «стандартных» макросов COM — STDMETHODIMP и STDMETHODIMP_, объявления которых содержатся в заголовочных файлах <basetyps.h> и <winnt.h>. Первый из них является «дополнением» макроса STDMETHOD и заменяется препроцессором C++ на HRESULT __stdcall
а второй имеет «прототип» STDMETHODIMP_(тип_возвр_знач)
и раскрывается в тип_возвр_знач __stdcall
«дополняя», таким образом, макрос STDMETHOD_, используемый при объявлении метода интерфейса. Шаг 5: объявление соответствующей фабрики класса.
Поскольку в автономном сервере фабрика каждого класса существует в единственном экземпляре, создаваемом в начале работы этого сервера, у класса CoFileFactory, унаследованного от реализуемого им интерфейса IClassFactory (строка 3), отсутствует поле для хранения количества созданных объектов. Класс CoFile объявлен дружественным (строка 5) для предоставления ему доступа к закрытому полю m_nNumLocks, выполняющему роль счетчика блокировок сервера приложением-клиентом. Шаг 6: реализация фабрики класса.
Конструктор (строки 3…5) и деструктор (строки 7…9) присутствуют только из формальных соображений и не выполняют никаких действий. Реализация метода QueryInterface (строки 11…28) также не блещет оригинальностью, а вот методы AddRef (строки 30…33) и Release (строки 35…38) выглядят несколько непривычно — каждый из них просто возвращает значение 1, поскольку счетчик ссылок у данного класса отсутствует за ненадобностью. Вторым существенным отличием от «внутрипроцессного» варианта является реализация метода LockServer (строки 59…70): в случае, когда сервер полностью разблокирован и все созданные экземпляры класса CoFile уничтожены (строка 65), вызывается функция PostQuitMessage (строка 67), которая прерывает цикл обработки сообщений, что приводит к завершению функции WinMain и выгрузке сервера из памяти (напомним, что аналогичные действия выполняются в деструкторе класса CoFile, рассмотренном на шаге 4). Шаг 7: описание библиотеки типов.
Библиотека типов имеет имя «FileServer» (строка 6), ей назначены идентификатор и номер версии (строка 5); кроме того, в ней доступны все объявления из библиотеки типов stdole32.tlb (строка 8). Интерфейс IFileInit (строки 10…15) объявлен как потомок IUnknown (строка 11); он относится к COM, имеет обязательный идентификатор и является OLE-automation-совместимым (строка 10). Аналогичным образом выполнены объявления интерфейсов IFileOperate (строки 17…25) и IFileStatus (строки 27…31). COM-классу CoFile (строки 33…39), реализующему перечисленные интерфейсы (строки 36…38), назначены обязательный идентификатор и номер версии (строка 33). Шаг 8: Реализация точки входа COM-сервера.
В самом начале своей работы сервер инициализирует COM-библиотеку вызовом функции CoInitialize (строка 14); дальнейшие действия зависят от содержания командной строки, указанной при его запуске. При запуске с ключом «/Register» сервер определяет свое полное имя (строка 22), после чего регистрирует в системном реестре класс CoFile (строки 23…29) и библиотеку типов (строки 30…41), находящуюся в его ресурсах. При запуске с ключом «/Unregister» (строка 44) выполняются обратные действия: из системного реестра удаляется информация о библиотеке типов (строки 46…47) и классе CoFile (строки 48…50). И наконец, если в командной строке отсутствует слово «Embedding» (строка 52), то создается экземпляр фабрики класса (строка 54), который регистрируется в COM-библиотеке (строка 55), после чего создается цикл обработки сообщений (строки 56…60). После того, как этот цикл будет прерван разблокированием сервера и уничтожением всех созданных им COM-объектов, из COM-библиотеки удаляется информация о фабрике класса (строка 61), которая затем уничтожается навеки (строка 62). Завершая свое выполнение, сервер «расстается» с COM-библиотекой, вызывая функцию CoUninitialize (строка 66). | ||||||||||||||||||||||||||||||||||||||