![]() |
![]() |
Отличие технологии железного века от предыдущихНадеюсь, вы внимательно изучили функционирование примера, опубликованного в прошлой статье. Сегодня немного о том, как был написан пример и что в нём главное для нас. Новшеств,которые были внесены в исходный сервер из примера №2 только два: новые экспортируемые функции и счётчик ссылок всего сервера, который использовался для реализации метода 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, и запустите клиента. Клиент будет работать как ни в чём не бывало... Почему? Ответ, естественно, в исходных текстах примера №3. Изменения, которые мы внесли в исходный клиент из примера №2 заключаются только в том, что всюду предложение: ::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):
Как видно, они есть совокупность кодов ошибок, которые могут произойти на всех стадиях процесса - от поиска в реестре до попытки запросить ссылку у сервера. Во всяком случае, если не изменяет память, то код E_NOINTERFACE возвращали мы сами, когда реализовывали DllGetClassObject :) Вообще же говоря, предыдущей и этой статьями мы совершили своего рода прорыв - от решений "по-колхозному" мы перешли к решениям, поддерживаемым системой. Т.е. мы уже точно находимся "внутри настоящего COM". Хотя, если продолжать такую аналогию, находимся мы пока очень недалеко от входа. Во всяком случае написать к нашему серверу клиента на Visual Basic мы пока не сможем: при всей корректности нашего сервера объекты, которые он реализует - пока ещё "не совсем правильные". В этом же и причина того, почему вместо рекламируемой ранее функции CoCreateInstance мы пока воспользовались только CoGetClassObject, но в чём именно причина эта состоит - в следующей статье...
Авторские права © 2001 - 2004, Михаил Безверхов Публикация требует разрешения автора |
|