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

"Надежда" работает под контролем

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

Я напомню, что в предыдущей статье мы рассмотрели структуру кода HRESULT и обращение с ошибкой, которую мы ещё ранее классифицировали как "ошибку второго рода", а сейчас должны рассматривать обработку ошибки "первого рода". И мы её и рассмотрим.

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

К сожалению таких авторов, я должен их дважды разочаровать. Во-первых, и сама излагаемая ими проблема не нова. Она была отмечена ещё Д.И. Фонвизиным и в те времена формулировалась так: "Зачем географию учить, когда извозчики и так довезут?". Во-вторых -.NET построена поверх, или, поменьшей мере - с использованием COM... :) А вот, с другой стороны, меня радует появление таких писем – они означают, что в нашу читательскую аудиторию влились и продолжают вливаться свежие силы, как раз те, которым "рассылка по COM" и была бы особенно полезна. И, если читатель не просто молча отвернулся, а всё же сочёл нужным высказать мне своё, пусть пока и такое, мнение – ещё не всё потеряно. Я не буду больше вступать в дискуссии по таким вопросам – на протяжении всего изложения я уже высказался немало по этому поводу, поэтому заинтересованные читатели просто отсылаются к архиву статей.

Мы же возвращаемся к теме изложения – существует механизм передачи "ошибки первого рода". Ранее отмечалось, что его необходимость возникает по той причине, что программисту в COM одновременно видимы два концептуальных уровня – уровень служебной функциональности самого COM и уровень абстракций пользовательской программы. И их желательно как-то разделять. А вот как пользоваться таким разделением и что в него должно попадать мы сейчас попробуем догадаться, рассматривая саму концепцию деления системы на уровни абстракции.

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

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

Допустим, что сказанное - тривиально. Тогда вопрос – одно и то же первичное событие, например, "сигнал контроллера диска об ошибке чтения сектора", должно ли оно интерпретироваться одинаково слоем чтения секторов, слоем компоновки из них кластеров и слоем обработки файла в целом? Конечно – нет! Это – разные абстракции и в каждой из них это одно событие будет иметь свою семантику, т.е. одновременно с преобразованием уровня абстракции мы должны преобразовывать уровень и событий, обрабатываемых данным слоем. Вообще любых событий, с которыми имеет дело данный слой, т.е. некоторые из них мы можем не поднимать на уровень выше, некоторые – заменять другими событиями, третьи - генерировать программно. Обработка событий может быть произвольной и определяется семантикой слоя-обработчика. Естественно, что сказанное в полной мере относится и к "событиям ошибки". А... вот как это сделать?

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

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

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

Этим радикально решаются завязанные в единый узел проблемы. Во-первых, пользовательская оболочка не обязана уметь транслировать коды ошибок в сообщения пользователю, а, значит, её теперь действительно может касаться только "наличие ошибки исполнения метода – отсутствие ошибки". Во-вторых, "код ошибки" обязан быть уникальным только в пределах даже не всего компонента, а только слоя или даже объекта или интерфейса объекта (FACILITY_ITF из предыдущей статьи), что, естественно, реализуется намного легче, чем разработка глобально непротиворечивой системы кодов ошибок. В-третьих, если данный объект/компонент – не самый верхний, т.е. вызывается не пользователем, а каким-то объектом/компонентом ещё более высокого уровня, то тот компонент по-прежнему располагает, пусть и преобразованным, но технологическим кодом ошибки типа HRESULT. И может построить на нём анализ и преобразование абстракций своего уровня, а текстовую строку просто проигнорировать, если она ему не требуется.

Вот, собственно, и всё... осталось только рассмотреть детали реализации описанного механизма, который в COM просторечно называется "механизм IErrorInfo".

Итак, в COM существует т.н. "объект ошибки", который экспонирует интерфейс IErrorInfo. Объект этот обеспечивается слоем поддержки COM, поэтому реализовывать его самому не нужно. То, что это именно "объект" - артефакт реализации, поскольку давно подмечено, что в COM значительно легче организовать передачу специального объекта, нежели передачу обычной заполненной данными структуры. В рассматриваемом же нами случае "объект ошибки" по своему содержанию как раз такая структура и есть - достаточно только взглянуть на состав методов интерфейса IErrorInfo (методы 8 - 12):

1.QueryInterface
2.AddRef
3.Release
 
4.GetTypeInfoCount
5.GetTypeInfo
6.GetIDsOfNames
7.Invoke
 
8.GetDescriptionВозвращает текстовое описание ошибки
9.GetGUIDВозвращает GUID интерфейса, определившего ошибку
10.GetHelpContextВозвращает контекст подсказки, относимый к данному случаю возникшей ошибки
11.GetHelpFileВозвращает спецификацию файла подсказки, которая содежит требуемый раздел, описывающий ошибку
12.GetSourceВозвращает зависящий от языка идентификатор объекта или программы, вызвавших ошибку

Сам интерфейс IErrorInfo наследует пока не изученному нами интерфейсу автоматизации IDispatch (методы 4 - 7), поскольку описанная выше проблема "перевода на человеческий язык" возникла именно в эпоху господства в эволюции COM автоматизации. Интерфейс IDispatch – сам по себе есть отдельный и очень большой раздел COM, но, если вы используете интерфейсы, от него унаследованные, "как обычно", то вы можете просто проигнорировать это знание – ничего специфического для обращения с интерфейсом просто сам факт наследования от IDispatch не вносит. Кроме, конечно, дополнительно занимаемых четырёх ячеек vtbl.

Интерфейс IErrorInfo – интерфейс, предназначенный для извлечения клиентом уже установленной сервером в "объект ошибки" информации. Изменить её в объекте ошибки клиент не может. А вот для того, чтобы её первично установить, "объект ошибки" реализует и второй, парный к описанному, интерфейс ICreateErrorInfo. Он тоже является наследником интерфейса IDispatch:

1.QueryInterface
2.AddRef
3.Release
 
4.GetTypeInfoCount
5.GetTypeInfo
6.GetIDsOfNames
7.Invoke
 
8.SetDescriptionУстанавливает текстовое описание ошибки
9.SetGUIDУстанавливает GUID интерфейса, определившего ошибку
10.SetHelpContextУстанавливает контекст подсказки, относимый к данному случаю возникшей ошибки
11.SetHelpFileУстанавливает спецификацию файла подсказки, которая содежит требуемый раздел, описывающий ошибку
12.SetSourceУстанавливает зависящий от языка идентификатор объекта или программы, вызвавших ошибку

Работает вся механика так. Объект ошибки реализован в Oleaut32.dll, т.е. его сервис доступен в операционной системе всегда. Существует функция системного APICreateErrorInfo:

HRESULT CreateErrorInfo(ICreateErrorInfo  **pperrinfo);

которая создаёт новый объект ошибки и возвращает вызывающей процедуре ссылку на интерфейс ICreateErrorInfo этого объекта. Пользуясь методами этого интерфейса сервер заполняет объект значениями данных. Далее, сервер должен получить у объекта ошибки ссылку на интерфейс IErrorInfo и должен вызвать другую функцию системного APISetErrorInfo:

HRESULT SetErrorInfo(DWORD dwReserved,IErrorInfo  *perrinfo);

которая "отцепляет" объект ошибки от сервера и "прицепляет" его к системному механизму передачи этого объекта на сторону клиента. Естественно, что это "отцепление/зацепление" рассматривается как удерживание ссылок на объект – сервер передаёт ссылку системе и освобождает свою, т.е. всё делается примерно таким образом:

   ICreateErrorInfo *pcerrinfo;   IErrorInfo *perrinfo;   HRESULT hr;   $nbsp;   hr = ::CreateErrorInfo(pcerrinfo);        . . .     //заполнить объект через ICreateErrorInfo   hr = pcerrinfo->QueryInterface(IID_IErrorInfo, (LPVOID FAR*) perrinfo);   if(SUCCEEDED(hr)){     ::SetErrorInfo(0, perrinfo);     perrinfo->Release();                    }   pcerrinfo->Release();

после чего сервер может возвратить управление клиенту как обычно - завершив исполнение метода с возвратом HRESULT. На клиентской же стороне программа обращается к функции системного API GetErrorInfo, которая извлекает переданный сервером объект ошибки:

HRESULT GetErrorInfo(DWORD  dwReserved,IErrorInfo  **pperrinfo);

Возможно, что в уже "промаршалированном виде", но это будет ссылка на тот самый объект ошибки, который передавал сервер... После того, как надобность в полученном объекте ошибки у клиента минет, он должен просто отпустить на него ссылку. Вот так:

   IErrorInfo *perrinfo;   HRESULT hr;     hr = ::GetErrorInfo(perrinfo);   if(SUCCEEDED(hr)){     . . .     //прочитать значения через IErrorInfo     perrinfo->Release();                    }

Следует обратить особенное ваше внимание на одно подразумеваемое при использовании данного механизма передачи ошибки обстоятельство – этот механизм в точности повторяет механизм errno из CRT. А errno обеспечивается в контексте каждого потока, а не процесса в целом. Понятно почему – необходимо предотвратить вмешательство параллельных потоков между установкой и чтением errno. COM, посредством механизма proxy/stub эмулирует т.н. "логический поток исполнения программы" (как мы видели ранее, фактически в этом принимает участие два разных физических потока), который, с точки зрения клиента, аналогичен системному физическому потоку. Естественно, что система предохраняет объект ошибки от вмешательства параллельных потоков, поэтому никаких специальных мер по синхронизации потоков при использовании SetErrorInfo/GetErrorInfo не требуется – с этим механизмом можно работать одинаково во всех потоковых моделях.

Но и это - отнюдь не всё. Механизм IErrorInfo – опционален, т.е. сервер может использовать его, а может обходиться только возвратом кода ошибки через HRESULT. Как клиенту узнать – HRESULT с кодом ошибки, который вернул метод сервера, это – всё, чем располагает клиент или же клиент может выяснить и больше? Существует специальное соглашение, которому должен следовать интерфейс, если он реализует механизм передачи ошибки посредством IErrorInfo.

Во-первых, все методы данного интерфейса либо всегда должны пользоваться SetErrorInfo, когда возвращают код ошибки, либо – никакой метод данного интерфейса этим пользоваться не должен. Во-вторых, объект, имеющий реализации интерфейсов использующие SetErrorInfo должен вести у себя реестр интерфейсов, которые в состоянии предоставить клиенту IErrorInfo. В-третьих, объект должен реализовывать в составе своих интерфейсов специальный интерфейс ISupportErrorInfo, пользуясь которым клиент может выяснить – получив через HRESULT состояние ошибки от метода некоего интерфейса может ли клиент рассчитывать на то, что этот интерфейс предоставил и заполненный IErrorInfo?

Интерфейс ISupportErrorInfo также наследует интерфейсу IDispatch и состоит из одного своего метода:

1.QueryInterface
2.AddRef
3.Release
 
4.GetTypeInfoCount
5.GetTypeInfo
6.GetIDsOfNames
7.Invoke
 
8.InterfaceSupportsErrorInfoВозвращает признак, поддерживает ли данный интерфейс механизм ISupportInfo

Полезный метод данного интерфейса описывается так:

HRESULT InterfaceSupportsErrorInfo(REFIID  riid); 

он принимает в качестве аргумента ссылку на IID интерфейса, вернувшего состояние ошибки, а возвращает значение S_OK, если этот интерфейс передаёт объекты ошибок и S_FALSE в противном случае. Т.е. действия клиента по извлечению всей доступной ему информации, возвращаемой сервером при возникновении ошибки выглядят примерно таким образом:

   IErrorInfo *perrinfo;   ISupportErrorInfo *psupeinfo;   HRESULT hr_obj,hr;     hr_obj = obj->Method(...);//вызываем метод интерфейса с IID == IID_MyItf   if(FAILED(hr_obj)){     hr = obj->QueryInterface(IID_ISupportErrorInfo, (LPVOID FAR*) psupeinfo);     if(SUCCEEDED(hr)){       hr = psupeinfo->InterfaceSupportsErrorInfo(IID_MyItf);       if(SUCCEEDED(hr)){         hr = ::GetErrorInfo(perrinfo);         if(SUCCEEDED(hr)){           . . .     //прочитать значения через IErrorInfo           perrinfo->Release();                          }                        }         psupeinfo->Release();                         }                  }

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

В следующей статье мы рассмотрим необходимую для возврата к теме о библиотеке типа информацию о наследовании в COM...

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

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

форум

технология COM

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


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