Скачиваний:
100
Добавлен:
01.05.2014
Размер:
1.56 Mб
Скачать

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 применимы следующие правила:

  • Они в совокупности не должны быть длиннее GUID, т.е. 39 символов;

  • Они не могут содержать знаков пунктуации, в том числе - знаков подчёркивания, исключение - только разделяющие точки;

  • Последовательность не должна начинаться с цифры.

Но перечисленное - еще не всё. Описанный набор параметров задаёт связь "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. Такое решение, например, было использовано вATL1.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, но "в целях совместимости" эта функция продолжает экспортироваться некоторыми серверами и поныне... Во всяком случае, на практике обнаруживается, что и без неё всё хорошо работает.