Что такое "технология 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
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 заключаются только в том,
что всюду предложение:
и из состава проекта клиента была удалена реализация процедуры эмулятора.
Это - как раз то самое изменение к которому мы так долго подбирались! Рассмотрим его
(т.е. функцию CoGetClassObject) подробнее. Во-первых, можно подумать, что
эту самую функцию можно и самому написать... если бы мы в состав нашего эмулятора внесли
поиск по реестру, то получили бы то же самое? Но это - очень обманчивое впечатление.
Всё дело в том, что мы в данном случае работаем с одним и самым простым типом сервера -
с inproc (внутрипроцессным). Для его запуска действительно ничего не
требуется, как только отыскать его и загрузить в процесс клиента. А еще есть
local (местный, существующий на той же машине но в другом процессе) и
remote (удалённый, существующий на другой машине) серверы. И процедура
их "приведения в боевое положение" - значительно более сложная. А функция
CoGetClassObject, которую вызывает клиент - всегда одна и та же. Ведь
клиент не должен знать как реализован сервер!
Но это - не совсем точное утверждение... Клиент может не знать как реализован
сервер. Но может и весьма этим интересоваться - ведь накладные расходы на связь с
сервером в буквальном смысле на порядки отличаются в зависимости от того удалённый он,
локальный или внутрипроцессный. И может оказаться так, что с каким-то типом сервера
клиент захочет иметь дело, а с каким-то - нет. Поэтому у функции
CoGetClassObject имеется специальный параметр, значения которого определены в
виде перечисления:
Этот перечислитель определяет "допустимые контексты запуска сервера", если один и
тот же объект реализуется серверами разных типов. Такое возможно, поскольку для одного
и того же 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, но в чём именно причина эта состоит -
в следующей рассылке...