Создание внутрипроцессного COM-сервера |
Общие замечанияДанный тип COM-сервера представляет собой обычную DLL-библиотеку, в которой реализован как минимум один COM-класс и несколько специальных функций, экспортируемых этой DLL и используемых COM-библиотекой. Для оптимизации создания однотипных COM-объектов приложением-клиентом, в дополнение к каждому COM-классу может быть реализована так называемая фабрика класса (class factory; широко применяется также довольно неудачный термин class object). Фабрика класса представляет собой специальный COM-класс, реализующий интерфейс IClassFactory. Заметим, что фабрике класса никогда не назначается собственный GUID. Интерфейс IClassFactory
Ниже перечислены методы интерфейса IClassFactory. HRESULT CreateInstance( IUnknown* pUnk, REFIID iid, void** ppvDest ); Через параметр pUnk в этот метод передается указатель на интерфейс IUnknown объекта-агрегата; этот указатель может быть равен NULL, если поддержка агрегации не была затребована приложением-клиентом. Через параметр iid передается идентификатор интерфейса, реализуемого COM-классом, с которым связана данная фабрика. Метод должен создать соответствующий COM-объект, запросить у него указатель на интерфейс iid и записать полученный указатель в переменную по адресу ppvDest. Возвращать необходимо одно из следующих значений:
HRESULT LockServer( BOOL fLock ); Через переметр fLock в этот метод передается TRUE, если приложение-клиент пытается блокировать COM-сервер в памяти, или FALSE — если оно пытается его разблокировать. В реализации метода необходимо соответствующим образом, в зависимости от значения параметра fLock, изменять значение счетчика блокировок сервера, который обычно реализуется как глобальная целочисленная переменная. Метод должен возвращать одно из следующих значений:
Заметим, что COM-сервер не должен позволять выгружать себя из памяти, если значение счетчика блокировок больше нуля. Создание COM-объектаДля создания COM-объекта при помощи фабрики класса приложение-клиент должно вначале вызвать функцию HRESULT CoGetClassObject( REFCLSID clsid, DWORD fdwContext, COSERVERINFO* pInfo, REFIID iid, void** ppvDest ); Через параметр clsid передается идентификатор COM-класса, объект-экземпляр которого необходимо создать. Параметр fdwContext должен быть комбинацией флагов CLSCTX_xxx, определяющих, какие виды COM-серверов необходимо задействовать. Через параметр pInfo необходимо передать адрес структуры, определяющей местоположение COM-сервера; для внутрипроцессных серверов, всегда выполняющихся на том же компьютере, что и приложение-клиент, этот параметр задается равным NULL. Через параметр iid передается идентификатор реализованного фабрикой класса стандартного интерфейса; как правило, это интерфейс IClassFactory, имеющий идентификатор IID_IClassFactory (фабрика класса может также реализовывать интерфейс IClassFactory2, являющийся расширением IClassFactory и позволяющий блокировать выполнение COM-сервера на компьютере, не имеющем соответствующего лицензионного ключа). Функция записывает указатель на полученный интерфейс в переменную по адресу ppvDest и возвращает одно из следующих значений:
После получения указателя на интерфейс IClassFactory можно воспользоваться его методом CreateInstance для создания соответствующего COM–объекта и получения указателя на один реализуемых им интерфейсов. Ниже приведен пример фрагмента кода условного приложения-клиента.
Экспортируемые функцииВнутрипроцессный COM-сервер должен экспортировать следующие четыре функции, вызываемые COM-библиотекой в процессе взаимодействия приложения-клиента с этим сервером. STDAPI DllGetClassObject( REFCLSID clsid, REFIID iid, void** ppvDest ); Реализация этой функции должна создавать экземпляр фабрики класса, имеющего идентификатор clsid, получать у этого экземпляра указатель на интерфейс с идентификатором iid, записывать полученный указатель в переменную по адресу ppvDest и возвращать одно из следующих значений:
Таким образом, при создании COM-объекта с использованием фабрики класса выполняются следующие действия:
STDAPI DllCanUnloadNow(void);
Именно в реализации этой функции должно анализироваться значение счетчика блокировок сервера в памяти, а также количество существующих в данный момент COM-объектов, созданных им. Если обе эти величины равны нулю, функция должна возвращать значение S_OK, показывающее COM-библиотеке, что сервер может быть выгружен из памяти. В противном случае, необходимо возвращать значение S_FALSE, запрещающее выгружать сервер из памяти. Наличие следующих двух функций не является обязательным, но каждый уважающий себя COM-сервер их реализует. STDAPI DllRegisterServer(void);
Реализация этой функции должна записывать в системный реестр информацию, необходимую для корректного функционирования COM-сервера, и возвращать одно из следующих значений:
STDAPI DllUnregisterServer(void);
Реализация этой функции должна удалять из системного реестра информацию, записанную туда функцией DllRegisterServer и возвращать одно из следующих значений:
Информация в системном реестреКаждый COM-класс, помимо числового идентификатора в формате GUID, должен иметь так называемый программный идентификатор (programmatic identifier, PROGID). Этот идентификатор должен быть строкой, длиной не более 39 символов, начинающейся с буквы, состоящей из букв и цифр и не содержащей никаких других символов, кроме точек. Как правило, этот идентификатор имеет вид vendor.component.version Здесь vendor — это название фирмы-разработчика COM-класса (иногда название продукта, частью которого он является), component — строковое имя COM-класса, а version — номер версии данного класса. Таким образом, получается что-то вроде SuperCorp.SuperCoClass.1 (если Вы являетесь программистом-индивидуалом, то в качестве названия фирмы можете указать свое имя). Получить программный идентификатор COM-класса, зная его GUID, можно с помощью функции WINOLEAPI ProgIDFromCLSID( REFCLSID clsid, LPOLESTR* ppszDest ); Через параметр clsid передается идентификатор COM-класса, а через параметр ppszDest — адрес переменной, в которую будет записан указатель на строку, содержащую PROGID. Память под эту строку выделяется COM-библиотекой и приложение-клиент должно освободить ее, используя метод Free интерфейса IMalloc. Функция возвращает одно из следующих значений:
Обратная задача (получение GUID'а COM-класса по его программному идентификатору) решается с помощью функции WINOLEAPI CLSIDFromProgID( LPCOLESTR pszProgID CLSID* pDest, ); Через параметр pszProgID передается адрес строки, содержащей программный идентификатор, а через pDest — адрес переменной, в которую будет скопирован идентификатор COM-класса. Функция может вернуть одно из следующих значений:
Для обеспечения корректной работы COM-библиотеки (в частности, двух перечисленных выше функций) необходимо, чтобы в системном реестре каждому COM-классу соответствовала информация приведенной ниже структуры. 1. Должен существовать ключ вида HKEY_CLASSES_ROOT\CLSID\{идентификатор_класса} содержащий в качестве значения по умолчанию (безымянного значения) строку вида программный_идентификатор 2. Должен существовать ключ вида HKEY_CLASSES_ROOT\CLSID\{идентификатор_класса}\InprocServer32 содержащий в качестве значения по умолчанию строку с полным именем исполняемого файла сервера. 3. Должен существовать ключ вида HKEY_CLASSES_ROOT\программный_идентификатор\CLSID содержащий в качестве значения по умолчанию строку вида {идентификатор_класса} Заметим, что при записи в системный реестр идентификатор_класса должен указываться в строковом представлении (соответствующий вариант в утилите guidgen.exe так и называется — Registry Format); сама же запись «значений по умолчанию» выполняется с помощью функции Win32 API RegSetValue. Вся перечисленная информация должна записываться в системный реестр функцией DllRegisterServer и удаляться из него функцией DllUnregisterServer; в этом случае, для «ручной» регистрации COM–сервера можно будет воспользоваться командой вида regsvr32[.exe] имя_файла_сервера а для «разрегистрации» (unregistration) — командой вида regsvr32[.exe] /u имя_файла_сервера Процесс разработкиВ качестве примера мы рассмотрим процесс написания простейшего COM-сервера, предоставляющего клиентам всего один COM-класс — CoFile, который инкапсулирует базовые операции файлового ввода/вывода. Эти операции разделены на три группы (инициализация, чтение/запись данных и получение информации), образующие соответственно интерфейсы IFileInit, IFileOperate и IFileStatus. Шаг 1: генерация идентификаторов и связывание их с предоставляемыми интерфейсами.
Для генерации идентификаторов была использована уже упоминавшаяся утилита guidgen.exe, которая поставляется вместе с Microsoft Visual Studio (исходные тексты этой утилиты входят в число примеров, прилагающихся к MSDN), причем в качестве формата был выбран последний из предлагаемых ей вариантов — Registry Format. Связывание сгенерированных идентификаторов с предоставляемыми интерфейсами (строки 8, 10, 12) и COM-классом (строка 15) выполнено с помощью описателя класса памяти (storage-class specifier) вида __declspec(uuid("идентификатор")) который может использоваться в объявлении структур и классов C++. Этот «описатель» является расширением языка C++, которое поддерживается всеми последними версиями компиляторов от Borland и Microsoft. Заметим, что достаточно однократного употребления этого описателя (например, в опережающем объявлении класса или структуры, как это сделано в рассматриваемом исходном тексте); в дальнейших объявлениях он может быть опущен, хотя его повторение и не воспринимается компилятором как синтаксическая ошибка. Для «извлечения» из класса или структуры связанного с ней подобным образом идентификатора используется оператор __uuidof(выражение)
где выражение может быть либо именем типа, либо указателем или ссылкой на переменную соответствующего типа. С целью соблюдения единообразия символических имен идентификаторов интерфейсов и COM-классов в исходном тексте объявлены четыре макроподстановки (строки 9, 11, 13 и 16). Шаг 2: объявление предоставляемых интерфейсов.
Все предоставляемые интерфейсы (строки 3…19) объявлены обычным для C++ образом — как структуры, наследуемые от IUnknown, и содержащие только чисто-виртуальные методы. Заметим, что все методы интерфейсов должны использовать соглашение о вызовах __stdcall (by design of COM). Шаг 3: объявление класса, реализующего предоставляемые интерфейсы.
Для реализации COM-класса, являющегося, с формальной точки зрения, обычным классом C++, использовано множественное наследование от всех предоставляемых интерфейсов (строка 3); заметим, что указание интерфейса IUnknown в качестве предка не требуется, поскольку все интерфейсы уже являются его потомками. Функция DllCanUnloadNow объявлена дружественной (строка 5) для получения доступа к закрытому полю m_nNumObjs (строка 25), в котором хранится количество созданных сервером COM-объектов — экземпляров данного класса. Класс CoFile содержит все методы всех предоставляемых им интерфейсов (включая методы IUnknown). Поле m_nNumRefs (строка 23) используется для реализации счетчика ссылок, а m_fp (строка 24) — для хранения идентификатора файла. Шаг 4: реализация методов COM-класса.
Конструктор класса CoFile (строки 3…8) обнуляет счетчик ссылок на созданный объект и увеличивает на единицу значение количества созданных объектов; в деструкторе (строки 10…13) это значение соответствующим образом уменьшается. Реализация метода QueryInterface (строки 15…40) проверяет значение, переданное через параметр iid, и если оно совпадает с идентификатором одного из реализуемых интерфейсов — записывает требуемый указатель по адресу ppvDest, выполняя соответствующее приведение типа. Заметим, что для предоставленного интерфейса вызывается метод AddRef (строка 38). Если же запрошенный интерфейс не реализуется данным COM-классом, метод обнуляет указатель, находящийся по адресу ppvDest и возвращает значение E_NOINTERFACE (ну нету такого интерфейса). Реализация метода AddRef (строки 42…45) очень проста — она увеличивает на единицу значение счетчика ссылок и возвращает его. В методе Release (строки 47…54) это значение, наоборот, уменьшается и если оно оказывается равным нулю — объект уничтожается за ненадобностью (строка 51). Остальные методы класса CoFile (строки 56…94) реализуют «содержательную часть» и позволяют выполнять с помощью таких объектов базовые операции файлового ввода/вывода. По сути, все эти методы являются «тонкими» оболочками функций стандартной библиотеки C. Шаг 5: объявление соответствующей фабрики класса.
Фабрика класса выполнена в виде отдельного класса C++, унаследованного от интерфейса IClassFactory, и реализующего его методы (строки 14…15), а также методы IUnknown (строки 10…12), который является предком этого интерфейса. Еще раз подчеркнем, что фабрике класса не назначается собственный GUID. Функция DllCanUnloadNow объявлена дружественной (строка 5) для получения доступа к закрытому полю m_nNumObjs (строка 19), в котором хранится количество созданных сервером объектов — экземпляров данного класса. В поле m_nNumRefs (строка 17) хранится текущее значение счетчика ссылок, а статическое поле m_nNumLocks (строка 18) предназначено для подсчета количества блокировок COM-сервера, выполняемых с помощью метода LockServer. Шаг 6: реализация фабрики класса.
Конструктор класса CoFileFactory (строки 3…7) обнуляет счетчик ссылок на созданный объект и увеличивает на единицу значение количества созданных объектов; в деструкторе (строки 9…12) это значение соответствующим образом уменьшается. Реализация методов IUnknown (строки 14…45) стандартна и по смыслу полностью совпадает с тем, что было описано выше для класса CoFile. Метод CreateInstance (строки 47…64) вначале проверяет, не пытается ли приложение-клиент создать экземпляр класса CoFile как составляющую часть объекта-агрегата (строка 49) и если это так — возвращает значение CLASS_E_NOAGGREGATION, показывающее, что наш класс агрегацию не поддерживает. В противном случае, создается экземпляр класса (строка 56) и у него запрашивается указатель на интерфейс с идентификатором iid (строка 57). Если при получении этого указателя происходит ошибка (например, класс не реализует такого интерфейса), созданный объект тут же уничтожается (строка 60). В рализации метода LockServer (строки 66…70) выполняется изменение счетчика блокировок сервера в соответствии со значением параметра fLock. Шаг 7: реализация экспортируемых функций и точки входа COM-сервера.
Реализация функции DllGetClassObject (строки 7…23) проверяет значение, переданное через параметр clsid, и если оно совпадает с идентификатором реализуемого данным сервером COM-класса — создается экземпляр фабрики этого класса (строка 11) и у него запрашивается указатель на интерфейс с идентификатором iid (строка 12). Если при получении этого указателя происходит ошибка, созданная фабрика класса уничтожается (строка 15). Вызов данной функции с идентификатором класса, отличным от CLSID_CoFile, приводит к ее завершению со значением CLASS_E_CLASSNOTAVAILABLE (класс абсолютно недосягаем). Фукнция DllCanUnloadNow (строки 25…29) анализирует количество существующих в данный момент объектов (экземпляров фабрики класса и самого CoFile), а также значение счетчика блокировок сервера. Если сервер не блокирован и живых объектов нет, выгрузка сервера разрешается; в противном случае он остается висеть в памяти, отнимая у системы и без того скудные ресурсы. Функция DllRegisterServer (строка 31…49) формирует и записывает в системный реестр информацию, необходимую для корректного взаимодействия COM-библиотеки и приложения-клиента с данным сервером, а функция DllUnregisterServer (строки 51…60) удаляет из реестра эту информацию. Единственное назначение точки входа DllMain (строки 62…69) состоит в том, чтобы записывать дескриптор модуля в глобальную переменную g_hInstance при его загрузке в адресное пространство очередного процесса (строки 64…67). Шаг 8: экспорт функций через файл определения модуля.
Компоновщик Microsoft требует, чтобы четыре специальные функции, экспортируемые COM-сервером, не попадали в автоматически создаваемую им библиотеку импорта, поэтому их необходимо экспортировать через так называемый файл определения модуля (module definition file). В разделе EXPORTS этого файла необходимо явно задать произвольные номера экспортируемых функций, предваряя их символом «@», и указать атрибут PRIVATE (строки 7…10). ЗаключениеПолный текст примера, включающий в себя тестовое приложение-клиент, можно скачать по ссылке, находящейся в конце этой страницы. Заметим, что здесь реализован минимально возможный COM-сревер, обращение к которому из приложений, написанных на других языках программирования (особенно Visual Basic), очень затруднительно. В следующих темах данного раздела мы постараемся избавиться от этого недостатка. | ||||||||||||||||||||