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

Ошибка резидента

Продолжаем наше изложение. Я полагаю, что не только теоретики, но и программисты-практики читают наши статьи, а потому надо и на их улице устроить праздник. В предыдущей статье мы упомянули одно интересное обстоятельство - разрыв контекста процесса, в том случае, если клиент и сервер располагаются в разных процессах. Нужно заметить, что использование механизма proxy/stub в организации связи компонентов в среде компонентной разработки - норма. Т.е. компоненты всегда связываются посредством proxy и stub и никогда - как-то иначе. А поэтому программист просто обязан всегда предполагать разрыв этого контекста. Как же тогда понимать inproc-взаимодействие? А понимать его нужно так - inproc-взаимодействие в COM является своего рода вырожденным случаем, который использует "нуль-proxy". Как мы видели ранее, отнюдь не этот случай определяет возможности COM. Напротив, мы специально всё время ограничивали его возможности. Теперь можно понять почему - в ином случае компоненты (хотя бы гипотетически) не смогли бы работать через proxy/stub, а они - обязаны обладать такими способностями, иначе это - не COM. Широты кругозора ради нужно сказать и о том, что в CORBA компоненты физически всегда взаимодействуют через proxy/stub, т.е. в данном стандарте нет inproc-взаимодействия. Почему же тогда Microsoft ввела (или - оставила) inproc-взаимодействие в COM? Видимо, ответ существует только один - эффективность. В отличие от CORBA, которая заведомо задумывалась как среда распределённых вычислений, COM "вырос снизу", т.е. первоначальной целью COM (тогда ещё целиком содержавшегося в недрах OLE) не было обеспечить среду распределённых вычислений, но - только возможность представить пользователю "составной документ". А, как мы видели, inproc-взаимодействие почти ничем и не отличается от вызова процедуры из DLL, т.е. накладные расходы на именно связь - практически не увеличиваются по сравнению с вызовом обычной процедуры из DLL. Вместе с тем COM обеспечивает ряд удобств для программиста - возможность не знать формат сервера и его местоположение, возможность простого версионирования сервера, отсутствие линковки к клиенту даже той маленькой библиотечки *.lib, которая образуется при изготовлении DLL. Видимо, эти обстоятельства и послужили причиной того, что не только в COM осталось inproc-взаимодействие, но и что оно в операционной системе понемногу вытесняет стандартный вызов функции из DLL, сохраняя все преимущества последнего.

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

Сегодня мы поговорим о ещё одном, весьма практически важном, обстоятельстве той же самой природы - о передаче ошибки в COM. Поскольку COM весьма эффективно инкапсулирует свои внутренние структуры, то начинающие программисты, порой, и вовсе не догадываются, что всё, что они видят на уровне клиента - только иллюзия, что механизмы передачи вызова в другой процесс основаны на других принципах. Как следствие, это можно косвенно заподозрить, но вот как с этим правильно бороться? (Ответ-то известен - читать нашу серию :)

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

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

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

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

Для обеспечения таких своих функций система располагает т.н. "контекстом процесса", который включает в себя всё системное сопровождение (обработчики, перехватчики, процедуры аварийного завершения и т.д.) данного процесса. Существует и "контекст модуля", состоящий из структур аналогичного назначения, входящих в состав модуля. Исключение, возникшее в коде, локализуется относительно "контекста модуля", программисты на C++ это очень хорошо знают - исключение в тексте модуля можно поймать и на самом верхнем уровне вызова процедуры, в какой бы глубине оно ни произошло. Если в "контексте модуля" исключение не обрабатывается, то в дело вступает "контекст процесса", а там - и до завершения недалеко...

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

Ложность этого предположения выясняется сразу же, как только оказывается, что эта технология не срабатывает при перехвате ошибок второго рода в случае local-взаимодействия - несмотря на то, что в inproc-варианте "всё работало", в local-варианте сервер будет "вылетать". А это - нарушение принципов COM, которые требуют, чтобы клиент не видел разницы (возможно - кроме времени выполнения) в зависимости от того, каким образом он взаимодействует с сервером.

Причины, я думаю, теперь понятны - системный перехват ошибки второго рода действителен только в пределах контекста процесса, а они - эти контексты - в общем случае вызова COM-метода не одинаковы. Как следует поступать, тоже, наверное, очевидно - все ошибки второго рода должны быть перехвачены и превращены в ошибки первого рода исключительно в пределах того контекста модуля, в крайнем случае - процесса, в котором возникли. Применительно к inproc-серверу это означает только одно - при вызове любого метода COM-сервера этот метод не должен распространять исключение за пределы самого себя, т.е. абсолютно все исключения должны быть перехвачены в пределах той же DLL, где возникли. Они должны быть превращены в состояния ошибки первого рода и возвращены "как положено" - через HRESULT метода. Это сама по себе - не очень простая задача, поскольку изящный перехват исключения возможен не в пределах формально определённой процедуры, но в пределах некоторого функционального фрагмента, который, в общем случае, включает в себя и структуры клиента. Во всяком случае этот функциональный фрагмент начинается именно в клиенте, может включать в себя много вызовов разных методов сервера и очень изящно такие исключения ловить именно в клиенте, в целом для всего фрагмента.

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

Однако, и сама трактовка ошибки в COM несколько отличается от таковой в привычных технологиях программирования. Необходимость преобразовывать ошибку второго рода в ошибку первого рода - исключительно технологическая, она не вызывается семантикой самой ошибки. А HRESULT кода возврата метода - один. И клиенту COM, логике его программы, в общем случае, не должно быть дела до ошибок второго рода, где бы они ни возникали. Поэтому в COM код возврата метода должен использоваться "в технологических целях", а именно - для передачи состояния либо ошибки второго рода, либо вообще информации о технической успешности вызова метода. Состояния действительно ошибок первого рода, т.е. ошибок уровня пользовательских абстракций не должны передаваться посредством HRESULT. Для их передачи в COM существует специальный аппарат, очень похожий на аппарат errno или GetLastError/SetLastError. Представлен этот аппарат специальным, обеспечиваемым слоем поддержки COM, "объектом ошибки", который является полнофункциональным COM-объектом со своими интерфейсами. Сервер, пользуясь специальной функцией API SetErrorInfo получает доступ к интерфейсу этого объекта IErrorInfo и заполняет его значениями. Клиент, получив "технологический" код ошибки и узнав, что имеется информация об ошибке, извлекает заполненный сервером объект ошибки посредством функции API GetErrorInfo и узнаёт из него всё, что хотел до клиента донести сервер.

Указанный механизм кажется громоздким - какая разница, как передавать состояние ошибки, если сервер гарантированно не должен никогда терять потока управления? Но разница есть и оказывается весьма существенной, если вспомнить, что в случае local-взаимодействия клиент не вызывает сервер, а вызывает-то proxy. И код возврата в виде HRESULT возвращает ему не сервер, а proxy. И у proxy тоже могут возникать ошибки, которые в таком случае, к клиенту-то вот точно отношения не имеют. Аналогичное можно сказать и про сервер - возвращаемый методом сервера HRESULT не попадает к клиенту, а попадает к stub, который может в каких-то случаях и не передавать его на сторону клиента - семантика только связи компонентов и семантика собственно содержательной обработки являются разными семантиками, при этом связь - от клиента скрыта и ошибки именно связи для него не предназначены.

Поэтому передача ошибки посредством специального, обеспечиваемого системой объекта, является семантически правильной для изоляции механизма связи от того взаимодействия клиента и сервера, которое реализовано поверх него.

Но сказанное - не догма. Это не тот случай, когда следует делать именно так, поскольку иначе ничего работать не будет. Сказанное - только "нормы поведения", которые должен выдерживать сервер, чтобы уметь работать со "стандартными клиентами". Visual Basic - именно такой клиент, т.е. VB не воспринимает ошибки, которые сервер передаёт посредством HRESULT. Точнее сказать - он воспринимает их только на уровне среды исполнения программы, а в программу пользователя (написанную на VB) он их не доставляет. Если же сервер хочет доставить ошибку именно в программу клиента, он обязан будет пользоваться интерфейсом IErrorInfo...

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

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

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

форум

технология COM

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


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