Предыдущая Оглавление Следующая

COM-сервер

На данном этапе мы рассматриваем самый простой случай компонентного взаимодействия - вызов объекта из сервера, который реализован в виде DLL. Существуют значительно более сложные случаи - например, вызов объекта из сервера, который реализован в виде EXE, причём этот EXE может исполняться не только на той же машине, где исполняется клиент. Я думаю, что нам плавно не перейти к рассмотрению таких случаев взаимодействия компонент без философских знаний, полученных в результате обобщения пройденного пути по написанию DLL сервера. Поэтому сегодня мы приступим к рассмотрению конструкции того, что есть COM-сервер.

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

Начнём с назначения COM-сервера. В самом примитивном интуитивном понимании это - обиталище двоичных объектов. Т.е. для каждого COM-объекта существует хотя бы один реализующий его сервер, и, наоборот, каждый COM-сервер реализует хотя бы один COM-объект. Это, правда, не совсем точное утверждение - сервер реализует не объекты, а - статические типы, но до некоторых пор мы этой разницей вполне можем пренебречь.

Но это - не вся сущность COM-сервера. Сервер - это единый модуль. То, что сервер показывает наружу отдельные объекты вовсе не означает, что объекты эти внутри сервера обязательно изолированы. Как раз напротив - только в очень примитивных случаях удаётся свести функциональность программы к одному объекту с одним интерфейсом. Или к совокупности самостоятельных объектов. Во всех остальных случаях разные объекты, экспонируемые сервером, обычно связаны внутри - по данным или по совместному управлению. Т.е. сервер как раз и есть та "программа", которая обслуживает потребности пользователя. А вот то, что она показывает "объекты с интерфейсами" так это не более, чем артефакты способа доступа к этой функциональности "снаружи программы", из клиента.

Из этого должно быть понятно, что COM-сервер имеет, как минимум, две взаимно проникающие архитектуры. Одна - архитектура, которая связана с собственно целевым назначением этого сервера. Будь соответствующая программа реализована и не в виде COM-сервера эта архитектура всё равно определяла бы её конструкцию. Вторая - архитектура, связанная с тем, что у нас именно COM-сервер. Т.е. эта архитектура определяет каким образом реализуются те самые "объекты с интерфейсами", независимо от того, какие "полезные действия" они выполняют.

Эти архитектуры часто и путают и забывают о существовании какой-то одной. Например, программисты "в COM" не видят, что COM - только способ взаимодействия, оболочка. Что "полезные действия" - это не COM. И поэтому спроектировать бездарную по своей архитектуре программу COM нисколько не мешает, а даже - и способствует, потому, что в проекте появляется "лишний код". И просто приделыванием COM-интерфейсов к уже существующей программе её архитектуру тоже не улучшить.

Критики с другого края, напротив, не видят, что COM это тоже архитектура, предназначенная для решения специфических задач взаимодействия программ. Эти считают, что раз "полезная архитектура" построена, раз функциональность работает, то всё остальное - от лукавого. В самом деле, если все функции программы могут быть вызваны либо как функции экспортируемые DLL, либо как экспортируемые ею классы - чего огород-то городить? (Когда мы будем обобщать то, что мы изучили по данной теме в "соответствующую философию", мы очень убедительно ответим на этот вопрос, а пока просто отметим себе, что такой взгляд есть и он мешает видеть явление в целости).

И это - действительно взаимно проникающие архитектуры. Выполняя проектное конструирование "полезной функциональности" нельзя не учитывать требований, диктуемых COM. И наоборот - реализуя COM-функциональность её всё время приходится связывать с функциональностью "полезной", чтобы программа работала как единое целое.

Мы обязательно доберемся до этой, очень интересной, темы и всесторонне рассмотрим её во множестве аспектов, но сейчас предмет нашего рассмотрения значительно скромнее - формат модуля, который гордо именуется "COM-сервер".

Здесь нужно сказать, что канонически известно два типа COM-серверов - загружаемые в адресное пространство клиентского процесса, и работающие в адресном пространстве другого процесса. Последний тип сервера также рассматривается в двух разных качествах - работающий в параллельном процессе на той же самой машине (в одном и том же экземпляре операционной системы) и работающий на другой машине (предмет, реализация которого некогда называлась DCOM - DistributedCOM). Такая классификация имеет под собой смысл, поскольку построена на сложности преодоления границы между клиентом и сервером. Самый простой случай, когда граница фактически отсутствует - реализация сервера в виде DLL. Такой сервер называется inproc-server, при его использовании от системы в большинстве случаев требуется только его запуск. Именно его конструкцию мы и будем рассматривать подробно. Другие типы серверов (а фактически - типы клиент-серверного взаимодействия) требуют значительно бОльших усилий операционной системы по обслуживанию вызова клиентом метода сервера и, к сожалению, значительно бОльших знаний всего вместе о COM тоже.

Ранее мы обнаружили, что inproc-сервер не может совсем ничего не экспортировать. Хотя бы одну функцию (мы назвали её DllGetClassObject) сервер экспортировать должен. И связано это с системной спецификацией доступа к DLL. Затем, когда программа-клиент уже "пришла в соединение с сервером и получила ссылку на объект" клиент и сервер могут взаимодействовать и без посредничества механизма экспортируемых функций.

Но, фактически, перед сервером в целом встают те же проблемы, что вставали и перед отдельным объектом. Например, система должна знать месторасположение этого сервера и список статических типов, реализуемых этим сервером. Система должна уметь загружать этот сервер и выгружать, когда надобность в нём минет. В реализации COM-объекта такая функциональность оформлялась методами объекта. В реализации всей DLL эта функциональность оформляется экспортируемыми функциями. Природа этого явления - в такой же степени философская, в какой философская сама природа интерфейса IUnknown. И точно так же, как в интерфейс IUnknown попали только три, но уж совершенно фундаментальных, метода, так же и inproc-сервер должен экспортировать совсем немного "фундаментальных функций".

С чем они связаны? Одна уже известна - она связана с получением ссылок на объекты, реализуемые сервером. Две других связаны с регистрацией сервера в реестре и удалением регистрационной информации из реестра. Имя сервера клиенту знать необязательно. Клиенту достаточно знать GUID объекта, а система, просматривая системный реестр, в состоянии выяснить какой модуль для получения этого объекта надо запустить. Только вот информация эта в реестр как-то должна попадать. И одной из функций, возложенных на сам COM-сервер как раз и является внесение информации о реализуемых им объектах в системный реестр. Разумеется, раз есть инсталляция, то должна быть и деинсталляция - чтобы не вводить систему в заблуждение, при деинсталляции сервер должен удалить информацию о реализуемых им объектах. Это - очень разумное решение, поскольку сам сервер о себе уж точно всё знает. И как либо иначе побудить сервер зарегистрировать себя способа нет - пока сервер не зарегистрирован система не знает о нём ничего. Поэтому способ "вызвать специальную экспортируемую функцию DLL" для этого - единственное возможное решение.

А вот наличие четвёртой функции - неочевидно. Хотя её наличие совершенно необходимо, если немного подробнее представить себе взаимодействие "клиент - система - сервер". Итак, клиент обращается к системе. Система в реестре отыскивает путь к модулю сервера. Система выполняет функцию LoadLibrary и загружает DLL в процесс. Далее система обращается к функции DllGetClassObject, получает указатель и передаёт его клиенту. Клиент, получив указатель, начинает им распоряжаться совершенно самостоятельно, без вмешательства операционной системы... Всё хорошо, но! Когда клиент освободит последнюю ссылку на последний объект этого сервера это ведь будет означать, что и сервер больше не нужен и система может его выгрузить, освободив занятые им ресурсы? А как системе об этом узнать? Ведь в отличие от объекта, вызывающего delete this, сервер как раз не может в отношении самого себя вызвать FreeLibrary! Поэтому четвёртая функция сообщает системе, может ли сервер быть выгружен из памяти в данный момент. Это тоже разумно - если есть возможность сервер загружать только по требованию клиента, то ведь можно и выгружать его "при отсутствии требований". Но выгрузить его "при живых объектах" нельзя, а сам клиент сервер не загружает. Сервер загружает система. Поэтому и выгружать сервер должна система, а сообщать о том, может ли сервер быть выгружен, вынужден сам сервер. Что он и делает, реализуя данную функцию - система вызывает её и, если функция возвращает "да", - выгружает такой сервер.

Вот эти четыре функции, которые называются:

DllGetClassObject

DllRegisterServer

DllUnregisterServer

DllCanUnloadNow

как раз и есть предмет нашего дальнейшего изложения по теме. Одну из них мы уже рассмотрели ранее, её конструкция понятна.

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

Главным, с точки зрения разыскания сервера, ответственного за реализацию статического типа обозначенного указываемым клиентом CLSID, является раздел системного реестра HKEY_CLASSES_ROOT\CLSID, в котором имеются параметры под именами соответствующих CLSID, подпараметры которых описывают характеристики запуска соответствующего сервера. Пример такого параметра применительно к нынешнему случаю он будет выглядеть так:

HKCR\CLSID\{CLSID нашего статического типа} = <"человеческое имя" типа>

и у этого параметра имеется, как минимум, вложенный параметр - InprocServer32, значением которого является спецификация полного пути, где располагается модуль сервера.

Поэтому minimum minimorum информации о статическом COM-типе, который нужно вписать в реестр, если клиент собирается "поднимать" объект только по имени его CLSID и только если объект реализуется inproc-сервером на данной же машине:

HKCR\CLSID\{CLSID нашего статического типа} = ""
HKCR\CLSID\{CLSID нашего статического типа}\InprocServer32 = <спецификация полного пути в файловой системе>

В принципе, этого достаточно, чтобы вызвав CoCreateInstance получить ссылку на запрашиваемый интерфейс объекта данного статического типа. Но, в реальной жизни, так бывает не всегда. Мало кто желает иметь дело с GUID в его "натуральном выражении" - информативности у GUID нет никакой по определению самого же GUID. Поэтому с объектом обычно (но обязательно этого не требуется, просто часть сопутствующих сервисов будет неработоспособна) связывается еще и "человеческое имя" - обозначение из нескольких символьных идентификаторов, разделённых точками, структура которого устанавливается следующей:

<название приложения>.<имя статического типа>.<версия>.<подверсия>...<...>

например: Word.Document.6

Структурированность имени позволяет выяснить отношения преемственности между серверами, компонентами и версиями - другой, тоже возможный здесь же, параметр реестра VersionIndependentProgID имеет своим значением только <название приложения>.<имя статического типа>, что позволяет в случае необходимости разыскивать либо "общеизвестную" версию компонента, либо - точно заданную. Впрочем, если клиент знает точный GUID компонента, этой проблемы не существует вовсе - он просто адресует GUID, а GUID всех других версий этого же компонента должны быть уже другими.

Нужно специально отметить: и название приложения и имя статического типа - имена нарицательные. Они не связаны ни с именем файла сервера, ни с именем класса компонента. Например, все версии Microsoft Word имеют одно нарицательное имя Word.

Поэтому более полная информация о статическом типе будет содержать ещё и параметр ProgID:

HKCR\CLSID\{CLSID нашего статического типа}\ProgID = <"человеческое имя">

а уж совсем продвинутая еще и параметр:

HKCR\CLSID\{CLSID нашего статического типа}\VersionIndependentProgID = <"короткое имя">

К последовательности идентификаторов, являющихся значением ProgID и VersionIndependentProgID применимы следующие правила:

Но перечисленное - еще не всё. Описанный набор параметров задаёт связь "CLSID - ProgID", а существует и "зеркальный набор параметров реестра", который задаёт связь "ProgID - CLSID". Его можно отыскать прямо в разделе реестра HKEY_CLASSES_ROOT:

HKCR\<название приложения>.<имя статического типа> = <строковое описание>
HKCR\<название приложения>.<имя статического типа>.<версия> = <строковое описание>

Внутри этих параметров определен вложенный параметр CLSID, значением которого является GUID из раздела HKEY_CLASSES_ROOT\CLSID, вот так:

HKCR\<название приложения>.<имя статического типа> = <строковое описание>
HKCR\<название приложения>.<имя статического типа>\{CLSID нашего статического типа} = <GUID>
HKCR\<название приложения>.<имя статического типа>.<версия> = <строковое описание>
HKCR\<название приложения>.<имя статического типа>.<версия>\{CLSID нашего статического типа} = <GUID>

<>Здесь <строковое описание> - произвольная и необязательная строка символов, которая может содержать всё, что угодно. Она используется браузерами реестра для того, чтобы выдать откомментированное описание компонента, но для функционирования "собственно COM" никакого значения не имеет.

Существует практическое правило, по которому инсталлятор самой первой версии продукта записывает данные параметры в реестр следующим образом:

HKCR\<мой продукт>.<мой тип>.1 = "первая версия продукта"
HKCR\<мой продукт>.<мой тип>.1\{CLSID данного типа в версии 1} = <GUID>
HKCR\<мой продукт>.<мой тип> = "последняя известная системе версия продукта"
HKCR\<мой продукт>.<мой тип>\{CLSID данного типа в версии 1} = <GUID>

Если поверх первой версии данного программного продукта будет устанавливаться вторая его версия, то её инсталлятор запишет в реестр следующие строки:

HKCR\<мой продукт>.<мой тип>.2 = "вторая версия продукта"
HKCR\<мой продукт>.<мой тип>.2\{CLSID данного типа в версии 2} = <GUID>
HKCR\<мой продукт>.<мой тип> = "последняя известная системе версия продукта"
HKCR\<мой продукт>.<мой тип>\{CLSID данного типа в версии 2} = <GUID>

т.е. он перезапишет значения параметра HKCR\<мой продукт>.<мой тип> В результате, после работы обоих инсталляторов в реестре окажется совокупность записей:

HKCR\<мой продукт>.<мой тип>.1 = "первая версия продукта"
HKCR\<мой продукт>.<мой тип>.1\{CLSID данного типа в версии 1} = <GUID>
HKCR\<мой продукт>.<мой тип>.2 = "вторая версия продукта"
HKCR\<мой продукт>.<мой тип>.2\{CLSID данного типа в версии 2} = <GUID>
HKCR\<мой продукт>.<мой тип> = "последняя известная системе версия продукта"
HKCR\<мой продукт>.<мой тип>\{CLSID данного типа в версии 2} = <GUID>

Такое правило позволяет хранить в системе одновременно все версии компонентов данного статического типа и реализующих их серверов без взаимной интерференции, если, конечно, инсталлятор последующих версий намеренно не удаляет все предыдущие. Но, конечно, никто и не обязывает инсталлятор делать VersionIndependentProgID обязательно последней устанавливающейся версией. Например, если вы выпускаете beta-release вашего продукта, то более безопасным будет считать independent-версией как раз не последнюю, а предыдущую... Поскольку все GUID - разные, подобная проблема не возникает в разделе HKCR\CLSID, туда инсталлятор просто должен вписывать "свои" параметры, корректируя только VersionIndependentProgID по необходимости, и не трогая никаких других.

Если вы претендуете на звание архитектора, а не маклера-риэлтора, то, наверное, надо иметь представление о том, что есть "кирпичная кладка"? И что "из кирпича, в принципе, можно построить"? Вас никто не заставляет становиться каменщиком, но, видимо, вам всё-таки надо знать, сколько и какого труда стоит действительно построить то или иное архитектурное решение?

Всё что будет изложено ниже будет относиться к тому, как может быть реализована экспортируемая функция DllRegisterServer. Но оно с тем же успехом будет относиться и к тому, что вообще можно сделать для регистрации COM-сервера в системе и зачем.

Мы совсем немножечко затронули тему, какая информация должна содержаться в реестре, чтобы система могла обслужить запрос клиента на предоставление ссылки на COM-объект. И как-то так самопроизвольно обнаружилось, что этой (заведомо неполной!) информации уж очень много. И заметьте - то, что уже было описано относится к одному статическому типу, экспонируемому сервером. Если сервер реализует десять статических типов, то, соответственно всю регистрационную информацию, описанную в прошлой статье нужно записать в реестр для каждого объекта, т.е. повторить с изменяющимися параметрами десять раз. Как это сделать и остаться в живых?

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

Вот фрагмент такого файла:

Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\CLSID\{0002E510-0000-0000-C000-000000000046}] @="Microsoft Office Spreadsheet 9.0"
[HKEY_CLASSES_ROOT\CLSID\{0002E510-0000-0000-C000-000000000046}\InprocServer32] @="C:\\PROGRA~1\\MICROS~1\\Office\\MSOWC.DLL"
[HKEY_CLASSES_ROOT\CLSID\{0002E510-0000-0000-C000-000000000046}\ProgID] @="OWC.Spreadsheet.9"
[HKEY_CLASSES_ROOT\CLSID\{0002E510-0000-0000-C000-000000000046}\VersionIndependentProgID] @="OWC.Spreadsheet"

Вот командная строка, которая заставляет редактор реестра произвести импорт в реестр:

regedit.exe <имя файла>.reg

Дело за малым - написать инсталяционный скрипт, например, .bat-файл, вписать все необходимые команды копирования наших компонентов программного комплекса и в том числе указать эту команду. Функция DllRegisterServer здесь, фактически не нужна. И обойтись можно без нее - ведь она и вызывается только на этапе инсталляции.

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

Следующий способ предполагает использование "профессионального инсталлятора". Например, Install Shield или MS Installer. Эта категория программных средств позволяет составить скрипт дистрибутива, потом его как-то преобразовать в "человеконеисправляемую форму" и предложить специальной программе setup.exe которую сам же "профессиональный инсталлятор" и составляет - именно для того, чтобы исполнить этот самый скрипт на пользовательской машине. Каждый программист знает, что это такое, а то и сам пользовался этим способом создания дистрибутивов.

Этот способ сложнее в использовании, но обладает значительно бОльшими достоинствами - и целостность дистрибутива можно проверить, и деинсталляция возможна "правильная" и при регистрации компонентов можно не просто вписать значения параметров, а ещё кое-что проверить. Однако и здесь не обходится без ложки дёгтя - полную структуру инсталляционной информации, необходимой для функционирования вашего продукта вам приходится раскрывать (системный реестр ведь используется не только для функционирования COM, в нём можно держать ещё массу другой полезной информации, которая не вся может быть предназначена для посторонних глаз - ни по содержанию, ни по структуре). Когда автором и правообладателем всего "конечного дистрибутива" являетесь вы сами это не составляет проблемы. Но ситуация меняется, если вы являетесь автором только т.н. redistributable components - модулей, которые просто входят в состав дистрибутива чужого.

Третий способ... третий способ как раз и есть использование специальной экспортируемой функции саморегистрации. Используя этот способ вы абсолютно свободны как в способе её реализации , так и в возможностях сохранения конфиденциальности требований к операционному окружению вашего модуля. Немаловажно, что этот способ не отрицает использования способа второго - в скрипте "конечного дистрибутива" может быть сделан вызов этой функции вашего модуля и "большая инсталляция" будет корректной. А можно сделать и "малую инсталляцию" - если вы зафиксировали повреждение информации реестра относимой именно к вашему модулю, то можно просто вызвать эту функцию для него отдельно. Т.е. для такого автономного и замкнутого программного изделия предназначенного комплексироваться с другими, как COM-сервер, этот способ стратегически является самым подходящим. Насколько он является подходящим тактически - нужно оценивать. Оценивать по затратам, которые требуется понести для того, чтобы реализовать эту функцию.

Здесь можно сказать, что функция DllRegisterServer вызывается за время эксплуатации данной копии модуля, по сути, только один раз. Но присутствует в составе модуля как неотъемлемая его часть постоянно. А, значит, она не должна занимать очень много места, хотя вполне допустимо, если она будет работать не очень быстро.

Канонически (по современным меркам - давным-давно) для этого использовалось семейство системных функций Reg????Key???. Как выглядит один из его представителей - RegCreateKeyEx любопытства ради приведено ниже:

LONG RegCreateKeyEx(HKEY hKey, //дескриптор открытого параметра реестра
                    LPCTSTR lpSubKey, //имя подпараметра 
                    DWORD Reserved, //зарезервировано 
                    LPTSTR lpClass, //строка класса 
                    DWORD dwOptions, //особые опции 
                    REGSAM samDesired, //желаемый уровень безопасности доступа 
                    LPSECURITY_ATTRIBUTES lpSecurityAttributes, //наследуемые значения 
                    PHKEY phkResult, //дескриптор созданного ключа 
                    LPDWORD lpdwDisposition //буфер для диспозиции 
);

Проблем с его использованием нет никаких - надо подставить параметры и функция впишет в реестр один параметр реестра. Это грустное обстоятельство, а также то, что информация о статическом типе в реестре регулярна, стимулировала программистов делать "внутренние скрипты", которые бы в виде двоичного ресурса присутствовали в модуле, и для которых бы вызывалась относительно универсальная процедура, просто вызывающая серию RegCreateKeyEx. Такое решение, например, было использовано в ATL 1.1 (ныне применяющаяся версия ATL - 3.0).

Ну и наконец, существует самое изящное решение - воспользоваться "стандартным системным сервисом". Можно только гадать, почему Microsoft ввела этот сервис в состав операционной системы так поздно. Ведь в спецификациях CORBA "сервис репозитариев" был определён изначально и программисты CORBA имели с регистрацией куда меньше мучений (что, впрочем, с лихвой компенсировалось и компенсируется мучениями другими). Однако, начиная с версии Windows NT 4.0 SP3 и выше, такой сервис в системе присутствует, причём, как и положено, сервис этот - сам является COM-объектом.

Называется он Component Categories Manager, его CLSID - {0002E005-0000-0000-C000-000000000046} и он реализован в составе модуля OLE32.DLL, т.е. если в системе есть COM, есть и данный сервис. В собственной программе на него можно сослаться по идентификатору CLSID_StdComponentCategoriesMgr. Компонент экспонирует два интерфейса ICatInformation и ICatRegister. Как использовать это чудо компонентной технологии - сейчас желающие отсылаются к MSDN. В нашем повествовании сейчас пока достаточно только знать, что такой сервис есть и что им можно и нужно пользоваться.

Но это никоим образом не умаляет заслуг старого семейства функций Reg????Key??? и не освобождает программиста от необходимости знать и о них и том, как они работают. Всё дело-то в том, что Component Categories Manager регистрирует в реестре только "стандартную системную" информацию! А иногда нужно регистрировать и проверять и информацию "внесистемную" - например, при реализации интерфейса IClassFactory2 вам может понадобиться "куда-то в системе запрятать ключ лицензии". Это вряд ли стоит делать какими-то уж очень стандартными средствами, верно?

Поэтому, исключительно демонстрации того ради, "как это делается на самом деле" мы в очередном примере пока используем только Reg????Key??? и зарегистрируем только те параметры реестра, которые упоминались как "минимально достаточные".

Естественно, что вторая экспортируемая функция сервера - DllUnregisterServer - реализуется совершенно аналогично, только направление её работы будет прямо противоположным - всё, что сервер вписал в реестр при регистрации она должна вычистить. Хотя... хотя для удаления сервера из системы это - вовсе не обязательная операция. Стереть файл можно и без предварительной разрегистрации сервера. Многие, особенно небрежно составленные программы и инсталляционные скрипты, делают так. Но сам я не думаю, что они - достойный пример для подражания. Поэтому и функцию разрегистрации сервера реализовывать тоже необходимо.

Теперь рассмотрим функцию inproc-сервера DllCanUnloadNow . Проблема, которую она решает состоит в следующем. Сервер - динамический ресурс. Система его загружает в процесс, к нему обратившийся. И система же должна иметь возможность его корректно выгрузить, когда сервер перестанет быть нужным.

Сформулированная столь ясно и понятно задача, тем не менее, требует, чтобы о ней поговорить подробнее. Ведь функция DllCanUnloadNow - просто частная реализация частного случая для частного же вида сервера... Но проблема, которая с её помощью решается - проблема общая. Как освободить ресурс (в данном случае - DLL сервера), когда ресурс больше не нужен?

В общем-то решение, конечно, очевидно - если ресурс "к жизни" вызывал клиент, то клиент должен от этого ресурса и отказаться. Отказываться можно по-разному. Для "нормальной DLL" существует функция FreeLibrary, вызывая которую клиент сообщает системе, что DLL с указанным именем ему больше не нужна. Но клиент может вызвать эту функцию лишь тогда, когда клиент же и вызывал для этой DLL функцию LoadLibrary. Если DLL загружала система (а в большинстве случаев именно так и бывает), то вызывать FreeLibrary клиент не может - это крах.

В COM - клиент никогда не загружает сервер сам. Это один из основных принципов COM - клиент не должен знать как реализован объект и где он обитает. Связь ссылки на статический тип с реализацией осуществляет система. Поэтому такой способ здесь ни в каких случаях не годится. Но в COM именно клиент управляет временем жизни ресурса - это тоже основной принцип COM. Поэтому, видимо, механизм отказа от ресурса в общих чертах должен бы напоминать механизм AddRef/Release.

Владельцем объекта является сервер, а клиент является только его пользователем. Пользователь сообщает серверу о своих желаниях вызывая методы AddRef и Release, а сервер смотрит состояние счётчика ссылок и в какой-то момент принимает решение уничтожить объект, потому, что он больше никому не нужен. Подобное должно происходить и в данном случае, только владельцем сервера является уже операционная система (она вызывала LoadLibrary)... Интересно, а кто является в данном случае "пользователем сервера"? "Пользователем сервера" являются все его клиенты... Ведь всякий раз, когда для какого-то объекта, реализуемого данным сервером откуда-то кем-то вызывается AddRef это же означает и то, что "AddRef" вызывается и для всего сервера в целом. Поэтому в данном случае и нужен только один метод - метод, который бы информировал операционную систему о текущем состоянии сервера - нужен ещё он кому-то или уже нет. Я особо обращаю ваше внимание, что метод нужен только один - если объект при освобождении занятых им ресурсов может вызвать delete this, то сервер в отношении себя вызвать FreeLibrary не может - если FreeLibrary вызывается из того же модуля, который удаляется, то, после того, как она успешно отработает, DLL перестанет существовать. И куда, в данном случае, должна привести инструкция процессора ret, которая возвращает управление в код уже несуществующей DLL?

Именно поэтому всё сделано так, как оно сделано. Сервер внутри себя ведёт глобальный счётчик ссылок, возможно - на все ссылки всех интерфейсов, возможно - только на все "живые" пока объекты, но в ответ на вызов DllCanUnloadNow должно быть возвращено значение S_OK, если сервер больше никому не нужен, и S_FALSE, если сервер имеет "живые" объекты и выгружать его нельзя. В этом - и вся функция DllCanUnloadNow. Но для её реализации нужно, чтобы всякий внутренний метод сервера создающий объект продвигал счётчик ссылок сервера вперед, а метод уничтожающий объекты - продвигал счётчик назад. Т.е. для реализации этой функции требуется некая "инфраструктурная" деятельность на уровне всех реализаций интерфейсов IUnknown всех объектов сервера. Сервер, по своей внутренней конструкции - совсем не простая DLL!

Функция DllCanUnloadNow - функция, предназначенная для вызова только из операционной системы. Вызывать её из своего клиента бессмысленно - ведь функция ничего не делает...

Когда система вызывает функцию DllCanUnloadNow? Очень хотелось бы думать, что система "сидит за углом" и только и ждёт, ну когда же сервер возвратит S_OK, чтобы его тут же немедленно и выгрузить... На самом деле это не так. Система в отношении загруженного сервера ведёт себя совершенно пассивно - когда она вызывает эту функцию по собственной инициативе ведомо только Microsoft. Видимо, причина здесь в том, что адресное пространство текущего процесса - ресурс процесса, а не системы. И это сам процесс должен побеспокоиться об освобождении от ненужного. Во всяком случае если клиент хочет "собрать мусор" он должен из себя вызвать функцию CoFreeUnusedLibraries или CoFreeUnusedLibrariesEx. Эти функции "проходятся" по всем загруженным DLL и вызывают DllCanUnloadNow (если они такую функцию у DLL найдут, об этом чуть ниже). В отношении DLL, возвративших S_OK проделывается следующая процедура.

Если сервер реализовывал однопоточную модель (STA), то система вызывает в отношении него функцию CoFreeLibrary и действительно освобождает DLL. Если же сервер реализовывал модель свободных потоков (MTA), или модель нейтральных потоков (NA), то система помещает данную DLL в список "кандидатов на удаление" и устанавливает "таймер задержки" на 10 минут, откладывая вызов CoFreeLibrary. Это сделано по причинам, связанным исключительно с мультизадачностью (параллельный поток, который может повлиять на судьбу DLL может спать и не быть в состоянии себя проявить). Если будет сделан второй вызов CoFreeUnusedLibraries, то этот второй вызов освободит те DLL из списка кандидатов на удаление, чей интервал ожидания истёк, и поместит новые DLL в список кандидатов, если такие DLL есть к этому моменту. Величина интервала в 10 минут установлена волюнтаристски самой Microsoft и не может быть как-то настроена - это данность.

Естественно, что если в течение этого 10-тиминутного интервала последует обращение к серверу, находящемуся в списке кандидатов на удаление, то он удаляется из списка, вновь становится активным - он еще не был выгружен из памяти, поэтому все его функции работоспособны. Загружать такой сервер вновь системе не нужно. Но для клиента такой механизм совершенно прозрачен - он просто пытается "поднять объект" по имеющемуся у него CLSID. Явления связанные с фактической загрузкой/выгрузкой сервера можно заметить только косвенно - например, по скорости загрузки или по тому, что файл "выгруженного" сервера система не даст переписать...

Интересно, что функция DllCanUnloadNow - необязательная. Так же, как необязательны и DllRegisterServer и DllUnregisterServer. Но если функциональность "регистрирующих" функций можно реализовать "снаружи", то отсутствие DllCanUnloadNow приводит только к тому, что система совсем не может установить факта можно ли выгрузить этот сервер. И считает его невыгружаемым всё время "работы COM". А время это - интервал между вызовами клиентом функций CoIninialize и CoUninitialize, т.е. когда клиент вызовет CoUninitialize он тем самым сообщит системе, что "услуги COM" ему больше не нужны. А, значит, не нужны и все COM-серверы, независимо, имеют ли они в своём составе функцию DllCanUnloadNow.

Вот и почти все экспортируемые функции inproc-сервера, которые известны. Они - достаточны для всех случаев жизни сервера. Но, точности ради, нужно упомянуть и о пятой, "призрачной" функции, которую тоже может экспортировать сервер. Эта функция называется DllRegisterServerEx. Как можно заметить по названию она выполняет какие-то "расширенные" действия по регистрации сервера, по сравнению с функцией DllRegisterServer. Но какие? Тайна сия велика есть! В MSDN для этой функции не найти прототипа, ссылки на то, где она встречается приносят две статьи про скрипты в которых говорится, что "...регистрация сервера производится функциями DllRegisterServer или DllRegisterServerEx ...". В Большой Сети есть несколько конференций где вопрос о том, что такое DllRegisterServerEx уже давно есть, а ответа на него пока нет... Я думаю, что изобретение этой функции было каким-то неудачным экспериментом Microsoft, но "в целях совместимости" эта функция продолжает экспортироваться некоторыми серверами и поныне... Во всяком случае, на практике обнаруживается, что и без неё всё хорошо работает.

Предыдущая Оглавление Следующая