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

COM - об известных граблях...

Мы уже знаем точные структуры, которые требуются для организации взаимодействия объектов клиента и сервера, располагающихся в разных модулях. Мы знаем протокол этого взаимодействия. Мы уже сочинили первый работающий пример, иллюстрирующий нашу рассмотренную ранее теорию и выяснили чего ещё не хватает для продвижения вперёд. Но программирование - точная наука. Есть мелкие детали, которые мы считали само собой разумеющимися, но которые таковыми, к сожалению, не являются. О них и речь. Тем более, что я получил целую серию однотипных вопросов, авторов которых можно условно разделить на два "лагеря".

Авторы из одного лагеря имеют некоторое недоумение относительно нашего постоянного упоминания "что у COM внутри в двоичном коде", чем, собственно, и недовольны. Авторы из противоположного лагеря, напротив, пеняют, что я неточно излагаю "что у COM внутри" и что "вон те байты вот не там расположены".

Уважаемые читатели! Возможно, что я что-то недопереоценил и это обстоятельство нужно высказать явно. Суть нашей серии - совсем не "технология COM". А - программирование и архитектурное проектирование из двоичных компонент на примере одной из известных (и бесплатных!) технологий для этого - COM. Но окружающая действительность такова, что подавляющее большинство программистов мыслят сугубо конкретными категориями, а то и ещё хуже - категориями одной известной им из собственной практики конкретной реализации. Просто потому, что иначе - не умеют. И мне приходится сочинять что-то "синтетическое", которое, понятно, не всегда оказывается оправдывающим ожидания тех или иных читателей. Простите великодушно!

Я же пытаюсь двигаться путём, который как-то соединяет оба подхода - на примере конкретной реализации пытаюсь показать сущность подхода к программированию и архитектуре. Пытаюсь показать "вечные ценности" и, прежде всего, - философию взгляда. Своего, естественно, взгляда, ведь другого у меня нет. А ваша задача - дать себе труд построить свой собственный взгляд. Пользуясь и моим взглядом и другими источниками информации, как некоторой опорой. Заранее понятно, что ваш взгляд будет в чём-то отличаться от моего - это отличие не есть основание считать, что "доцент - тупой". Но ведь и рассматривать мои статьи как "сборник рецептов" - также бессмысленно. Мне не хватит жизни, чтобы этот сборник сделать, а у вас для формирования своего взгляда есть и собственная жизнь и собственная практика. Поэтому предельно конкретные значения параметров вы всегда сможете прочитать в MSDN или в учебнике, где объясняется "как вызвать Wizard", важно, чтобы программист понимал существо, стержень, каркас. А что на этот каркас можно навесить и как именно - на то есть собственная голова творца, это даже и перечислить невозможно. Поэтому я и просил бы относиться к серии данных статей соответственно - она не справочник, она - учебник. И учебник, который ближе стоит не к учебнику ремесла, а к учебнику философии. Я очень надеюсь, что она сможет принести чуть больше пользы, чем написано в ее заглавии. Кроме того, COM - развивается. Появляются новые возможности, интерфейсы, значения констант... Даже "мгновенно" я не обладаю всеми знаниями о 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 STDMETHODCALLTYPE __stdcall #define interface struct #define STDMETHOD(method) virtual HRESULT STDMETHODCALLTYPE method #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> (с небольшими сокращениями и упрощениями, пока несущественными для нашей рассылки) известный нам по предыдущей рассылке интерфейс описан как:

class IUnknown{   public:   virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid,void ** ppvObject) = 0;   virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;   virtual ULONG STDMETHODCALLTYPE Release(void) = 0; };

из чего можно заключить, что стандартный интерфейс это:

struct{       ...

или

class{   public:       ...

а стандартный метод, экспонируемый интерфейсом, должен иметь атрибуты:

virtual  long __stdcall <имя метода>(<<список  параметров метода  >) = 0;

 Учитывая, что в среде компиляции определено макро STDMETHOD(method), методы в интерфейсе можно описывать и так:

STDMETHOD(<имя  метода>)(<список параметров метода>) = 0;

Ну, и в завершение сегодняшнего выпуска немного о типе HRESULT - это тип стандартного значения, которое должен возвращать COM-метод. Все методы любых интерфейсов, кроме методов IUnknown::AddRef и IUnknown::Release, обязаны возвращать значение именно этого типа. Структура сообщения о состоянии, возвращаемого через HRESULT, тоже определена на уровне системной спецификации и мы рассмотрим эту спецификацию позднее, в соответствующей теме. Когда именно проблема возвращения кода ошибки из сервера клиенту для нас станет актуальной. А пока, в следующей статье, мы возвращаемся к основному интерфейсу COM - к интерфейсу IUnknown.

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

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

форум

технология COM

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


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