Рассылка закрыта
При закрытии подписчики были переданы в рассылку "Мастерская программиста" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
Что такое "технология COM" и как с ней бороться?
Информационный Канал Subscribe.Ru |
М. Безверхов
vasilisk@clubpro.spb.ru
Что такое "технология COM" и как с ней бороться? №45
"Косой" на связь не вышел...
После столь долгого перерыва продолжим. Напомню, что, как обозначено в предыдущем номере рассылки, речь идёт о передаче состояния ошибки от одного компонента другому, между которыми существует "разрыв контекста процесса". Термин "состояние ошибки" является в известной мере условным, поскольку с тем же успехом можно бы было говорить и о передаче "состояния успешности" выполнения метода, но уж так исторически сложилось, хотя оба термина равноправны и выражают одно и то же. И оба же с разной частотой употребляются.
Начнём, как и подобает, "от начала" - сегодня мы рассмотрим передачу ошибки "второго рода" (в терминах предыдущей рассылки), а в следующем номере - передачу ошибки "первого рода". После этого тему технической передачи ошибки можно будет считать исчерпывающе завершённой. Но - отнюдь не тему передачи ошибки как явления в программе, поскольку к теме возможного построения программы с учётом удобной обработки ошибок мы будем ещё не раз возвращаться.
Итак, в наших терминах "ошибка второго рода" есть ошибка, передаваемая посредством кода возврата метода типа HRESULT. Она является сугубо технологическим параметром, т.е. её наличие вообще-то, говорит о том, что семантические свои функции метод выполнил неуспешно, но вот по какой именно "пользовательской" причине - ошибка эта не сообщает. Или - не может, или - не должна сообщать.
Столь интересное и внутренне противоречивое утверждение возможно потому, что так было не всегда. История не сохранила для нас имени того программиста, который первым додумался до того, что код возврата функции, достающийся в программе практически бесплатно, можно использовать не для передачи "значения" (как это делается в функции математической), а для передачи служебной информации. Но его изобретение оказалось поистине революционным - ему удалось логически разделить передачу семантики функции и передачу служебной информации о ходе её выполнения. Причём, и я это подчеркну, передача этой служебной информации делается "автоматически" и практически "бесплатно", т.е. если программа по каким-либо причинам не заинтересована в получении и обработке подобной информации она может просто этот код возврата проигнорировать. И это не влечёт за собой расхода каких-либо программных ресурсов.
По мере того, как изощрённость отдельных программных компонентов (читай - функций) нарастала, нарастало и значение передаваемой служебной информации. Появились специальные приёмы программирования, которые наиболее эффективным образом позволяли отреагировать на необычный код возврата функции, при этом не сильно затемняя логику "нормального исполнения" программы. Логически это завершилось введением аппарата т.н. "исключений" прямо в текст программы, но важно то, что в современных программах, в общем случае, отмахнуться от информации о ходе исполнения подпрограммы (функции, метода) - нельзя.
Естественно, что в данных условиях возникло стремление хотя бы унифицировать "код возврата функции", т.е. обязать все функции одной программной системы возвращать значение одного типа и одной семантической структуры. Пока системы были простыми (не сложнее DOS) это выглядело как сплошая сквозная нумерация возможных в системе ошибок. Когда системы стали сложнее и возникла необходимость выделять не только "ошибку", но и "подсистему" в которой ошибка произошла, код возврата приобрёл структурность - в нём стали выделяться группы битов, которые кодируют собственно ошибку и группы битов, которые кодируют подсистему её вызвавшую. Это оказалось важным, поскольку одно и то же по своей семантике событие (например - ошибка чтения файла) могло произойти в разных программных компонентах совокупной системы и просто информация "ошибка чтения файла" перестала иметь достаточное для практики значение - для того, чтобы понять её причину требовалось знать не только "что", но и "где".
Очень сложные системы уже не удовольствовались дихотомией "ошибка - отсутствие ошибки", поскольку возникали ситуации, которые "не дотягивали" до состояния "ошибка", но, тем не менее не были и состоянием "нормального исполнения". Любой читатель, наверное, может вспомнить самый обычный компилятор, который выдаёт сообщения класса warning, error, severe error и fatal error.
Всю эту историю развития представлений о коде ошибки в операционной системе мы можем наблюдать воочию хотя бы на примере файла winerror.h, включенного в поставку Visual Studio. Кодов ошибок там определено много, но "ранние ошибки" являются просто номерами, более "поздние" ошибки определяются ещё и применительно к подсистеме, а уж совсем "современные" коды ошибок структурированы по формально определённым правилам и включают в себя и обозначение степени грубости (прописанное, впрочем, довольно невнятно).
COM, как довольно "поздний ребёнок", уже не застал простых систем и с самого начала ориентировался на то, что код ошибки должен быть структурирован, причём с явным запасом на дальнейшее развитие. Поэтому "ошибка" в COM изначально была хорошо проработана на уровне всей системы, была определена не только структура кода ошибки, но и довольно подробно описано в каких случаях какая конструкция должна применяться. Это, конечно, здорово облегчает жизнь, особенно в тех случаях, когда серверную часть программы пишет один программист, а клиентскую - другой.
Но всё это - уже история. А в современности читателям известно, что в COM любой метод любого интерфейса (исключение составляют только два метода AddRef и Release интерфейса IUnknown) обязан возвращать значение типа HRESULT. Строго говоря, тип HRESULT определён только в 32-хбитных системах. Ранее, в 16-тибитных системах, OLE для возврата своего состояния определял тип SCODE. Оба этих типа представляют собой 32-хбитное целое и имеют несколько различающуюся внутреннюю структуру. Структура кода типа SCODE (более раннего) приведена на рис. 12, а структура кода типа HRESULT (более позднего) - на рис. 13:

рис 12. Структура типа SCODE

рис 13. Структура типа HRESULT
Назначение полей кодов определяется следующим образом:
- S - severity - признак ошибки. Имеет два значения: SEVERITY_SUCCESS (0), сообщающее, что код возврата описывает "не ошибку" и SEVERITY_ERROR (1), сообщающее, что имеет место именно "ошибка";
- Сontext - контекст [системы] в котором произошла ошибка;
- Facility - особенность, которое так же кодирует и контекст подсистемы вызвавшей ошибку;
- Code - собственно код, описывающий семантику ошибки;
- Другие поля - зарезервированы.
Видно, что HRESULT является своего рода проэволюционировавшим преемником SCODE, в частности, поле S в обеих структурах располагается на одном и том же месте, да и состав полей выражает почти одно и то же. Поле Context изначально предназначалось для кодирования системы, которая вызвала ошибку, а поле Facility - для кодирования более мелкой единицы в составе этой системы. Нетрудно видеть, что в целом код приводился к некоторому единому перечислителю всех ошибок в системе. Такой подход влёк за собой необходимость единого нумератора, обеспечить функции которого при разработке многих программных систем многими независимыми разработчиками, естественно, было невозможно. Видимо поэтому Microsoft зарезервировала использование поля Context (хотя в разработке своих систем им пользовалась). А вот поле Facility оказалось более удобным, поскольку типовые "мелкие части" можно было выделить в каждой системе и их семантика либо вовсе не зависела от назначения "большой системы", либо особенность относилась к тому компоненту, который присутствовал в каждой системе. Типовые константы, кодирующие значение поля Facility приведены ниже:
FACILITY_NULL | Случай "общего применения", такой, как S_OK. Должен использоваться всюду, где нельзя определить более точного значения. |
FACILITY_ITF | Применяется для большинства кодов состояния возвращаемых методами интерфейсов, значение определяется интерфейсом. Т.е. совершенно одинаковые значения этого кода, возвращаемые разными интрефейсами могут выражать совершенно разную семантику. |
FACILITY_DISPATCH | Используется интерфейсом IDispatch. |
FACILITY_RPC | Используется подсистемой RPC. |
FACILITY_STORAGE | Используется методами интерфейсов IStorage или IStream. Значения поля Code в данных кодах возврата совпадает с кодами DOS и имеет ту же семантику, что и коды DOS. |
FACILITY_WINDOWS | Используется интерфейсами Microsoft. |
FACILITY_WIN32 | Используется подсистемой Win32® API. |
Можно заметить, что при использовании такой кодификации Microsoft просто разделяет все коды ошибки/состояния на "от Microsoft" и на "все остальные" оставляя при этом ещё и место для того, чтобы инкорпорировать коды ошибок старых систем, таких, как DOS. Но, вместе с тем, код как раз и становится "технологическим", поскольку значение FACILITY_ITF красноречиво и явно заявляет - совпадающие коды могут выражать совершенно разный смысл. Ну а поле Code в данном случае отдаётся на безраздельный откуп самому программисту-разработчику.
Обдумаем полученную информацию. С одной стороны такое деление удобно тем, что существует некоторое количество заранее предопределённых кодов "типовых ошибок", которыми просто нужно пользоваться не выдумывая ничего своего. Например, если метод любого интерфейса завершается неуспешно по причине нехватки памяти, то он должен вернуть именно значение E_OUTOFMEMORY. И ряд клиентов уже запрограммированы таким образом, что могут как-то особо отреагировать на эти стандартные коды - например, выдать пользователю не типовое, а какое-то специальное текстовое описание ошибки. С другой стороны код состояния, по определению, делается способен описать уже только "ограниченную семантику" и для передачи "пользовательских состояний ошибки" он становится не очень пригоден, особенно, когда разных "ошибок уровня пользователя" в проектируемой системе возможно много.
Но, с третьей стороны, внятное разделение на ошибки "уровня системных абстракций" и ошибки "уровня пользовательских абстракций" с обеспечением отдельных механизмов их передачи произошло в COM сравнительно недавно. Ранее, а в некоторых системах такое положение сохраняется и поныне, код ошибки мог передавать и семантику пользовательской системы. И эту особенность структура кода состояния сохранила - FACILITY_NULL как раз и позволяет свести набор возможных ошибок к простому их перечислителю в данной системе. При этом, программист (за исключением предопределённых операционной системой кодов) абсолютно свободен в назначении кодов ошибок подсистемы и не очень ограничен в назначении кодов подсистем, важно, чтобы они не пересекались с существующими кодами поля Facility, чтобы не вводить в заблуждение пользователя. Т.е. я хочу сказать, что если программист для определённых им самим ошибок выберет старую структуру кода SCODE и использует поле Context так, как его когда-то использовала Microsoft, это не будет слишком некорректно. Так не стоит делать в том случае, если программист хочет полностью выдержать современную спецификацию COM для передачи ошибки - следует пользваться передачей объектов ошибок, которые мы рассмотрим в следующем выпуске. Но не исключено, что использование "нового механизма" будет чем-то избыточно в конкретном случае разработки. В таком случае можно использовать "старый подход" - он тоже будет корректен для различения состояний "ошибка - отсутствие ошибки", во всяком случае этот подход совместим и с новыми программами-клиентами.
Всё сказанное выше касалось "переменных". Естественно, что и символьные обозначения констант, кодирующих состояния или ошибки тоже имеют структурность. Правила её выражения требуют, чтобы обозначение константы записывалось в виде:
Facility_Severity_Reason
где
- Facility - обозначение значения Facility
- Severity - S или E, обозначающие успешность(S) или провальность(E) выполнения,
- Reason - обозначение величины в поле Code
Значение FACILITY_NULL - опускается, как, например в коде E_NOINTERFACE, А значение Faclity отличное от FACILITY_NULL отмечается, как, например, в коде STG_E_FILENOTFOUND. Описанные правила - не более, чем соглашение, подобное тому, в котором идентификаторы переменных полезно снабжать префиксом описывающим их тип (польская нотация). Этим правилам можно, хотя и не следует, и не следовать. (Простите за каламбур!)
Для работы с значениями кодов состояния/ошибки среда программирования располагает рядом макроопределений, которые упрощают и делают более прозрачным исходный текст программы, поскольку просто сравнение кода ошибки с нулём - некорректно. Хотя значение кода, обозначающее отсутствие каких-либо ошибок (нормальное исполнение) S_OK и равно нулю, но любой "код ошибки" в котором поле S не установлено в 1 не считается ошибкой, поэтому работа с таким сложным по структуре кодом делается в несколько этапов.
Макроопределения FAILED(x) и SUCCEEDED(x) [всегда в этом слове путаю, где сколько буковок ставить!], где x - проверяемое значение кода, расширяются в программный код, который проверяет значение бита S и возвращает логическое значение. Нетрудно догадаться, что если бит S установлен, то макро FAILED вернёт значение true, а макро SUCCEEDED возвращает true в прямо противоположном случае.
Код типа HRESULT считается неделимым, т.е. опознание, что перед нами именно то самое значение кода ошибки производится простой операцией сравнения с константой. Но, если имеется надобность выделить именно значение полей кода, то к услугам программиста имеется ряд макро, котрорые это делают. Макро HRESULT_CODE(x), HRESULT_FACILITY(x) и HRESULT_SEVERITY(x) возвращают значения соответствующих полей кода x, причём не просто "выкусывая" их из кода, но и сдвигая к 0-му биту. Пользуются ими приблизительно так:
HRESULT hr = obj.Method();
if(FAILED(hr)){
//выполняем обработку возникшей ошибки
...
int code = HRESULT_CODE(hr);
...
}
Ну, и наконец, если у программиста возникает надобность не пользоваться константными значениями кодов, а сгенерировать код по месту, к его услугам есть макро MAKE_HRESULT(sev,fac,code), которое из компонентов делает код с правильным сдвигом полей внутри результирующего значения. Что есть что в списке его аргументов, я думаю, вполне понятно и ещё раз комментировать это не нужно.
Закончим же данную тему небольшой порцией философии. Сказанное, если так можно выразиться, является только технологическим изложением, т.е. оно описывает "как гайку крутить". А вот где эту "гайку" конструктор должен предусмотреть... Но я хочу, раз пришлось к слову, заметить - очень великий самообман программиста состоит в том, что "ошибки исполнения программы" он считает событиями либо второго сорта, либо вообще - досадными исключениями, которые просто "портят жизнь". Это - исключительно неправильный взгляд на вещи, поскольку социальное качество программы (её удобство и вообще пригодность для того, чтобы с ней работали разные люди - программисты, менеджеры по продажам, конечные пользователи и т.д.) определяется не тем, что в 99% случаев она "правильна" и как с ней управляться в этих случаях пользователи либо знают, либо их этому специально учат. А как раз - тем самым редко возникающим 1% (а то и менее), когда реакция программы для человека оказывается неожиданной и что в этом случае делать - не знают ни сама программа, ни этот человек.
Для программиста это измерение имеет особое выражение, поскольку программист, с одной
стороны, обязан предусмотреть "все возможные ошибки", а с другой - очевидно не имеет никакой
возможности это сделать. Поэтому обработка ошибки не просто "какой-то второстепенный код",
а, вообще
говоря, самая настоящая подсистема в составе проектируемой программной системы. А даже и
одна из самых важных подсистем - любая подсистема может отказать, а вот подсистема обработки ошибок, наверное,
отказывать не должна? Внимательный читатель заметил, что выше по тексту я всё время
употреблял термин "система" вместо термина "программа" - если бы мы жили в мире программ,
то, видимо, кроме "правил перенумерования ошибочных состояний" и сказать было бы нечего.
Я еще застал то время, когда появившееся на консоли сообщение "Ошибка 0017" было исключительно
информативным и обслуживающий персонал либо сразу, либо после прочтения соответствующего
толстого руководства знал, что делать. Но время это давно прошло - протестировать все
возможные состояния современного программного комплекса даже средней сложности невозможно.
А, значит, системный подход к проектированию таких изделий является определённого рода
гарантией их качества. И подсистема обработки ошибок (во всяком случае хорошее логическое
проектирование поведения системы в данных случаях и корректной обработки таких случаев)
может выступить одним из стержней, вокруг которых можно построить целую систему - ведь в
первую очередь должны проектироваться более надёжные компоненты, а по убывающей надёжности -
остальные? Поэтому "ошибка", "средства обработки ошибки" и "реакция на ошибки" являются
никакими не второстепенными, а как раз самыми фундаментальными основами системы. Это,
собственно, и является причиной того, что простой передаче ошибки мы посвящаем целых
два выпуска нашей рассылки. Можно было бы посвятить и больше. О более же "высоких" аспектах ошибки - следующий выпуск.
Информация для новоприсоединившихся читателей:
- У рассылки есть архив, ссылка на который присутствует в каждом выпуске рассылки;
- Архив содержит все выпуски рассылки;
- Сайт, на котором рассылка пребывает, предназначен для программистов,а поэтому содержит и другую интересную программисту информацию;
- Автору рассылки и вообще администрации сайта было бы интересно узнать ваше мнение о том, что ещё стоило бы освещать по теме "программирование".
Авторские права © 2001 - 2002, М. Безверхов
Публикация требует разрешения автора.
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||