developing.ru - клуб программистов Здесь может быть и ваша реклама.
developing.ru >технология COM >
Михаил Безверхов
aspid@developing.ru

Отличие технологии железного века от предыдущих

Надеюсь, вы внимательно изучили функционирование примера, опубликованного в прошлой статье. Сегодня немного о том, как был написан пример и что в нём главное для нас.

Новшеств,которые были внесены в исходный сервер из примера №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):

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, но в чём именно причина эта состоит - в следующей статье...

предыдущий выпускархив и оглавлениеследующий выпуск
Авторские права © 2001 - 2004, Михаил Безверхов
Публикация требует разрешения автора

разделы сайта

форум

технология COM

оптимизация сайтов


© 2000-2004 Клуб программистов developing.ru