Сконструируем пример действительно "настоящего 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_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.
Предыдущая Оглавление Следующая