Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Что такое "технология COM" и как с ней бороться?


Служба Рассылок Subscribe.Ru проекта Citycat.Ru

М. Безверхов.
vasilisk@clubpro.spb.ru

Что такое "технология COM" и как с ней бороться?     №29


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

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

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

Именно этот .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, М. Безверхов
Публикация требует разрешения автора.


"); // -->

http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться Рейтингуется SpyLog

В избранное