Что такое "технология COM" и как с ней бороться?№20
Об известных граблях в COM -
первая серия
Мы
уже знаем точные структуры, которые
требуются для организации взаимодействия
объектов клиента и сервера,
располагающихся в разных модулях. Мы знаем
протокол этого взаимодействия. Мы уже
сочинили первый работающий пример,
иллюстрирующий нашу рассмотренную ранее
теорию и выяснили чего ещё не хватает для
продвижения вперёд. Но программирование -
точная наука. Есть мелкие детали, которые мы
считали само собой разумеющимися, но
которые таковыми, к сожалению, не являются.
О них и речь. Тем более, что я получил целую
серию однотипных вопросов, авторов которых
можно условно разделить на два "лагеря".
Авторы
из одного лагеря имеют некоторое
недоумение относительно нашего
постоянного упоминания "что у COM внутри в
двоичном коде", чем, собственно, и
недовольны. Авторы из противоположного
лагеря, напротив, пеняют, что я неточно
излагаю "что у COM внутри" и что "вон
те байты вот не там расположены".
Уважаемые
читатели нашей рассылки! Возможно, что я что-то
недопереоценил и это обстоятельство
нужно высказать явно. Суть нашей рассылки -
совсем не "технология COM". А -
программирование и архитектурное
проектирование из двоичных компонент на
примере одной из известных (и бесплатных!)
технологий
для этого - COM. Но окружающая
действительность такова, что подавляющее
большинство программистов мыслят сугубо
конкретными категориями, а то и ещё хуже -
категориями одной известной им из
собственной практики конкретной
реализации. Просто потому, что
иначе - не умеют. И мне приходится сочинять
что-то "синтетическое", которое,
понятно, не всегда оказывается
оправдывающим ожидания тех или иных
читателей. Простите великодушно!
Я
же пытаюсь двигаться путём, который как-то
соединяет оба подхода - на примере
конкретной реализации пытаюсь показать
сущность подхода к программированию и
архитектуре. Пытаюсь показать "вечные
ценности" и, прежде всего, - философию
взгляда. Своего, естественно, взгляда, ведь
другого у меня нет. А ваша задача - дать себе
труд построить свой собственный взгляд.
Пользуясь и моим взглядом и другими
источниками информации, как некоторой
опорой. Заранее понятно, что ваш взгляд
будет в чём-то отличаться от моего - это
отличие не есть основание считать, что "доцент
- тупой". Но ведь и рассматривать мою
рассылку как "сборник рецептов" - также
бессмысленно. Мне не хватит жизни, чтобы
этот сборник сделать, а у вас для
формирования своего взгляда есть и
собственная жизнь и собственная практика.
Поэтому предельно конкретные значения
параметров вы всегда сможете прочитать в MSDN
или в учебнике, где объясняется "как
вызвать Wizard", важно, чтобы программист понимал
существо, стержень, каркас. А что на этот
каркас можно навесить и как именно - на то есть
собственная голова творца, это даже и
перечислить невозможно. Поэтому я и
просил бы относиться к рассылке
соответственно - она не справочник, она -
учебник. И учебник, который ближе стоит не к
учебнику ремесла, а к учебнику философии. Я
очень надеюсь, что она сможет принести чуть
больше пользы, чем написано в ее заглавии.
Кроме того, COM - развивается. Появляются
новые возможности, интерфейсы, значения
констант... Даже "мгновенно" я не
обладаю всеми знаниями о COM, не говоря уже о
том, что я не могу всё это мгновенно
записать. И разослать. Я знаю, что я не все
знаю... и даже, зная, где проходит граница
моего знания, я, тем не менее, не знаю ее
положение в каждой точке абсолютно точно.
Поправьте меня, если кто знает лучше. А
сейчас, обратимся к нашему предмету...
Почему
мы должны держаться "примерно посередине"
и не можем мыслить ни только в терминах
языка высокого уровня, ни только в терминах
двоичных конструкций? До сих пор мы
рассматривали конструкции COM в границах
абстракций C++ - компилятор делает их
совершенно автоматически. И мы это ранее
упоминали. Но COM-то - двоичная технология! И
двоичные модули, которые вступают между
собой во взаимодействие, могут быть
написаны на совершенно различных языках. И
исходный их текст - недоступен по
определению. Это требует, чтобы двоичные
структуры, которые производят компиляторы
с разных языков были совместимыми хотя бы в
объёме, требуемом COM. (Для компиляторов Microsoft
это действительно так, про другие -
доподлинно не знаю.
Компилятор Delphi, например, совместим...) Если бы все модули и
компоненты COM писались, скажем, только на
C++ (или
- на любом другом, но - одном, языке), то этой
проблемы не было бы вовсе - сколько модулей
ни компилируй одним и тем же компилятором
везде он построит одинаковые структуры. А в
существующем положении вещей эта проблема
есть. Поэтому конструкцию "присоединительных
частей"
объектов и модулей знать нужно. Но едва ли
их практически нужно знать с точностью до того, где
какие байты лежат - это может показать и ваш
отладчик. Вы же не пишете на языке
Ассемблера? Важно знать, какие байты
обязательно должны лежать и где их надо бы
было искать, если в вашей практике такая
надобность появится.
Самая
главная неприятность этой проблемы состоит
в том, что она находится ровно посередине
между клиентом и сервером, поэтому "в ней
никто не виноват". Если учесть, что
одна из сторон взаимодействия - чужая
двоичная компонента и "заглянуть внутрь"
нее уже невозможно, то выдерживание
строгости протокола является единственной
мерой, которая бы могла быть эффективной
против ошибок такого рода. Например, ряд
читателей обратил внимание, что в описании
методов интерфейса употребляется
спецификатор __stdcall. Почему?
С++
- очень гибкий язык (сделайте в другом языке
вызов функции с переменным числом
параметров или откомпилируйте функцию без
пролога/эпилога?), а именно в данном случае
конструирования его гибкость является
скорее недостатком. Поэтому, определяя
проектные конструкции C++, предназначенные
для построения конструкций COM постоянно
приходится помнить о том, что компилятор
должен быть специально ограничен. Это - категория ошибок, причина которых очень
трудно диагностируется - ведь в разных
единицах компиляции порознь-то все
правильно! Например, стандартным
соглашением о связях для компилятора C++
является __cdecl, которое предписывает
вызывающей процедуре не только помещать
параметры в стек перед вызовом вызываемой
процедуры, но и самой очищать стек после
вызова. А вызываемая процедура этого не
делает. Это - единственная возможность
правильно оформить вызов функции с
переменным числом параметров. Во всех
других языках соглашение о связях - __stdcall,
которое предписывает вызываемой процедуре
самой очищать стек перед завершением. Стоит
написать клиента, который оформит вызов
метода в __stdcall (по умолчанию
для его языка), а сам метод
сервера будет написан в соглашении __cdecl (тоже
по умолчанию, но для C++), как вызов метода
будет приводить в разрушению стека
процесса и нарушению защиты памяти. Вы
сможете "с ходу" вспомнить почему бы
это могло быть?
Я
хочу подчеркнуть особо - эта ситуация
вообще не может быть "проконтролирована
автоматически". Избежать её можно только
аккуратностью кодирования всего, что
экспонируется наружу модуля. Необходимо
постоянно помнить, что "нормальное
внешнее имя" C++ - декорировано, т.е. в
двоичном модуле выглядит совсем не так, как
оно выглядит внутри исходного текста.
Необходимо помнить, что всякий
экспонируемый наружу метод должен быть
написан только в соглашении о связях __stdcall.
Необходимо помнить, что методы
экспонируемых интерфейсов не могут быть
перегружены. Эти ограничения следуют
только потому, что "другие компиляторы
этого не умеют", а COM - двоичная технология.
Другой
распространённой ошибкой отнимающей
пропасть собственной жизни программиста
является ... неидентичность интерфейсов,
используемых при сборке клиента и сервера.
Технология COM с этим научилась бороться, как
именно, мы до этого ещё дойдем. Но в C++
интерфейс описывается только чисто абстрактным
классом. Соответственно, где-то существует
файл, подаваемый компилятору, где все эти
классы и перечислены - файл с определением
всех интерфейсов. И это только в теории "интерфейс
- никогда не изменяемая сущность".
Пока интерфейс не опубликован и
разрабатывается, программист, естественно,
его иногда и изменяет. Изменять-то изменяет,
а вот всё ли (всех клиентов) после этого
перекомпилирует и пересобирает? Об этом
тоже всегда следует помнить - например, я,
сочиняя пример
№1, на
эти грабли наступил, хотя наступал уже
такое количество раз, что мог бы и научиться
:)
Поэтому
точное знание "какая точно конструкция
языка есть интерфейс
в COM"
для программиста - насущная необходимость.
В языке C++ всякий интерфейс описывается
структурой - структура это класс, все члены
которого являются public. Не исключено и
описание интерфейса самой конструкцией class.
В включаемом файле <basetyps.h> имеются такие
определения конструкций для определения
частей интерфейса:
#define STDMETHOD_(type,method)
virtual type STDMETHODCALLTYPE method
...
#define DECLARE_INTERFACE(iface)
interface iface
#define DECLARE_INTERFACE_(iface, baseiface)
interface iface : public baseiface
а
в файле <wtypes.h>:
typedef LONG HRESULT;
В файле же <unknwn.h> (с небольшими сокращениями и упрощениями, пока
несущественными для нашей рассылки) известный нам по предыдущей рассылке
интерфейс описан как:
Ну
и в завершение сегодняшнего выпуска немного
о типе HRESULT - это тип
стандартного значения, которое должен
возвращать COM-метод. Все методы любых
интерфейсов, кроме
методов IUnknown::AddRef и IUnknown::Release, обязаны
возвращать значение именно этого типа.
Структура сообщения о состоянии,
возвращаемого через HRESULT, тоже определена на
уровне системной спецификации и мы
рассмотрим эту спецификацию немного позднее, в
соответствующей теме. Когда именно
проблема возвращения кода ошибки из
сервера клиенту для нас станет актуальной. А
пока, в следующей
рассылке, мы возвращаемся к основному
интерфейсу COM - к интерфейсу IUnknown.