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

Создание "настоящего" com-сервера.

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

Ход конструирования. Берем предыдущий пример. То, что делает сам сервер "с содержательной стороны" сегодня нас вполне устраивает. Поэтому ни интерфейсы, ни объекты мы изменять не будем. Мы только добавим к серверу реализацииDllRegisterServer,DllUnregisterServerиDllCanUnloadNow, а реализацияDllGetClassObjectу нас и так существует с самого начала.

В клиенте нас тоже всё устраивает, поэтому единственной заменой будет замена вызова CoGetClassObjectEmulator(который реализовывался самим клиентом) на вызов "настоящей системной"CoGetClassObject. Ещё, конечно, в клиент добавятся вызовы инициализацииCOM-CoInitialize, которая должна быть вызвана клиентом перед тем, как клиент намерен воспользоваться сервисомCOM, иCoUninitialize, которая должна быть вызвана когдаCOMклиенту больше не нужен. Эти функции, в известном смысле, являются рудиментом - некогдаCOM, тогда ещё "носивший девичью фамилию"OLE, был совершенно отдельной, не встроенной в операционную систему подсистемой. Оформленной в виде модуляOLExxx.DLL, которую система загружала "по требованию". СейчасCOMвстроен в операционную систему, но вызов этих функций по прежнему требуется.

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

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

DllMain- системой определенная процедура, реализуемая любойDLL, которая вызывается системой же в четырёх случаях:

  • при загрузке DLLв поток, инициировавший её загрузку -DLL_PROCESS_ATTACH

  • при создании нового потока в процессе, владеющем DLL-DLL_THREAD_ATTACH

  • при завершении не последнего потока в процессе - DLL_THREAD_DETACH

  • при завершении последнего потока в "жизни DLL" -DLL_PROCESS_DETACH

Поэтому, "отлавливая" событие DLL_PROCESS_ATTACHможно выяснить когда на самом деле даннаяDLLвызвается к жизни, а "отлавливая"DLL_PROCESS_DETACH- когда она выгружается. Поскольку в нашем примере используется только один поток, то событияDLL_THREAD_ATTACHиDLL_THREAD_DETACHне возникают совсем.

Итак, первым шагом после сборки модуля сервера NeoSrv3.dllнеобходимо его зарегистрировать. Регистрация производится функциейDllRegisterServer, которая вписывает в системный реестр минимально необходимую информацию о двух статических типах -CBanzaiиCHello. Эта информация состоит только из параметраInprocServer32, который сообщает системе в какойDLLэти статические типы реализованы.

Для вызова функций DllRegisterServer<иDllUnregisterServerоперационная система располагает специальной утилитойregsvr32.exe, которая обычно обитает в каталогеWindows\System32. Если эту утилиту запустить из командной строки (через меню "Пуск -> Выполнить", например) без параметров, то она выведет на экран транспарант с описанием списка своих ключей. Если её запускать с параметром, то она предполагает, что параметр этот - имяDLL, которую она должна загрузить чтобы вызвать у этойDLLкакую-то экспортируемую функцию. Например, командная строка:

regsvr32 <имя DLL>

приводит к тому, что утилита загружает DLLс указанным именем и вызывает у неё экспортируемую функциюDllRegisterServer, если, конечно, такая функция уDLLобнаружится. А командная строка:

regsvr32 /u <имя DLL>

заставит вызвать у DLLэкспортируемую функциюDllUnregisterServer.

Это воочию можно и увидеть - в ответ на команду с консоли:

regsvr32 NeoSrv3.dll

будет последовательно показано четыре транспаранта (на каждом нужно нажать кнопку "OK"): "DLL_PROCESS_ATTACH" -> "вызов DllRegisterServer" -> "DllRegisterServer in NeoSrv3.dll succeeded" -> "DLL_PROCESS_DETACH", из них первый, второй и четвёртый - наши, а третий вывешивает сама regsvr32.exe. Стоит только отметить, что команда эта должна быть выдана из того каталога, где обитаетNeoSrv3.dll- в другом случае нужно указывать и спецификацию пути к модулю.

Сказанное можно не принимать на веру, а проверить - запустить программу regedit.exeи в разделеHKEY_CLASSES_ROOT\CLSID\найти два параметра (они записаны в реестр подряд){3F9D5DB0-3413-11d5-AE38-00E02944637A}и{3F9D5DB1-3413-11d5-AE38-00E02944637A}и убедиться, что значение (под)параметраInprocServer32у них действительно - полная спецификация пути к модулюNeoSrv3.dll

Теперь наш сервер можно использовать. Можно запускать клиента - модуль NeoCln3.exe

Клиент запускается "в одиночку". Это и видно - никаких транспарантов не появляется. Т.е. наш сервер вызывается только по мере надобности - стоит нажать кнопку "Создать Banzai" или "Создать Hello", как немедленно появляется транспарант "DLL_PROCESS_ATTACH". Это система отыскала сервер и пытается загрузить его на выполнение. После нажатия кнопки "ОК" на транспаранте появляется сам заказанный объект - сервер нашёлся, загрузился и у него отработал вызов экспортируемой функции DllGetClassObject, который возвратил ссылку на заказанный объект. Далее у сервера можно попросить и другие ссылки - транспарант "DLL_PROCESS_ATTACH" больше не появляется, ведь сервер уже загружен. Но вот мы избавились от всех ссылок, а сервер не выгружается - верно, ранее было описано такое поведение. Если мы вновь будем запрашивать у сервера ссылки, то сервер нам будет готов служить вновь - без всяких загрузок в процесс. Попробуем завершить клиента - нажмём "Закрыть панель". И тут мы увидим транспарант "вызов DllCanUnloadNow" - это система "наткнулась" на вызовUnInitializeи отрабатывает завершениеCOM. А после него последует событиеDLL_PROCESS_DETACH- система выгружает сервер. Всё!

Ну, и последним шагом нужно разрегистрировать сервер - ни к чему засорять системный реестр учебными программами. В ответ на командную строку:

regsvr32 /u NeoSrv3.dll

вы увидите уже знакомую последовательность транспарантов "DLL_PROCESS_ATTACH" -> " вызов DllUnregisterServer" -> "DllUnregisterServer in NeoSrv3.dll succeeded" -> "DLL_PROCESS_DETACH", а после этого попробуйте поискать зарегистрированные нами параметры реестра...

Единственное маленькое замечание - regedit.exeне всегда корректно обновляет своё представление реестра, т.е. если вы сначала запустилиregedit.exe, открыли разделHKEY_CLASSES_ROOT\CLSIDи только потом вы выдали командуregsvr32.exe NeoSrv3.dll, то велика вероятность, что зарегистрированных параметров вы не обнаружите. Аналогичное будет наблюдаться и при разрегистрации - вы будете продолжать видеть уже удалённые параметры. Это не означает, что изменения в реестр не вносятся, это означает только, чтоregedit.exeпоказывает статическую картину и его нужно перезапустить.

Новшеств, которые были внесены в исходный сервер только два: новые экспортируемые функции и счётчик ссылок всего сервера, который использовался для реализации метода DllCanUnloadNow. В отношении счётчика всё должно быть понятно - это просто статическая переменная уровня всего модуля, которая инициализируется вDllMain, когда в неё приходит событиеDLL_PROCESS_ATTACH. На самом деле она инициализируется ещё слоемCRT, до того какDllMainполучит управление в первый раз, поэтому вполне была допустима и конструкцияDWORD dwSrvRefCnt = 0;

В отношении же экспортируемых функций есть небольшая хитрость, которую, возможно, углядели не все, а программист COMдолжен её знать. Дело в том, что имена внешних экспортируемых символов, например,DllRegisterServer- действительноDllRegisterServer. А компиляторC++делать их такими не умеет. Декларация__declspec(dlliexport) DllRegisterServer даже с предупреждениемextern"C" порождает экспортируемый символ_DllRegisterServer, что ровно на один знак подчёркивания отличается от того, что должно быть. Для избежания этого в проект включен файл.def, инструктирующий линкер какими всё-таки должны быть эти самые внешние имена:

LIBRARY "NEOSRV3"

EXPORTS

DllGetClassObject PRIVATE

DllCanUnloadNow PRIVATE

DllRegisterServer PRIVATE

DllUnregisterServer PRIVATE

Именно этот .def-файл и делает экспортируемые имена такими, какие требуется - подобного рода обстоятельство следует где-то на задворках своего сознания иметь в виду. Хотя, конечно, при созданииATL-проекта все правильные компоненты проекта вам сделаетwizard, редко, но бывает необходимо привести к серверу уже существующий проектDLL. Так вот в таких случаях знание этого обстоятельства здорово сохраняет нервные клетки - такое поведение компилятора и линкера описано вMSDNплохо.

Функции DllRegisterServerиDllUnregisterServerмы реализовали "по-старинке" и сверхпримитивно - простая линейная последовательность вызовов функцийReg???Key???Сделано это было намеренно - простоты и ясности ради, поскольку реализовать возможные в данном случае циклы и "внутренние скрипты", о которых упоминалось ранее - из области "искусства программирования", а не именноCOM. Следует отметить, что мы вписали в реестр минимум (имея при этом такой большой, объёмный иодноразовый, по сути, код) информации, достаточной только для того, чтобы запустить сервер по прямо известному клиентуCLSID. Если бы нам необходимо было вписывать полную информацию, то, наверное, стоило бы и поизощряться в создании такой процедуры, которая была бы как можно короче и при этом была полнофункциональна - функцияDllRegisterServerможет ведь завершиться и некорректно, не суметь зарегистрировать все объекты.

Интересно, рассматривая реализацию DllRegisterServer, увидели ли вы, что нам теперь всё равно не только в каком каталоге располагается сервер, но даже и каково имя его модуля?! Если не верите - переименуйтеNeoSrv3.dll, зарегистрируйте через вызовregsvr32.exe, и запустите клиента. Клиент будет работать как ни в чём не бывало. Почему? Ответ, естественно, в исходных текстах примера.

Изменения, которые мы внесли в исходный клиент из предыдущего примера заключаются только в том, что всюду предложение:

::CoGetClassObjectEmulator(CLSID_... , IID_NeoInterface, (void **) ...);

было заменено на:

::CoGetClassObject(CLSID_..., CLSCTX_INPROC_SERVER, NULL, IID_NeoInterface, (void**) ...);

и из состава проекта клиента была удалена реализация процедуры эмулятора.

Это - как раз то самое изменение к которому мы так долго подбирались! Рассмотрим его (т.е. функцию CoGetClassObject) подробнее. Во-первых, можно подумать, что эту самую функцию можно и самому написать, если бы мы в состав нашего эмулятора внесли поиск по реестру, то получили бы то же самое? Но это - очень обманчивое впечатление. Всё дело в том, что мы в данном случае работаем с одним и самым простым типом сервера - сinproc(внутрипроцессным). Для его запуска действительно ничего не требуется, как только отыскать его и загрузить в процесс клиента. А еще естьlocal(местный, существующий на той же машине но в другом процессе) иremote(удалённый, существующий на другой машине) серверы. И процедура их "приведения в боевое положение" - значительно более сложная. А функцияCoGetClassObject, которую вызывает клиент - всегда одна и та же. Ведь клиент не должен знать как реализован сервер!

Тем не менее, это - не совсем точное утверждение. Клиент можетне знать как реализован сервер. Но может и весьма этим интересоваться - ведь накладные расходы на связь с сервером в буквальном смысле на порядки отличаются в зависимости от того удалённый он, локальный или внутрипроцессный. И может оказаться так, что с каким-то типом сервера клиент захочет иметь дело, а с каким-то - нет. Поэтому у функцииCoGetClassObjectимеется специальный параметр, значения которого определены в виде перечисления:

typedef enum tagCLSCTX

{

CLSCTX_INPROC_SERVER = 1,

CLSCTX_INPROC_HANDLER = 2,

CLSCTX_LOCAL_SERVER = 4,

CLSCTX_REMOTE_SERVER = 16,

CLSCTX_NO_CODE_DOWNLOAD = 400,

CLSCTX_NO_FAILURE_LOG = 4000

} CLSCTX;

#define CLSCTX_SERVER (CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER)

#define CLSCTX_ALL (CLSCTX_INPROC_HANDLER | CLSCTX_SERVER)

Этот перечислитель определяет "допустимые контексты запуска сервера", если один и тот же объект реализуется серверами разных типов. Такое возможно, поскольку для одного и того же CLSIDможно в реестре определить, скажем, параметры иInprocServer32иLocalServer32одновременно.

На практике такое встречается нечасто, значительно чаще только один сервер вполне определённого типа реализует данный CLSID. Поэтому, чтобы сказать, что клиенту всё равно, какой тип сервера будет загружаться, в параметрах вызова указывается значениеCLSCTX_ALL. Но система тоже "в меру ленива", она "знает", что проще всего "поднять" сервер в контекстеCLSCTX_INPROC_SERVER. В контексте жеCLSCTX_INPROC_HANDLERсделать это сложнее, чем вCLSCTX_INPROC_SERVER, но проще, чем в контекстеCLSCTX_LOCAL_SERVER... Поэтому, если определены несколько флажков возможных контекстов запуска сервера одновременно, система всё равно попытается первым запустить "самый простой" из них.

Ещё у функции имеется параметр типа COSERVERINFO, описывающий удалённый сервер (поскольку в нашем случае этого не требуется, в качестве его значения передаётсяNULL), но до этого мы ещё когда-нибудь дойдём.

В качестве своего значения функция CoGetClassObjectвозвращает несколько кодов, вот самые типовые (подробности вMSDN):

S_OK

- успешное завершение, все задачи выполнены

REGDB_E_CLASSNOTREG

- CLSIDнекорректно зарегистрирован

E_NOINTERFACE

- запрошенный интерфейс не поддерживается объектом

REGDB_E_READREGDB

- ошибка чтения регистрационной базы данных

CO_E_DLLNOTFOUND

- DLLсервера не найдена

CO_E_APPNOTFOUND

- EXEсервера не найден

Как видно, они есть совокупность кодов ошибок, которые могут произойти на всех стадиях процесса - от поиска в реестре до попытки запросить ссылку у сервера. Во всяком случае, если не изменяет память, то код E_NOINTERFACEвозвращали мы сами, когда реализовывалиDllGetClassObject:)

Теперь мы уже точно находимся "внутри настоящего COM". Хотя пока очень недалеко от входа. Во всяком случае написать к нашему серверу клиента наVisual Basicмы пока не сможем: при всей корректности нашего сервера объекты, которые он реализует - пока ещё "не совсем правильные". В этом же и причина того, почему вместо рекламируемой ранее функцииCoCreateInstanceмы пока воспользовались толькоCoGetClassObject.